diff --git a/.env.example b/.env.example index 8da7847..15c04a1 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ PROJECT_NAME=jama # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.9.12 +JAMA_VERSION=0.9.14 # App port — the host port Docker maps to the container PORT=3000 diff --git a/backend/package.json b/backend/package.json index f930b8a..8fd063f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.9.12", + "version": "0.9.14", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index f490be0..bc90722 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.9.12}" +VERSION="${1:-0.9.14}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index bebed99..9165d47 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.9.12", + "version": "0.9.14", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/ImageLightbox.jsx b/frontend/src/components/ImageLightbox.jsx index de843f0..24b63c1 100644 --- a/frontend/src/components/ImageLightbox.jsx +++ b/frontend/src/components/ImageLightbox.jsx @@ -1,22 +1,27 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; export default function ImageLightbox({ src, onClose }) { const overlayRef = useRef(null); - const imgRef = useRef(null); // Close on Escape useEffect(() => { const handler = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); + // Prevent body scroll while open + document.body.style.overflow = 'hidden'; + return () => { + window.removeEventListener('keydown', handler); + document.body.style.overflow = ''; + }; }, [onClose]); - return ( + return createPortal(
e.target === overlayRef.current && onClose()} style={{ - position: 'fixed', inset: 0, zIndex: 2000, + position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.92)', display: 'flex', alignItems: 'center', justifyContent: 'center', touchAction: 'pinch-zoom', @@ -30,8 +35,9 @@ export default function ImageLightbox({ src, onClose }) { background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: '50%', width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', - cursor: 'pointer', color: 'white', zIndex: 2001, + cursor: 'pointer', color: 'white', zIndex: 10000, }} + title="Close" > @@ -47,7 +53,7 @@ export default function ImageLightbox({ src, onClose }) { background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: '50%', width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center', - cursor: 'pointer', color: 'white', zIndex: 2001, textDecoration: 'none', + cursor: 'pointer', color: 'white', zIndex: 10000, textDecoration: 'none', }} title="Download" > @@ -60,19 +66,20 @@ export default function ImageLightbox({ src, onClose }) { {/* Image — fit to screen, browser handles pinch-zoom natively */} Full size e.stopPropagation()} /> -
+ , + document.body ); } diff --git a/frontend/src/components/Message.css b/frontend/src/components/Message.css index 02654d9..aa59c8a 100644 --- a/frontend/src/components/Message.css +++ b/frontend/src/components/Message.css @@ -328,7 +328,7 @@ .msg-bubble.emoji-only::after { display: none; } .msg-text.emoji-msg { - font-size: 48px; + font-size: 3em; line-height: 1.1; margin: 0; user-select: text; diff --git a/frontend/src/components/MessageInput.jsx b/frontend/src/components/MessageInput.jsx index f43c00a..53e6637 100644 --- a/frontend/src/components/MessageInput.jsx +++ b/frontend/src/components/MessageInput.jsx @@ -166,10 +166,33 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on await onSend({ content: trimmed || null, imageFile, linkPreview: lp, emojiOnly }); }; - // Send a single emoji directly (from picker) - const handleEmojiSend = async (emoji) => { + // Insert emoji at cursor position in the textarea + const handleEmojiSelect = (emoji) => { setShowEmojiPicker(false); - await onSend({ content: emoji.native, imageFile: null, linkPreview: null, emojiOnly: true }); + const el = inputRef.current; + const native = emoji.native; + + if (el) { + const start = el.selectionStart ?? 0; + const end = el.selectionEnd ?? 0; + const newText = text.slice(0, start) + native + text.slice(end); + setText(newText); + // Restore focus and move cursor after the inserted emoji + requestAnimationFrame(() => { + el.focus(); + const pos = start + native.length; + el.setSelectionRange(pos, pos); + // Resize textarea + el.style.height = 'auto'; + const lineHeight = parseFloat(getComputedStyle(el).lineHeight); + const maxHeight = lineHeight * 5 + 20; + el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px'; + el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden'; + }); + } else { + // No ref yet — just append + setText(prev => prev + native); + } }; const compressImage = (file) => new Promise((resolve) => { @@ -326,7 +349,7 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on