v0.9.14 adjusted emoticon and image view.
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
PROJECT_NAME=jama
|
PROJECT_NAME=jama
|
||||||
|
|
||||||
# Image version to run (set by build.sh, or use 'latest')
|
# 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
|
# App port — the host port Docker maps to the container
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-backend",
|
"name": "jama-backend",
|
||||||
"version": "0.9.12",
|
"version": "0.9.14",
|
||||||
"description": "TeamChat backend server",
|
"description": "TeamChat backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.9.12}"
|
VERSION="${1:-0.9.14}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="jama"
|
IMAGE_NAME="jama"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-frontend",
|
"name": "jama-frontend",
|
||||||
"version": "0.9.12",
|
"version": "0.9.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -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 }) {
|
export default function ImageLightbox({ src, onClose }) {
|
||||||
const overlayRef = useRef(null);
|
const overlayRef = useRef(null);
|
||||||
const imgRef = useRef(null);
|
|
||||||
|
|
||||||
// Close on Escape
|
// Close on Escape
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e) => { if (e.key === 'Escape') onClose(); };
|
const handler = (e) => { if (e.key === 'Escape') onClose(); };
|
||||||
window.addEventListener('keydown', handler);
|
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]);
|
}, [onClose]);
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
ref={overlayRef}
|
ref={overlayRef}
|
||||||
onClick={(e) => e.target === overlayRef.current && onClose()}
|
onClick={(e) => e.target === overlayRef.current && onClose()}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed', inset: 0, zIndex: 2000,
|
position: 'fixed', inset: 0, zIndex: 9999,
|
||||||
background: 'rgba(0,0,0,0.92)',
|
background: 'rgba(0,0,0,0.92)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
touchAction: 'pinch-zoom',
|
touchAction: 'pinch-zoom',
|
||||||
@@ -30,8 +35,9 @@ export default function ImageLightbox({ src, onClose }) {
|
|||||||
background: 'rgba(255,255,255,0.15)', border: 'none',
|
background: 'rgba(255,255,255,0.15)', border: 'none',
|
||||||
borderRadius: '50%', width: 40, height: 40,
|
borderRadius: '50%', width: 40, height: 40,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
cursor: 'pointer', color: 'white', zIndex: 2001,
|
cursor: 'pointer', color: 'white', zIndex: 10000,
|
||||||
}}
|
}}
|
||||||
|
title="Close"
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
@@ -47,7 +53,7 @@ export default function ImageLightbox({ src, onClose }) {
|
|||||||
background: 'rgba(255,255,255,0.15)', border: 'none',
|
background: 'rgba(255,255,255,0.15)', border: 'none',
|
||||||
borderRadius: '50%', width: 40, height: 40,
|
borderRadius: '50%', width: 40, height: 40,
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
cursor: 'pointer', color: 'white', zIndex: 2001, textDecoration: 'none',
|
cursor: 'pointer', color: 'white', zIndex: 10000, textDecoration: 'none',
|
||||||
}}
|
}}
|
||||||
title="Download"
|
title="Download"
|
||||||
>
|
>
|
||||||
@@ -60,19 +66,20 @@ export default function ImageLightbox({ src, onClose }) {
|
|||||||
|
|
||||||
{/* Image — fit to screen, browser handles pinch-zoom natively */}
|
{/* Image — fit to screen, browser handles pinch-zoom natively */}
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
|
||||||
src={src}
|
src={src}
|
||||||
alt="Full size"
|
alt="Full size"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '95vw',
|
maxWidth: '92vw',
|
||||||
maxHeight: '95vh',
|
maxHeight: '92vh',
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
touchAction: 'pinch-zoom',
|
touchAction: 'pinch-zoom',
|
||||||
|
boxShadow: '0 8px 40px rgba(0,0,0,0.6)',
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,7 +328,7 @@
|
|||||||
.msg-bubble.emoji-only::after { display: none; }
|
.msg-bubble.emoji-only::after { display: none; }
|
||||||
|
|
||||||
.msg-text.emoji-msg {
|
.msg-text.emoji-msg {
|
||||||
font-size: 48px;
|
font-size: 3em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
|||||||
@@ -166,10 +166,33 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
|
|||||||
await onSend({ content: trimmed || null, imageFile, linkPreview: lp, emojiOnly });
|
await onSend({ content: trimmed || null, imageFile, linkPreview: lp, emojiOnly });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send a single emoji directly (from picker)
|
// Insert emoji at cursor position in the textarea
|
||||||
const handleEmojiSend = async (emoji) => {
|
const handleEmojiSelect = (emoji) => {
|
||||||
setShowEmojiPicker(false);
|
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) => {
|
const compressImage = (file) => new Promise((resolve) => {
|
||||||
@@ -326,7 +349,7 @@ export default function MessageInput({ group, replyTo, onCancelReply, onSend, on
|
|||||||
<div className="emoji-input-picker" ref={emojiPickerRef}>
|
<div className="emoji-input-picker" ref={emojiPickerRef}>
|
||||||
<Picker
|
<Picker
|
||||||
data={data}
|
data={data}
|
||||||
onEmojiSelect={handleEmojiSend}
|
onEmojiSelect={handleEmojiSelect}
|
||||||
theme="light"
|
theme="light"
|
||||||
previewPosition="none"
|
previewPosition="none"
|
||||||
skinTonePosition="none"
|
skinTonePosition="none"
|
||||||
|
|||||||
Reference in New Issue
Block a user