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"
>
+ ,
+ 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