From 7b37259a2c9fe186e8cc231cde915a868a67d81a Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Tue, 17 Mar 2026 18:34:00 +0100 Subject: [PATCH] feat: redesign private messaging inbox --- resources/js/Pages/Messages/Index.jsx | 247 ++- .../components/messaging/ConversationList.jsx | 155 +- .../messaging/ConversationThread.jsx | 1336 +++++++++-------- .../js/components/messaging/MessageBubble.jsx | 366 +++-- .../messaging/NewConversationModal.jsx | 159 +- 5 files changed, 1278 insertions(+), 985 deletions(-) diff --git a/resources/js/Pages/Messages/Index.jsx b/resources/js/Pages/Messages/Index.jsx index ea7c9834..98e67f16 100644 --- a/resources/js/Pages/Messages/Index.jsx +++ b/resources/js/Pages/Messages/Index.jsx @@ -4,8 +4,6 @@ import ConversationList from '../../components/messaging/ConversationList' import ConversationThread from '../../components/messaging/ConversationThread' import NewConversationModal from '../../components/messaging/NewConversationModal' -// ── helpers ────────────────────────────────────────────────────────────────── - function getCsrf() { return document.querySelector('meta[name="csrf-token"]')?.content ?? '' } @@ -26,14 +24,34 @@ async function apiFetch(url, options = {}) { headers, ...options, }) + if (!res.ok) { const err = await res.json().catch(() => ({})) throw new Error(err.message ?? `HTTP ${res.status}`) } + return res.json() } -// ── MessagesPage ───────────────────────────────────────────────────────────── +function relativeTime(iso) { + if (!iso) return 'No activity yet' + + const diff = (Date.now() - new Date(iso).getTime()) / 1000 + if (diff < 60) return 'Just now' + if (diff < 3600) return `${Math.floor(diff / 60)}m ago` + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` + if (diff < 604800) return `${Math.floor(diff / 86400)}d ago` + + return new Date(iso).toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + }) +} + +function buildSearchPreview(item) { + const body = (item?.body || '').trim() + return body || '(attachment only)' +} function MessagesPage({ userId, username, activeConversationId: initialId }) { const [conversations, setConversations] = useState([]) @@ -46,7 +64,6 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) { const [searching, setSearching] = useState(false) const pollRef = useRef(null) - // ── Load conversations list ──────────────────────────────────────────────── const loadConversations = useCallback(async () => { try { const data = await apiFetch('/api/messages/conversations') @@ -62,7 +79,7 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) { loadConversations() apiFetch('/api/messages/settings') - .then(data => setRealtimeEnabled(!!data?.realtime_enabled)) + .then((data) => setRealtimeEnabled(!!data?.realtime_enabled)) .catch(() => setRealtimeEnabled(false)) return () => { @@ -77,10 +94,10 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) { } if (realtimeEnabled) { - return + return undefined } - pollRef.current = setInterval(loadConversations, 15_000) + pollRef.current = setInterval(loadConversations, 15000) return () => { if (pollRef.current) clearInterval(pollRef.current) @@ -100,9 +117,11 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) { }, [loadConversations]) const handleMarkRead = useCallback((conversationId) => { - setConversations(prev => - prev.map(c => c.id === conversationId ? { ...c, unread_count: 0 } : c) - ) + setConversations((prev) => prev.map((conversation) => ( + conversation.id === conversationId + ? { ...conversation, unread_count: 0 } + : conversation + ))) }, []) useEffect(() => { @@ -117,12 +136,13 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) { } setSearching(true) + try { const data = await apiFetch(`/api/messages/search?q=${encodeURIComponent(q)}`) if (!cancelled) { setSearchResults(data.data ?? []) } - } catch (_) { + } catch { if (!cancelled) { setSearchResults([]) } @@ -146,55 +166,107 @@ function MessagesPage({ userId, username, activeConversationId: initialId }) { history.replaceState(null, '', `/messages/${item.conversation_id}?focus=${item.id}`) }, []) - const activeConversation = conversations.find(c => c.id === activeId) ?? null + const activeConversation = conversations.find((conversation) => conversation.id === activeId) ?? null + const unreadCount = conversations.reduce((sum, conversation) => sum + Number(conversation.unread_count || 0), 0) + const pinnedCount = conversations.reduce((sum, conversation) => { + const me = conversation.my_participant ?? conversation.all_participants?.find((participant) => participant.user_id === userId) + return sum + (me?.is_pinned ? 1 : 0) + }, 0) + const archivedCount = conversations.reduce((sum, conversation) => { + const me = conversation.my_participant ?? conversation.all_participants?.find((participant) => participant.user_id === userId) + return sum + (me?.is_archived ? 1 : 0) + }, 0) + const activeSearch = searchQuery.trim().length >= 2 + const activeConversationLabel = activeConversation?.title + || activeConversation?.all_participants?.find((participant) => participant.user_id !== userId)?.user?.username + || 'Conversation' return ( -
-
- - {/* ── Left panel: conversation list ─────────────────────────────── */} -