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

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

View File

@@ -2,39 +2,40 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import Message from './Message.jsx';
import MessageInput from './MessageInput.jsx';
import { api } from '../utils/api.js';
import { useAuth } from '../contexts/AuthContext.jsx';
import { useToast } from '../contexts/ToastContext.jsx';
import { useSocket } from '../contexts/SocketContext.jsx';
import './ChatWindow.css';
function formatTime(ts) {
if (!ts) return '';
const d = new Date(ts);
const now = new Date();
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, onBack, onGroupUpdated, onDirectMessage, onlineUserIds = new Set() }) {
const { user: currentUser } = useAuth();
const { socket } = useSocket();
const { toast } = useToast();
export default function ChatWindow({ group, currentUser, onBack, isMobile, onlineUserIds = new Set() }) {
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(false);
const [typing, setTyping] = useState([]);
const [iconGroupInfo, setIconGroupInfo] = useState('');
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const messagesEndRef = useRef(null);
const messagesContainerRef = useRef(null);
const typingTimers = useRef({});
const { toast } = useToast();
const { socket } = useSocket();
useEffect(() => {
api.getSettings().then(({ settings }) => setIconGroupInfo(settings.icon_groupinfo || '')).catch(() => {});
const handler = () => api.getSettings().then(({ settings }) => setIconGroupInfo(settings.icon_groupinfo || '')).catch(() => {});
const onResize = () => setIsMobile(window.innerWidth < 768);
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);
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 }) => {
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]);
typingTimers.current[tid] = setTimeout(() => {
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));
};
const handleGroupUpdated = (updatedGroup) => {
if (updatedGroup.id === group.id) onGroupUpdated?.();
};
socket.on('message:new', handleNew);
socket.on('message:deleted', handleDeleted);
socket.on('reaction:updated', handleReaction);
socket.on('typing:start', handleTypingStart);
socket.on('typing:stop', handleTypingStop);
socket.on('group:updated', handleGroupUpdated);
return () => {
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('typing:start', handleTypingStart);
socket.off('typing:stop', handleTypingStop);
socket.off('group:updated', handleGroupUpdated);
};
}, [socket, group?.id]);
}, [socket, group?.id, currentUser?.id]);
const handleLoadMore = async () => {
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 }));
};
const handleDirectMessage = async (userId) => {
try {
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');
}
const handleDirectMessage = (dmGroup) => {
onDirectMessage?.(dmGroup);
};
if (!group) {
@@ -195,7 +200,7 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
<div className="chat-window">
{/* Header */}
<div className="chat-header">
{isMobile && (
{isMobile && onBack && (
<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">
<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="chat-header-name truncate">
{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>
{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>}
@@ -276,7 +281,7 @@ export default function ChatWindow({ group, currentUser, onBack, isMobile, onlin
</div>
{/* Input */}
{group.is_readonly && !['admin', 'owner'].includes(currentUser?.role) ? (
{group.is_readonly && currentUser?.role !== 'admin' ? (
<div className="readonly-bar">
<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"/>