This commit is contained in:
2026-03-09 14:36:19 -04:00
parent f37fe0086f
commit 42ad779750
40 changed files with 1928 additions and 593 deletions

View File

@@ -8,7 +8,7 @@ import MessageInput from './MessageInput.jsx';
import GroupInfoModal from './GroupInfoModal.jsx';
import './ChatWindow.css';
export default function ChatWindow({ group, onBack, onGroupUpdated }) {
export default function ChatWindow({ group, onBack, onGroupUpdated, onDirectMessage }) {
const { socket } = useSocket();
const { user } = useAuth();
const toast = useToast();
@@ -23,6 +23,8 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
const messagesEndRef = useRef(null);
const messagesTopRef = useRef(null);
const typingTimers = useRef({});
const swipeStartX = useRef(null);
const swipeStartY = useRef(null);
useEffect(() => {
api.getSettings().then(({ settings }) => {
@@ -33,6 +35,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
return () => window.removeEventListener('jama:settings-changed', handler);
}, []);
const scrollToBottom = useCallback((smooth = false) => {
messagesEndRef.current?.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto' });
}, []);
@@ -110,7 +113,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
setHasMore(older.length >= 50);
};
const handleSend = async ({ content, imageFile, linkPreview }) => {
const handleSend = async ({ content, imageFile, linkPreview, emojiOnly }) => {
if (!group) return;
const replyId = replyTo?.id;
setReplyTo(null);
@@ -125,7 +128,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
}
} else {
socket?.emit('message:send', {
groupId: group.id, content, replyToId: replyId, linkPreview
groupId: group.id, content, replyToId: replyId, linkPreview, emojiOnly
});
}
} catch (e) {
@@ -149,8 +152,30 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
);
}
const handleTouchStart = (e) => {
swipeStartX.current = e.touches[0].clientX;
swipeStartY.current = e.touches[0].clientY;
};
const handleTouchEnd = (e) => {
if (swipeStartX.current === null || !onBack) return;
const dx = e.changedTouches[0].clientX - swipeStartX.current;
const dy = Math.abs(e.changedTouches[0].clientY - swipeStartY.current);
// Swipe right: at least 80px horizontal, less than 60px vertical drift
if (dx > 80 && dy < 60) {
e.preventDefault();
onBack();
}
swipeStartX.current = null;
swipeStartY.current = null;
};
return (
<div className="chat-window">
<div
className="chat-window"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* Header */}
<div className="chat-header">
{onBack && (
@@ -172,12 +197,12 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
) : null}
</div>
<span className="chat-header-sub">
{group.type === 'public' ? 'Public group' : 'Private group'}
{group.is_direct ? 'Direct message' : group.type === 'public' ? 'Public message' : 'Private message'}
</span>
</div>
<button className="btn-icon" onClick={() => setShowInfo(true)} title="Group info">
<button className="btn-icon" onClick={() => setShowInfo(true)} title="Message info">
{iconGroupInfo ? (
<img src={iconGroupInfo} alt="Group info" style={{ width: 20, height: 20, objectFit: 'contain' }} />
<img src={iconGroupInfo} alt="Message info" style={{ width: 20, height: 20, objectFit: 'contain' }} />
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" width="24" height="24">
<path strokeLinecap="round" strokeLinejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z" />
@@ -203,9 +228,11 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
message={msg}
prevMessage={messages[i - 1]}
currentUser={user}
isDirect={!!group.is_direct}
onReply={(m) => setReplyTo(m)}
onDelete={(id) => socket?.emit('message:delete', { messageId: id })}
onReact={(id, emoji) => socket?.emit('reaction:toggle', { messageId: id, emoji })}
onDirectMessage={onDirectMessage}
/>
))}
{typing.length > 0 && (
@@ -236,7 +263,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
) : (
<div className="readonly-bar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
This channel is read-only
This message is read-only
</div>
)}
@@ -245,6 +272,7 @@ export default function ChatWindow({ group, onBack, onGroupUpdated }) {
group={group}
onClose={() => setShowInfo(false)}
onUpdated={onGroupUpdated}
onBack={onBack}
/>
)}
</div>