260 lines
9.0 KiB
HTML
260 lines
9.0 KiB
HTML
<!-- eslint-disable @angular-eslint/template/button-has-type, @angular-eslint/template/prefer-ngsrc -->
|
|
<div #composerRoot>
|
|
@if (replyTo()) {
|
|
<div class="pointer-events-auto flex items-center gap-2 bg-secondary/50 px-4 py-2">
|
|
<ng-icon
|
|
name="lucideReply"
|
|
class="h-4 w-4 text-muted-foreground"
|
|
/>
|
|
<span class="flex-1 text-sm text-muted-foreground">
|
|
Replying to <span class="font-semibold">{{ replyTo()?.senderName }}</span>
|
|
</span>
|
|
<button
|
|
(click)="clearReply()"
|
|
class="rounded p-1 hover:bg-secondary"
|
|
>
|
|
<ng-icon
|
|
name="lucideX"
|
|
class="h-4 w-4 text-muted-foreground"
|
|
/>
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<app-typing-indicator />
|
|
|
|
@if (toolbarVisible()) {
|
|
<div
|
|
class="pointer-events-auto"
|
|
(mousedown)="$event.preventDefault()"
|
|
(mouseenter)="onToolbarMouseEnter()"
|
|
(mouseleave)="onToolbarMouseLeave()"
|
|
>
|
|
<div
|
|
class="mx-4 -mb-2 flex flex-wrap items-center justify-start gap-2 rounded-lg border border-border bg-card/70 px-2 py-1 shadow-sm backdrop-blur"
|
|
>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyInline('**')"
|
|
>
|
|
<b>B</b>
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyInline('*')"
|
|
>
|
|
<i>I</i>
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyInline('~~')"
|
|
>
|
|
<s>S</s>
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyInline(inlineCodeToken)"
|
|
>
|
|
`
|
|
</button>
|
|
<span class="mx-1 text-muted-foreground">|</span>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyHeading(1)"
|
|
>
|
|
H1
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyHeading(2)"
|
|
>
|
|
H2
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyHeading(3)"
|
|
>
|
|
H3
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyPrefix('> ')"
|
|
>
|
|
Quote
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyPrefix('- ')"
|
|
>
|
|
• List
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyOrderedList()"
|
|
>
|
|
1. List
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyCodeBlock()"
|
|
>
|
|
Code
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyLink()"
|
|
>
|
|
Link
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyImage()"
|
|
>
|
|
Image
|
|
</button>
|
|
<button
|
|
class="rounded px-2 py-1 text-xs hover:bg-secondary"
|
|
(click)="applyHorizontalRule()"
|
|
>
|
|
HR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="border-border p-4">
|
|
<div
|
|
class="chat-input-wrapper relative"
|
|
(mouseenter)="inputHovered.set(true)"
|
|
(mouseleave)="inputHovered.set(false)"
|
|
(dragenter)="onDragEnter($event)"
|
|
(dragover)="onDragOver($event)"
|
|
(dragleave)="onDragLeave($event)"
|
|
(drop)="onDrop($event)"
|
|
>
|
|
<div class="absolute bottom-3 right-3 z-10 flex items-center gap-2">
|
|
@if (klipy.isEnabled()) {
|
|
<button
|
|
type="button"
|
|
(click)="openKlipyGifPicker()"
|
|
class="inline-flex h-10 min-w-10 items-center justify-center gap-1.5 rounded-2xl border border-border/70 bg-secondary/55 px-3 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground shadow-sm backdrop-blur-md transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/35 hover:bg-secondary/90 hover:text-foreground"
|
|
[class.border-primary]="showKlipyGifPicker()"
|
|
[class.opacity-100]="inputHovered() || showKlipyGifPicker()"
|
|
[class.opacity-70]="!inputHovered() && !showKlipyGifPicker()"
|
|
[class.shadow-none]="!inputHovered() && !showKlipyGifPicker()"
|
|
[class.text-primary]="showKlipyGifPicker()"
|
|
aria-label="Search KLIPY GIFs"
|
|
title="Search KLIPY GIFs"
|
|
>
|
|
<ng-icon
|
|
name="lucideImage"
|
|
class="h-4 w-4"
|
|
/>
|
|
<span class="hidden sm:inline">GIF</span>
|
|
</button>
|
|
}
|
|
|
|
<button
|
|
type="button"
|
|
(click)="sendMessage()"
|
|
[disabled]="!messageContent.trim() && pendingFiles.length === 0 && !pendingKlipyGif()"
|
|
class="send-btn visible inline-flex h-11 w-11 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-lg shadow-primary/25 ring-1 ring-primary/20 transition-all duration-200 hover:-translate-y-0.5 hover:bg-primary/90 disabled:translate-y-0 disabled:cursor-not-allowed disabled:bg-secondary disabled:text-muted-foreground disabled:shadow-none disabled:ring-0"
|
|
aria-label="Send message"
|
|
title="Send message"
|
|
>
|
|
<ng-icon
|
|
name="lucideSend"
|
|
class="h-5 w-5"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<textarea
|
|
#messageInputRef
|
|
rows="1"
|
|
[(ngModel)]="messageContent"
|
|
(focus)="onInputFocus()"
|
|
(blur)="onInputBlur()"
|
|
(keydown.enter)="onEnter($event)"
|
|
(input)="onInputChange(); autoResizeTextarea()"
|
|
(dragenter)="onDragEnter($event)"
|
|
(dragover)="onDragOver($event)"
|
|
(dragleave)="onDragLeave($event)"
|
|
(drop)="onDrop($event)"
|
|
placeholder="Type a message..."
|
|
class="chat-textarea w-full rounded-[1.35rem] border border-border py-2 pl-4 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
[class.border-dashed]="dragActive()"
|
|
[class.border-primary]="dragActive()"
|
|
[class.ctrl-resize]="ctrlHeld()"
|
|
[class.pr-16]="!klipy.isEnabled()"
|
|
[class.pr-40]="klipy.isEnabled()"
|
|
></textarea>
|
|
|
|
@if (dragActive()) {
|
|
<div
|
|
class="pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary bg-primary/5"
|
|
>
|
|
<div class="text-sm text-muted-foreground">Drop files to attach</div>
|
|
</div>
|
|
}
|
|
|
|
@if (pendingKlipyGif()) {
|
|
<div class="mt-2 flex">
|
|
<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">
|
|
<img
|
|
[src]="getPendingKlipyGifPreviewUrl()"
|
|
[alt]="pendingKlipyGif()!.title || 'KLIPY GIF'"
|
|
class="h-full w-full object-cover"
|
|
loading="lazy"
|
|
/>
|
|
<span
|
|
class="absolute bottom-1 left-1 rounded bg-black/70 px-1.5 py-0.5 text-[8px] font-semibold uppercase tracking-[0.18em] text-white/90"
|
|
>
|
|
KLIPY
|
|
</span>
|
|
</div>
|
|
<div class="min-w-0">
|
|
<div class="text-xs font-medium text-foreground">GIF ready to send</div>
|
|
<div class="max-w-[12rem] truncate text-[10px] text-muted-foreground">
|
|
{{ pendingKlipyGif()!.title || 'KLIPY GIF' }}
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
(click)="removePendingKlipyGif()"
|
|
class="rounded px-2 py-1 text-[10px] text-destructive transition-colors hover:bg-destructive/10"
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (pendingFiles.length > 0) {
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
@for (file of pendingFiles; track file.name) {
|
|
<div class="group flex items-center gap-2 rounded border border-border bg-secondary/60 px-2 py-1">
|
|
<div class="max-w-[14rem] truncate text-xs font-medium">{{ file.name }}</div>
|
|
<div class="text-[10px] text-muted-foreground">{{ formatBytes(file.size) }}</div>
|
|
<button
|
|
(click)="removePendingFile(file)"
|
|
class="rounded bg-destructive/20 px-1 py-0.5 text-[10px] text-destructive opacity-70 group-hover:opacity-100"
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (showKlipyGifPicker() && klipy.isEnabled()) {
|
|
<app-klipy-gif-picker
|
|
(gifSelected)="handleKlipyGifSelected($event)"
|
|
(closed)="closeKlipyGifPicker()"
|
|
/>
|
|
}
|