- @if (!msg.isDeleted) {
+ @if (!msg.isDeleted && !isMobile()) {
}
+
+
+
+
+
+
React
+
+ @for (emoji of commonEmojis; track emoji) {
+
+ }
+
+
+
+
+
+
+
+
+
+ @if (isOwnMessage()) {
+
+ }
+
+ @if (isOwnMessage() || isAdmin()) {
+
+ }
+
+
+
diff --git a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.ts b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.ts
index 526252e..2efdcfc 100644
--- a/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.ts
+++ b/toju-app/src/app/domains/chat/feature/chat-messages/components/message-item/chat-message-item.component.ts
@@ -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
;
+ @ViewChild('mobileSheetTpl') mobileSheetTpl?: TemplateRef;
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(null);
private readonly mediaSupportCache = new Map();
@@ -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 {
+ 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,
diff --git a/toju-app/src/app/domains/chat/feature/klipy-gif-picker/klipy-gif-picker.component.html b/toju-app/src/app/domains/chat/feature/klipy-gif-picker/klipy-gif-picker.component.html
index aafd437..ea999b4 100644
--- a/toju-app/src/app/domains/chat/feature/klipy-gif-picker/klipy-gif-picker.component.html
+++ b/toju-app/src/app/domains/chat/feature/klipy-gif-picker/klipy-gif-picker.component.html
@@ -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)"
>
-
-
-
KLIPY
-
Choose a GIF
-
- {{ searchQuery.trim() ? 'Search results from KLIPY.' : 'Trending GIFs from KLIPY.' }}
-
-
+ @if (!isMobile()) {
+
+
+
KLIPY
+
Choose a GIF
+
+ {{ searchQuery.trim() ? 'Search results from KLIPY.' : 'Trending GIFs from KLIPY.' }}
+
+
-
-
+
+
+ }
@@ -80,12 +82,14 @@
} @else {
-
+
@for (gif of results(); track gif.id) {
}
+
+ @if (isMobile() && hasNext()) {
+
+
+
+ }
}
-