v0.8.7 pin bug fixes

This commit is contained in:
2026-03-13 11:47:33 -04:00
parent 6d435844c9
commit 83b2105a9a
6 changed files with 45 additions and 39 deletions

View File

@@ -7,7 +7,7 @@ TZ=UTC
# Copy this file to .env and customize # Copy this file to .env and customize
# 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.8.6 JAMA_VERSION=0.8.7
# Default admin credentials (used on FIRST RUN only) # Default admin credentials (used on FIRST RUN only)
ADMIN_NAME=Admin User ADMIN_NAME=Admin User

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-backend", "name": "jama-backend",
"version": "0.8.6", "version": "0.8.7",
"description": "TeamChat backend server", "description": "TeamChat backend server",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {

View File

@@ -70,11 +70,12 @@ router.post('/subscribe', authMiddleware, (req, res) => {
return res.status(400).json({ error: 'Invalid subscription' }); return res.status(400).json({ error: 'Invalid subscription' });
} }
const db = getDb(); const db = getDb();
db.prepare(` // Use DELETE+INSERT to avoid relying on any specific UNIQUE constraint
INSERT INTO push_subscriptions (user_id, endpoint, p256dh, auth) // (existing DBs may have different schemas for this table)
VALUES (?, ?, ?, ?) const delStmt = db.prepare('DELETE FROM push_subscriptions WHERE endpoint = ?');
ON CONFLICT(endpoint) DO UPDATE SET user_id = ?, p256dh = ?, auth = ? const insStmt = db.prepare('INSERT INTO push_subscriptions (user_id, endpoint, p256dh, auth) VALUES (?, ?, ?, ?)');
`).run(req.user.id, endpoint, keys.p256dh, keys.auth, req.user.id, keys.p256dh, keys.auth); delStmt.run(endpoint);
insStmt.run(req.user.id, endpoint, keys.p256dh, keys.auth);
res.json({ success: true }); res.json({ success: true });
}); });

View File

@@ -13,7 +13,7 @@
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
set -euo pipefail set -euo pipefail
VERSION="${1:-0.8.6}" VERSION="${1:-0.8.7}"
ACTION="${2:-}" ACTION="${2:-}"
REGISTRY="${REGISTRY:-}" REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama" IMAGE_NAME="jama"

View File

@@ -1,6 +1,6 @@
{ {
"name": "jama-frontend", "name": "jama-frontend",
"version": "0.8.6", "version": "0.8.7",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -2,39 +2,40 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import Message from './Message.jsx'; import Message from './Message.jsx';
import MessageInput from './MessageInput.jsx'; import MessageInput from './MessageInput.jsx';
import { api } from '../utils/api.js'; import { api } from '../utils/api.js';
import { useAuth } from '../contexts/AuthContext.jsx';
import { useToast } from '../contexts/ToastContext.jsx'; import { useToast } from '../contexts/ToastContext.jsx';
import { useSocket } from '../contexts/SocketContext.jsx'; import { useSocket } from '../contexts/SocketContext.jsx';
import './ChatWindow.css'; import './ChatWindow.css';
function formatTime(ts) { export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMessage, onlineUserIds = new Set() }) {
if (!ts) return ''; const { user: currentUser } = useAuth();
const d = new Date(ts); const { socket } = useSocket();
const now = new Date(); const { toast } = useToast();
const diff = now - d;
if (diff < 86400000 && d.getDate() === now.getDate()) {
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
if (diff < 604800000) {
return d.toLocaleDateString([], { weekday: 'short' });
}
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
export default function ChatWindow({ group, currentUser, onBack, isMobile, onlineUserIds = new Set() }) {
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(false); const [hasMore, setHasMore] = useState(false);
const [typing, setTyping] = useState([]); const [typing, setTyping] = useState([]);
const [iconGroupInfo, setIconGroupInfo] = useState(''); const [iconGroupInfo, setIconGroupInfo] = useState('');
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
const messagesContainerRef = useRef(null); const messagesContainerRef = useRef(null);
const typingTimers = useRef({}); const typingTimers = useRef({});
const { toast } = useToast();
const { socket } = useSocket();
useEffect(() => { useEffect(() => {
api.getSettings().then(({ settings }) => setIconGroupInfo(settings.icon_groupinfo || '')).catch(() => {}); const onResize = () => setIsMobile(window.innerWidth < 768);
const handler = () => api.getSettings().then(({ settings }) => setIconGroupInfo(settings.icon_groupinfo || '')).catch(() => {}); window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
useEffect(() => {
api.getSettings()
.then(({ settings }) => setIconGroupInfo(settings.icon_groupinfo || ''))
.catch(() => {});
const handler = () => api.getSettings()
.then(({ settings }) => setIconGroupInfo(settings.icon_groupinfo || ''))
.catch(() => {});
window.addEventListener('jama:settings-updated', handler); window.addEventListener('jama:settings-updated', handler);
return () => window.removeEventListener('jama:settings-updated', handler); return () => window.removeEventListener('jama:settings-updated', handler);
}, []); }, []);
@@ -84,7 +85,10 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
}; };
const handleTypingStart = ({ userId: tid, user: tu }) => { const handleTypingStart = ({ userId: tid, user: tu }) => {
setTyping(prev => prev.find(t => t.userId === tid) ? prev : [...prev, { userId: tid, name: tu?.display_name || tu?.name || 'Someone' }]); if (tid === currentUser?.id) return;
setTyping(prev => prev.find(t => t.userId === tid)
? prev
: [...prev, { userId: tid, name: tu?.display_name || tu?.name || 'Someone' }]);
if (typingTimers.current[tid]) clearTimeout(typingTimers.current[tid]); if (typingTimers.current[tid]) clearTimeout(typingTimers.current[tid]);
typingTimers.current[tid] = setTimeout(() => { typingTimers.current[tid] = setTimeout(() => {
setTyping(prev => prev.filter(t => t.userId !== tid)); setTyping(prev => prev.filter(t => t.userId !== tid));
@@ -96,11 +100,16 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
setTyping(prev => prev.filter(t => t.userId !== tid)); setTyping(prev => prev.filter(t => t.userId !== tid));
}; };
const handleGroupUpdated = (updatedGroup) => {
if (updatedGroup.id === group.id) onGroupUpdated?.();
};
socket.on('message:new', handleNew); socket.on('message:new', handleNew);
socket.on('message:deleted', handleDeleted); socket.on('message:deleted', handleDeleted);
socket.on('reaction:updated', handleReaction); socket.on('reaction:updated', handleReaction);
socket.on('typing:start', handleTypingStart); socket.on('typing:start', handleTypingStart);
socket.on('typing:stop', handleTypingStop); socket.on('typing:stop', handleTypingStop);
socket.on('group:updated', handleGroupUpdated);
return () => { return () => {
socket.off('message:new', handleNew); socket.off('message:new', handleNew);
@@ -108,8 +117,9 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
socket.off('reaction:updated', handleReaction); socket.off('reaction:updated', handleReaction);
socket.off('typing:start', handleTypingStart); socket.off('typing:start', handleTypingStart);
socket.off('typing:stop', handleTypingStop); socket.off('typing:stop', handleTypingStop);
socket.off('group:updated', handleGroupUpdated);
}; };
}, [socket, group?.id]); }, [socket, group?.id, currentUser?.id]);
const handleLoadMore = async () => { const handleLoadMore = async () => {
if (!hasMore || loading || messages.length === 0) return; if (!hasMore || loading || messages.length === 0) return;
@@ -160,13 +170,8 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
window.dispatchEvent(new CustomEvent('jama:reply', { detail: msg })); window.dispatchEvent(new CustomEvent('jama:reply', { detail: msg }));
}; };
const handleDirectMessage = async (userId) => { const handleDirectMessage = (dmGroup) => {
try { onDirectMessage?.(dmGroup);
const { group: dmGroup } = await api.createGroup({ type: 'direct', userId });
window.dispatchEvent(new CustomEvent('jama:open-group', { detail: dmGroup.id }));
} catch (e) {
toast(e.message || 'Could not open DM', 'error');
}
}; };
if (!group) { if (!group) {
@@ -195,7 +200,7 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
<div className="chat-window"> <div className="chat-window">
{/* Header */} {/* Header */}
<div className="chat-header"> <div className="chat-header">
{isMobile && ( {isMobile && onBack && (
<button className="btn-icon" onClick={onBack} style={{ marginRight: 4 }}> <button className="btn-icon" onClick={onBack} style={{ marginRight: 4 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="15 18 9 12 15 6"/> <polyline points="15 18 9 12 15 6"/>
@@ -217,7 +222,7 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<div className="chat-header-name truncate"> <div className="chat-header-name truncate">
{isDirect ? peerName : group.name} {isDirect ? peerName : group.name}
{group.is_readonly && <span className="readonly-badge" style={{ marginLeft: 8 }}>read-only</span>} {group.is_readonly ? <span className="readonly-badge" style={{ marginLeft: 8 }}>read-only</span> : null}
</div> </div>
{isDirect && isOnline && <div className="chat-header-sub" style={{ color: 'var(--success)' }}>Online</div>} {isDirect && isOnline && <div className="chat-header-sub" style={{ color: 'var(--success)' }}>Online</div>}
{!isDirect && group.type === 'private' && <div className="chat-header-sub">Private group</div>} {!isDirect && group.type === 'private' && <div className="chat-header-sub">Private group</div>}
@@ -276,7 +281,7 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
</div> </div>
{/* Input */} {/* Input */}
{group.is_readonly && !['admin', 'owner'].includes(currentUser?.role) ? ( {group.is_readonly && currentUser?.role !== 'admin' ? (
<div className="readonly-bar"> <div className="readonly-bar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>