diff --git a/.env.example b/.env.example index 533927b..29c036c 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,7 @@ TZ=UTC # Copy this file to .env and customize # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.7.5 +JAMA_VERSION=0.7.7 # Default admin credentials (used on FIRST RUN only) ADMIN_NAME=Admin User diff --git a/backend/package.json b/backend/package.json index b920ed7..55bf090 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.7.5", + "version": "0.7.7", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index 3e96346..ce7bbd3 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.7.5}" +VERSION="${1:-0.7.7}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index d97fe07..1783964 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.7.5", + "version": "0.7.7", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/ChatWindow.jsx b/frontend/src/components/ChatWindow.jsx index 8f70f18..8a3ee69 100644 --- a/frontend/src/components/ChatWindow.jsx +++ b/frontend/src/components/ChatWindow.jsx @@ -233,7 +233,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMess {group.type === 'public' ? '#' : group.is_direct ? (group.peer_real_name || group.name)[0]?.toUpperCase() : group.name[0]?.toUpperCase()} )} - {!!group.is_direct && group.peer_id && onlineUserIds.has(group.peer_id) && ( + {!!group.is_direct && group.peer_id && (onlineUserIds instanceof Set ? onlineUserIds.has(Number(group.peer_id)) : false) && ( !p); }; - // Long press for mobile action menu + // Long press for mobile action menu (DMs only) const handleTouchStart = () => { + if (!isDirect) return; longPressTimer.current = setTimeout(() => setShowOptionsMenu(true), 500); }; const handleTouchEnd = () => { @@ -165,13 +166,13 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl {!isOwn && !prevSameUser && (
setShowProfile(p => !p)} onMouseEnter={e => e.currentTarget.style.boxShadow = '0 0 0 2px var(--primary)'} onMouseLeave={e => e.currentTarget.style.boxShadow = 'none'} > - {onlineUserIds.has(msg.user_id) && ( + {!!(onlineUserIds instanceof Set ? onlineUserIds.has(Number(msg.user_id)) : false) && (
setShowActions(true)} - onMouseLeave={() => { if (!showEmojiPicker) setShowActions(false); }} - onTouchStart={isDirect ? handleTouchStart : undefined} - onTouchEnd={isDirect ? handleTouchEnd : undefined} - onTouchMove={isDirect ? handleTouchEnd : undefined} + onMouseLeave={() => { if (!showEmojiPicker && !showOptionsMenu) setShowActions(false); }} + onTouchStart={handleTouchStart} + onTouchEnd={handleTouchEnd} + onTouchMove={handleTouchEnd} > {/* Actions toolbar — floats above the bubble, aligned to correct side */} {!isDeleted && (showActions || showEmojiPicker) && ( @@ -238,14 +239,6 @@ export default function Message({ message: msg, prevMessage, currentUser, onRepl )} - {isDirect && ( -
- -
- )} - {/* Emoji picker anchored to the toolbar */} {showEmojiPicker && (
{formatTime(msg.created_at)} - {/* Pin/unpin options menu — desktop triple-dot + mobile long-press */} - {isDirect && showOptionsMenu && ( -
e.stopPropagation()} - > - {isPinned ? ( - - ) : ( - + {showOptionsMenu && ( +
e.stopPropagation()} > - - {pinCount >= 5 ? 'Pin (5/5)' : `Pin (${pinCount + 1}/5)`} - + {isPinned ? ( + + ) : ( + + )} +
)}
)} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 818057e..2549322 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -102,7 +102,7 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica const unreadCount = unreadGroups.get(group.id) || 0; const hasUnread = unreadCount > 0; const isActive = group.id === activeGroupId; - const isOnline = !!group.is_direct && !!group.peer_id && onlineUserIds.has(group.peer_id); + const isOnline = !!group.is_direct && !!group.peer_id && (onlineUserIds instanceof Set ? onlineUserIds.has(Number(group.peer_id)) : false); return (
setOnlineUserIds(prev => new Set([...prev, userId])); - const handleUserOffline = ({ userId }) => setOnlineUserIds(prev => { const n = new Set(prev); n.delete(userId); return n; }); - const handleUsersOnline = ({ userIds }) => setOnlineUserIds(new Set(userIds)); + const handleUserOnline = ({ userId }) => setOnlineUserIds(prev => new Set([...prev, Number(userId)])); + const handleUserOffline = ({ userId }) => setOnlineUserIds(prev => { const n = new Set(prev); n.delete(Number(userId)); return n; }); + const handleUsersOnline = ({ userIds }) => setOnlineUserIds(new Set((userIds || []).map(Number))); socket.on('user:online', handleUserOnline); socket.on('user:offline', handleUserOffline);