feat: Response mobile layout support v1
All checks were successful
Queue Release Build / prepare (push) Successful in 1m6s
Deploy Web Apps / deploy (push) Successful in 7m35s
Queue Release Build / build-windows (push) Successful in 29m57s
Queue Release Build / build-linux (push) Successful in 46m28s
Queue Release Build / finalize (push) Successful in 49s
All checks were successful
Queue Release Build / prepare (push) Successful in 1m6s
Deploy Web Apps / deploy (push) Successful in 7m35s
Queue Release Build / build-windows (push) Successful in 29m57s
Queue Release Build / build-linux (push) Successful in 46m28s
Queue Release Build / finalize (push) Successful in 49s
This commit is contained in:
@@ -40,31 +40,43 @@
|
||||
</div>
|
||||
|
||||
@if (showKlipyGifPicker()) {
|
||||
<div
|
||||
class="fixed inset-0 z-[89]"
|
||||
(click)="closeKlipyGifPicker()"
|
||||
(keydown.enter)="closeKlipyGifPicker()"
|
||||
(keydown.space)="closeKlipyGifPicker()"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="Close GIF picker"
|
||||
style="-webkit-app-region: no-drag"
|
||||
></div>
|
||||
|
||||
<div class="pointer-events-none fixed inset-0 z-[90]">
|
||||
@if (isMobile()) {
|
||||
<app-bottom-sheet (dismissed)="closeKlipyGifPicker()">
|
||||
<div appThemeNode="chatGifPickerSurface">
|
||||
<app-klipy-gif-picker
|
||||
[signalSource]="currentRoom()"
|
||||
(gifSelected)="handleKlipyGifSelected($event)"
|
||||
(closed)="closeKlipyGifPicker()"
|
||||
/>
|
||||
</div>
|
||||
</app-bottom-sheet>
|
||||
} @else {
|
||||
<div
|
||||
appThemeNode="chatGifPickerSurface"
|
||||
class="pointer-events-auto absolute w-[calc(100vw-2rem)] max-w-5xl sm:w-[34rem] md:w-[42rem] xl:w-[52rem]"
|
||||
[style.bottom.px]="composerBottomPadding() + 8"
|
||||
[style.right.px]="klipyGifPickerAnchorRight()"
|
||||
>
|
||||
<app-klipy-gif-picker
|
||||
[signalSource]="currentRoom()"
|
||||
(gifSelected)="handleKlipyGifSelected($event)"
|
||||
(closed)="closeKlipyGifPicker()"
|
||||
/>
|
||||
class="fixed inset-0 z-[89]"
|
||||
(click)="closeKlipyGifPicker()"
|
||||
(keydown.enter)="closeKlipyGifPicker()"
|
||||
(keydown.space)="closeKlipyGifPicker()"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="Close GIF picker"
|
||||
style="-webkit-app-region: no-drag"
|
||||
></div>
|
||||
|
||||
<div class="pointer-events-none fixed inset-0 z-[90]">
|
||||
<div
|
||||
appThemeNode="chatGifPickerSurface"
|
||||
class="pointer-events-auto absolute w-[calc(100vw-2rem)] max-w-5xl sm:w-[34rem] md:w-[42rem] xl:w-[52rem]"
|
||||
[style.bottom.px]="composerBottomPadding() + 8"
|
||||
[style.right.px]="klipyGifPickerAnchorRight()"
|
||||
>
|
||||
<app-klipy-gif-picker
|
||||
[signalSource]="currentRoom()"
|
||||
(gifSelected)="handleKlipyGifSelected($event)"
|
||||
(closed)="closeKlipyGifPicker()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<app-chat-message-overlays
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||
import { ViewportService } from '../../../../core/platform';
|
||||
import { BottomSheetComponent } from '../../../../shared';
|
||||
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
||||
import { Attachment, AttachmentFacade } from '../../../attachment';
|
||||
import { KlipyGif, KlipyService } from '../../application/services/klipy.service';
|
||||
@@ -45,6 +47,7 @@ import {
|
||||
KlipyGifPickerComponent,
|
||||
ChatMessageListComponent,
|
||||
ChatMessageOverlaysComponent,
|
||||
BottomSheetComponent,
|
||||
ThemeNodeDirective
|
||||
],
|
||||
templateUrl: './chat-messages.component.html',
|
||||
@@ -59,6 +62,9 @@ export class ChatMessagesComponent {
|
||||
private readonly webrtc = inject(RealtimeSessionFacade);
|
||||
private readonly attachmentsSvc = inject(AttachmentFacade);
|
||||
private readonly klipy = inject(KlipyService);
|
||||
private readonly viewport = inject(ViewportService);
|
||||
|
||||
readonly isMobile = this.viewport.isMobile;
|
||||
|
||||
readonly allMessages = this.store.selectSignal(selectAllMessages);
|
||||
private readonly activeChannelId = this.store.selectSignal(selectActiveChannelId);
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
[attr.data-message-id]="msg.id"
|
||||
class="group relative flex gap-3 rounded-lg p-2 transition-colors hover:bg-secondary/30"
|
||||
[class.opacity-50]="msg.isDeleted"
|
||||
(touchstart)="onMessageTouchStart($event)"
|
||||
(touchend)="onMessageTouchEnd()"
|
||||
(touchmove)="onMessageTouchEnd()"
|
||||
(touchcancel)="onMessageTouchEnd()"
|
||||
>
|
||||
<div
|
||||
appThemeNode="chatMessageAvatar"
|
||||
@@ -469,7 +473,7 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!msg.isDeleted) {
|
||||
@if (!msg.isDeleted && !isMobile()) {
|
||||
<div
|
||||
appThemeNode="chatMessageActions"
|
||||
class="absolute right-2 top-2 flex items-center gap-1 rounded-lg border border-border bg-card shadow-lg opacity-0 transition-opacity group-hover:opacity-100"
|
||||
@@ -534,4 +538,83 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-template #mobileSheetTpl>
|
||||
<app-bottom-sheet
|
||||
title="Message"
|
||||
ariaLabel="Message actions"
|
||||
(dismissed)="closeMobileActions()"
|
||||
>
|
||||
<div class="flex flex-col py-1">
|
||||
<div class="px-3 pb-2 pt-1">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-muted-foreground">React</p>
|
||||
<div class="mt-2 grid grid-cols-8 gap-1">
|
||||
@for (emoji of commonEmojis; track emoji) {
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-xl transition-colors hover:bg-secondary"
|
||||
(click)="onMobileReact(emoji)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-1 h-px bg-border"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
||||
(click)="onMobileReply()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideReply"
|
||||
class="h-5 w-5 text-muted-foreground"
|
||||
/>
|
||||
<span>Reply</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
||||
(click)="onMobileCopy()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideCopy"
|
||||
class="h-5 w-5 text-muted-foreground"
|
||||
/>
|
||||
<span>Copy message content</span>
|
||||
</button>
|
||||
|
||||
@if (isOwnMessage()) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-foreground transition-colors hover:bg-secondary"
|
||||
(click)="onMobileEdit()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideEdit"
|
||||
class="h-5 w-5 text-muted-foreground"
|
||||
/>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (isOwnMessage() || isAdmin()) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-destructive transition-colors hover:bg-destructive/10"
|
||||
(click)="onMobileDelete()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideTrash2"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</app-bottom-sheet>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@@ -8,15 +8,21 @@ import {
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
OnDestroy,
|
||||
output,
|
||||
signal,
|
||||
ViewChild
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { TemplatePortal } from '@angular/cdk/portal';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
lucideCheck,
|
||||
lucideCopy,
|
||||
lucideDownload,
|
||||
lucideEdit,
|
||||
lucideExpand,
|
||||
@@ -34,7 +40,7 @@ import {
|
||||
MAX_BROWSER_INLINE_MEDIA_SIZE_BYTES,
|
||||
MAX_AUTO_SAVE_SIZE_BYTES
|
||||
} from '../../../../../attachment';
|
||||
import { PlatformService } from '../../../../../../core/platform';
|
||||
import { PlatformService, ViewportService } from '../../../../../../core/platform';
|
||||
import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service';
|
||||
import {
|
||||
ExperimentalMediaSettingsService
|
||||
@@ -52,6 +58,7 @@ import { PluginRenderHostComponent } from '../../../../../plugins/feature/plugin
|
||||
import { PluginRequirementStateService, PluginUiRegistryService } from '../../../../../plugins';
|
||||
|
||||
import {
|
||||
BottomSheetComponent,
|
||||
ChatAudioPlayerComponent,
|
||||
ChatVideoPlayerComponent,
|
||||
ProfileCardService,
|
||||
@@ -125,11 +132,13 @@ interface MissingPluginEmbedFallback {
|
||||
UserAvatarComponent,
|
||||
PluginRenderHostComponent,
|
||||
ExperimentalVlcPlayerComponent,
|
||||
ThemeNodeDirective
|
||||
ThemeNodeDirective,
|
||||
BottomSheetComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideCheck,
|
||||
lucideCopy,
|
||||
lucideDownload,
|
||||
lucideEdit,
|
||||
lucideExpand,
|
||||
@@ -148,8 +157,9 @@ interface MissingPluginEmbedFallback {
|
||||
style: 'display: contents;'
|
||||
}
|
||||
})
|
||||
export class ChatMessageItemComponent {
|
||||
export class ChatMessageItemComponent implements OnDestroy {
|
||||
@ViewChild('editTextareaRef') editTextareaRef?: ElementRef<HTMLTextAreaElement>;
|
||||
@ViewChild('mobileSheetTpl') mobileSheetTpl?: TemplateRef<unknown>;
|
||||
|
||||
private readonly attachmentsSvc = inject(AttachmentFacade);
|
||||
private readonly klipy = inject(KlipyService);
|
||||
@@ -160,6 +170,13 @@ export class ChatMessageItemComponent {
|
||||
private readonly experimentalMedia = inject(ExperimentalMediaSettingsService);
|
||||
private readonly profileCard = inject(ProfileCardService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly viewport = inject(ViewportService);
|
||||
private readonly overlay = inject(Overlay);
|
||||
private readonly viewContainerRef = inject(ViewContainerRef);
|
||||
private mobileSheetOverlayRef: OverlayRef | null = null;
|
||||
private longPressTimer: number | null = null;
|
||||
readonly isMobile = this.viewport.isMobile;
|
||||
readonly mobileSheetOpen = signal(false);
|
||||
private readonly attachmentVersion = signal(this.attachmentsSvc.updated());
|
||||
private readonly experimentalPlayerAttachmentId = signal<string | null>(null);
|
||||
private readonly mediaSupportCache = new Map<string, boolean>();
|
||||
@@ -360,6 +377,116 @@ export class ChatMessageItemComponent {
|
||||
this.deleteRequested.emit(this.message());
|
||||
}
|
||||
|
||||
onMessageTouchStart(event: TouchEvent): void {
|
||||
if (!this.isMobile() || this.message().isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.touches.length !== 1) {
|
||||
this.clearLongPressTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEditableTarget(event.target)) {
|
||||
this.clearLongPressTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearLongPressTimer();
|
||||
this.longPressTimer = window.setTimeout(() => {
|
||||
this.longPressTimer = null;
|
||||
this.openMobileSheet();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
onMessageTouchEnd(): void {
|
||||
this.clearLongPressTimer();
|
||||
}
|
||||
|
||||
private clearLongPressTimer(): void {
|
||||
if (this.longPressTimer !== null) {
|
||||
window.clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof Element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"]') !== null;
|
||||
}
|
||||
|
||||
closeMobileActions(): void {
|
||||
this.detachMobileSheet();
|
||||
}
|
||||
|
||||
private openMobileSheet(): void {
|
||||
if (this.mobileSheetOverlayRef || !this.mobileSheetTpl) {
|
||||
this.mobileSheetOpen.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const overlayRef = this.overlay.create({
|
||||
positionStrategy: this.overlay.position().global(),
|
||||
scrollStrategy: this.overlay.scrollStrategies.block(),
|
||||
hasBackdrop: false,
|
||||
panelClass: 'metoyou-chat-actions-sheet-pane'
|
||||
});
|
||||
const portal = new TemplatePortal(this.mobileSheetTpl, this.viewContainerRef);
|
||||
|
||||
overlayRef.attach(portal);
|
||||
this.mobileSheetOverlayRef = overlayRef;
|
||||
this.mobileSheetOpen.set(true);
|
||||
}
|
||||
|
||||
private detachMobileSheet(): void {
|
||||
this.mobileSheetOpen.set(false);
|
||||
|
||||
if (this.mobileSheetOverlayRef) {
|
||||
this.mobileSheetOverlayRef.dispose();
|
||||
this.mobileSheetOverlayRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.clearLongPressTimer();
|
||||
this.detachMobileSheet();
|
||||
}
|
||||
|
||||
onMobileReact(emoji: string): void {
|
||||
this.addReaction(emoji);
|
||||
this.closeMobileActions();
|
||||
}
|
||||
|
||||
onMobileReply(): void {
|
||||
this.requestReply();
|
||||
this.closeMobileActions();
|
||||
}
|
||||
|
||||
onMobileEdit(): void {
|
||||
this.startEdit();
|
||||
this.closeMobileActions();
|
||||
}
|
||||
|
||||
onMobileDelete(): void {
|
||||
this.requestDelete();
|
||||
this.closeMobileActions();
|
||||
}
|
||||
|
||||
async onMobileCopy(): Promise<void> {
|
||||
const text = this.message().content;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
// Clipboard API unavailable; silently ignore.
|
||||
}
|
||||
|
||||
this.closeMobileActions();
|
||||
}
|
||||
|
||||
removeEmbed(url: string): void {
|
||||
this.embedRemoved.emit({
|
||||
messageId: this.message().id,
|
||||
|
||||
@@ -4,27 +4,29 @@
|
||||
aria-label="KLIPY GIF picker"
|
||||
style="background: hsl(var(--background) / 0.85); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px)"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4 border-b border-border/70 bg-secondary/15 px-5 py-4">
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-[0.28em] text-primary">KLIPY</div>
|
||||
<h3 class="mt-1 text-lg font-semibold text-foreground">Choose a GIF</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
{{ searchQuery.trim() ? 'Search results from KLIPY.' : 'Trending GIFs from KLIPY.' }}
|
||||
</p>
|
||||
</div>
|
||||
@if (!isMobile()) {
|
||||
<div class="flex items-start justify-between gap-4 border-b border-border/70 bg-secondary/15 px-5 py-4">
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold uppercase tracking-[0.28em] text-primary">KLIPY</div>
|
||||
<h3 class="mt-1 text-lg font-semibold text-foreground">Choose a GIF</h3>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
{{ searchQuery.trim() ? 'Search results from KLIPY.' : 'Trending GIFs from KLIPY.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="close()"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-border/70 bg-secondary/30 text-muted-foreground transition-colors hover:bg-secondary/80 hover:text-foreground"
|
||||
aria-label="Close GIF picker"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
(click)="close()"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-border/70 bg-secondary/30 text-muted-foreground transition-colors hover:bg-secondary/80 hover:text-foreground"
|
||||
aria-label="Close GIF picker"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="border-b border-border/70 bg-secondary/10 px-5 py-4">
|
||||
<label class="relative block">
|
||||
@@ -37,7 +39,7 @@
|
||||
type="text"
|
||||
[ngModel]="searchQuery"
|
||||
(ngModelChange)="onSearchQueryChanged($event)"
|
||||
placeholder="Search KLIPY"
|
||||
[placeholder]="isMobile() ? 'Search KLIPY and add a gif to the chat' : 'Search KLIPY'"
|
||||
class="relative z-0 w-full rounded-xl border border-border/80 bg-background/70 px-10 py-3 text-sm text-foreground placeholder:text-muted-foreground shadow-sm backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</label>
|
||||
@@ -80,12 +82,14 @@
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="columns-[12rem] gap-4">
|
||||
<div [class]="isMobile() ? 'grid grid-cols-2 gap-2' : 'columns-[12rem] gap-4'">
|
||||
@for (gif of results(); track gif.id) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="selectGif(gif)"
|
||||
class="group mx-auto mb-4 inline-block w-full max-w-[15.5rem] break-inside-avoid align-top overflow-hidden rounded-2xl border border-border/80 bg-secondary/10 text-left shadow-sm transition-transform duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:bg-secondary/30"
|
||||
[class]="isMobile()
|
||||
? 'group block w-full overflow-hidden rounded-xl border border-border/80 bg-secondary/10 text-left shadow-sm transition-transform duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:bg-secondary/30'
|
||||
: 'group mx-auto mb-4 inline-block w-full max-w-[15.5rem] break-inside-avoid align-top overflow-hidden rounded-2xl border border-border/80 bg-secondary/10 text-left shadow-sm transition-transform duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:bg-secondary/30'"
|
||||
>
|
||||
<div
|
||||
class="relative flex items-center justify-center overflow-hidden bg-secondary/30"
|
||||
@@ -104,30 +108,55 @@
|
||||
KLIPY
|
||||
</span>
|
||||
</div>
|
||||
<div class="px-3 py-2">
|
||||
<p class="truncate text-xs font-medium text-foreground">
|
||||
{{ gif.title || 'KLIPY GIF' }}
|
||||
</p>
|
||||
<p class="mt-1 text-[10px] uppercase tracking-[0.22em] text-muted-foreground">Click to select</p>
|
||||
</div>
|
||||
@if (!isMobile()) {
|
||||
<div class="px-3 py-2">
|
||||
<p class="truncate text-xs font-medium text-foreground">
|
||||
{{ gif.title || 'KLIPY GIF' }}
|
||||
</p>
|
||||
<p class="mt-1 text-[10px] uppercase tracking-[0.22em] text-muted-foreground">Click to select</p>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isMobile() && hasNext()) {
|
||||
<div class="mt-3 flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
(click)="loadMore()"
|
||||
[disabled]="loading()"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-border/80 bg-background/60 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60"
|
||||
[attr.aria-label]="loading() ? 'Loading more GIFs' : 'Load more GIFs'"
|
||||
>
|
||||
@if (loading()) {
|
||||
<span class="h-4 w-4 animate-spin rounded-full border-2 border-primary/20 border-t-primary"></span>
|
||||
} @else {
|
||||
<ng-icon
|
||||
name="lucideChevronDown"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 border-t border-border/70 bg-secondary/10 px-5 py-4">
|
||||
<p class="text-xs text-muted-foreground">Click a GIF to select it. Powered by KLIPY.</p>
|
||||
@if (!isMobile()) {
|
||||
<div class="flex items-center justify-between gap-4 border-t border-border/70 bg-secondary/10 px-5 py-4">
|
||||
<p class="text-xs text-muted-foreground">Click a GIF to select it. Powered by KLIPY.</p>
|
||||
|
||||
@if (hasNext()) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="loadMore()"
|
||||
[disabled]="loading()"
|
||||
class="rounded-full border border-border/80 bg-background/60 px-4 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{{ loading() ? 'Loading...' : 'Load more' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (hasNext()) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="loadMore()"
|
||||
[disabled]="loading()"
|
||||
class="rounded-full border border-border/80 bg-background/60 px-4 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{{ loading() ? 'Loading...' : 'Load more' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FormsModule } from '@angular/forms';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
lucideChevronDown,
|
||||
lucideImage,
|
||||
lucideSearch,
|
||||
lucideX
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import { KlipyGif, KlipyService } from '../../application/services/klipy.service';
|
||||
import type { RoomSignalSourceInput } from '../../../server-directory';
|
||||
import { ChatImageProxyFallbackDirective } from '../chat-image-proxy-fallback.directive';
|
||||
import { ViewportService } from '../../../../core/platform';
|
||||
|
||||
const KLIPY_CARD_MIN_WIDTH = 140;
|
||||
const KLIPY_CARD_MAX_WIDTH = 248;
|
||||
@@ -42,6 +44,7 @@ const KLIPY_CARD_FALLBACK_SIZE = 160;
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideChevronDown,
|
||||
lucideImage,
|
||||
lucideSearch,
|
||||
lucideX
|
||||
@@ -58,6 +61,8 @@ export class KlipyGifPickerComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
@ViewChild('searchInput') searchInput?: ElementRef<HTMLInputElement>;
|
||||
|
||||
private readonly klipy = inject(KlipyService);
|
||||
private readonly viewport = inject(ViewportService);
|
||||
readonly isMobile = this.viewport.isMobile;
|
||||
private currentPage = 1;
|
||||
private searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private requestId = 0;
|
||||
|
||||
Reference in New Issue
Block a user