UNPKG

@churchapps/apphelper

Version:

Library of helper functions for React and NextJS ChurchApps

383 lines 22.5 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React, { useState, useEffect } from "react"; import { ApiHelper } from "@churchapps/helpers"; import { Box, Stack, List, ListItem, ListItemAvatar, ListItemText, Typography, IconButton, Chip, Paper, Skeleton, useTheme } from "@mui/material"; import { Add as AddIcon, ChatBubbleOutline as ChatIcon } from "@mui/icons-material"; import { PersonAvatar } from "../PersonAvatar"; import { ArrayHelper, DateHelper } from "../../helpers"; import { PrivateMessageDetails } from "./PrivateMessageDetails"; import { NewPrivateMessage } from "./NewPrivateMessage"; // Create a persistent store for PrivateMessages state that survives component re-renders const privateMessagesStateStore = { selectedMessage: null, inAddMode: false, listeners: new Set(), setSelectedMessage(value) { this.selectedMessage = value; this.listeners.forEach((listener) => listener()); }, setInAddMode(value) { this.inAddMode = value; this.listeners.forEach((listener) => listener()); }, subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } }; export const PrivateMessages = React.memo((props) => { const [privateMessages, setPrivateMessages] = useState([]); const [, forceUpdate] = React.useReducer(x => x + 1, 0); const [isLoading, setIsLoading] = useState(true); // Subscribe to state changes React.useEffect(() => { return privateMessagesStateStore.subscribe(forceUpdate); }, [forceUpdate]); const selectedMessage = privateMessagesStateStore.selectedMessage; const inAddMode = privateMessagesStateStore.inAddMode; const loadData = async () => { setIsLoading(true); const pms = await ApiHelper.get("/privateMessages", "MessagingApi"); // Store the current selected conversation ID if dialog is open const currentSelectedPersonId = selectedMessage ? (selectedMessage.fromPersonId === props.context.person.id) ? selectedMessage.toPersonId : selectedMessage.fromPersonId : null; // Group messages by person (conversation) const conversationMap = new Map(); const peopleIds = []; pms.forEach((pm) => { const personId = (pm.fromPersonId === props.context.person.id) ? pm.toPersonId : pm.fromPersonId; if (peopleIds.indexOf(personId) === -1) peopleIds.push(personId); // Keep only the most recent message per conversation const currentMessage = pm.conversation?.messages?.[0]; const existingPm = conversationMap.get(personId); const existingMessage = existingPm?.conversation?.messages?.[0]; if (!conversationMap.has(personId)) { // First message for this person conversationMap.set(personId, pm); } else if (currentMessage && existingMessage) { // Compare timestamps to keep the most recent const currentTime = new Date(currentMessage.timeUpdated || currentMessage.timeSent); const existingTime = new Date(existingMessage.timeUpdated || existingMessage.timeSent); if (currentTime > existingTime) { conversationMap.set(personId, pm); } } else if (currentMessage && !existingMessage) { // Current has message but existing doesn't, use current conversationMap.set(personId, pm); } // If !currentMessage but existingMessage, keep existing (do nothing) }); // Convert map back to array (one message per conversation) let conversations = Array.from(conversationMap.values()); // Filter out conversations without messages first conversations = conversations.filter(pm => pm.conversation?.messages?.[0]); // Get the filtered people IDs (only for conversations with messages) const filteredPeopleIds = conversations.map(pm => (pm.fromPersonId === props.context.person.id) ? pm.toPersonId : pm.fromPersonId).filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates if (filteredPeopleIds.length > 0) { try { const people = await ApiHelper.get("/people/basic?ids=" + filteredPeopleIds.join(","), "MembershipApi"); conversations.forEach(pm => { const personId = (pm.fromPersonId === props.context.person.id) ? pm.toPersonId : pm.fromPersonId; pm.person = ArrayHelper.getOne(people, "id", personId); }); } catch (error) { console.error("❌ Failed to load people data:", error); } } // Sort by most recent message (same logic as displayed in UI) conversations.sort((a, b) => { const aMessage = a.conversation?.messages?.[0]; const bMessage = b.conversation?.messages?.[0]; if (!aMessage && !bMessage) return 0; if (!aMessage) return 1; // b comes first if (!bMessage) return -1; // a comes first const aTime = new Date(aMessage.timeUpdated || aMessage.timeSent).getTime(); const bTime = new Date(bMessage.timeUpdated || bMessage.timeSent).getTime(); // Most recent first (descending order) return bTime - aTime; }); setPrivateMessages(conversations); // If a conversation is currently selected, update the selectedMessage to the new data // This prevents the dialog from closing when new messages arrive if (currentSelectedPersonId) { const updatedSelectedMessage = conversations.find(pm => { const personId = (pm.fromPersonId === props.context.person.id) ? pm.toPersonId : pm.fromPersonId; return personId === currentSelectedPersonId; }); if (updatedSelectedMessage) { privateMessagesStateStore.setSelectedMessage(updatedSelectedMessage); } else { privateMessagesStateStore.setSelectedMessage(null); } } setIsLoading(false); props.onUpdate(); }; // Initialize data on mount useEffect(() => { loadData(); }, []); //eslint-disable-line // Reload data when refreshKey changes useEffect(() => { loadData(); }, [props.refreshKey]); //eslint-disable-line const getMessageList = () => { if (privateMessages.length === 0) { return (_jsxs(Box, { sx: { textAlign: 'center', py: 8, px: 4, height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }, children: [_jsx(Box, { sx: { width: 120, height: 120, borderRadius: '50%', bgcolor: 'rgba(25, 118, 210, 0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 3, position: 'relative', '&::before': { content: '""', position: 'absolute', width: '100%', height: '100%', borderRadius: '50%', border: '1px solid rgba(25, 118, 210, 0.12)', animation: 'pulse 2s infinite' }, '@keyframes pulse': { '0%': { transform: 'scale(1)', opacity: 1 }, '50%': { transform: 'scale(1.05)', opacity: 0.7 }, '100%': { transform: 'scale(1)', opacity: 1 } } }, children: _jsx(ChatIcon, { sx: { fontSize: 56, color: 'primary.main', opacity: 0.8 } }) }), _jsx(Typography, { variant: "h5", sx: { color: 'text.primary', fontWeight: 600, mb: 1.5, letterSpacing: '-0.02em' }, children: "No conversations yet" }), _jsx(Typography, { variant: "body1", sx: { color: 'text.secondary', maxWidth: 280, lineHeight: 1.6, mb: 3 }, children: "Start meaningful conversations with your community members. Your messages will appear here." }), _jsxs(Box, { sx: { p: 2, borderRadius: 2, bgcolor: 'rgba(25, 118, 210, 0.04)', border: '1px solid rgba(25, 118, 210, 0.12)', display: 'flex', alignItems: 'center', gap: 1.5 }, children: [_jsx(AddIcon, { sx: { color: 'primary.main', fontSize: 20 } }), _jsx(Typography, { variant: "body2", sx: { color: 'primary.main', fontWeight: 500 }, children: "Click the + button above to start your first conversation" })] })] })); } return (_jsx(List, { sx: { width: '100%', p: 0, '& .MuiListItem-root': { transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' } }, children: privateMessages.map((pm, index) => { const person = pm.person; const message = pm.conversation?.messages?.[0]; // Only filter out if there's no message - show conversations even without person data if (!message) { return null; } const contents = message.content?.split("\n")[0]; let datePosted = new Date(message.timeUpdated || message.timeSent); const displayDuration = DateHelper.getDisplayDuration(datePosted); // Check if this conversation has unread messages const isUnread = pm.notifyPersonId === props.context.person.id; // Determine who sent the last message for better context const isLastMessageFromMe = message.personId === props.context.person.id; const isFromOtherPerson = pm.fromPersonId !== props.context.person.id; return (_jsx(Box, { sx: { px: 2, py: 0.5 }, children: _jsxs(ListItem, { component: "button", onClick: () => privateMessagesStateStore.setSelectedMessage(pm), sx: { alignItems: 'flex-start', p: 3, cursor: 'pointer', bgcolor: isUnread ? 'rgba(25, 118, 210, 0.04)' : 'background.paper', border: isUnread ? '1px solid rgba(25, 118, 210, 0.12)' : '1px solid transparent', borderRadius: 3, boxShadow: '0 1px 3px rgba(0, 0, 0, 0.02)', transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', transform: 'translateY(0)', '&:hover': { bgcolor: isUnread ? 'rgba(25, 118, 210, 0.06)' : 'rgba(0, 0, 0, 0.02)', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)', transform: 'translateY(-1px)', borderColor: 'rgba(0, 0, 0, 0.06)' }, '&:active': { transform: 'translateY(0)', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.06)' }, position: 'relative', overflow: 'hidden', '&::before': isUnread ? { content: '""', position: 'absolute', left: 0, top: 0, bottom: 0, width: 4, bgcolor: 'primary.main', borderRadius: '0 2px 2px 0' } : {} }, children: [_jsx(ListItemAvatar, { sx: { mr: 2 }, children: _jsxs(Box, { sx: { position: 'relative' }, children: [_jsx(PersonAvatar, { person: person || { name: { display: "Unknown User" } }, size: "medium" }), isUnread && (_jsx(Box, { sx: { position: 'absolute', top: -2, right: -2, width: 14, height: 14, bgcolor: 'primary.main', borderRadius: '50%', border: '2px solid white', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' } }))] }) }), _jsx(ListItemText, { sx: { m: 0, flex: 1 }, primary: _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "flex-start", sx: { mb: 0.5 }, children: [_jsx(Typography, { variant: "h6", sx: { fontWeight: isUnread ? 600 : 500, fontSize: '1rem', color: 'text.primary', lineHeight: 1.3 }, children: person?.name?.display || "Unknown User" }), _jsx(Typography, { variant: "caption", sx: { color: 'text.secondary', fontSize: '0.75rem', fontWeight: 500, letterSpacing: '0.02em', ml: 2, flexShrink: 0 }, children: displayDuration })] }), secondary: _jsxs(Typography, { variant: "body2", sx: { color: 'text.secondary', fontSize: '0.875rem', fontWeight: isUnread ? 500 : 400, lineHeight: 1.4, overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', opacity: contents ? 1 : 0.6, fontStyle: contents ? 'normal' : 'italic' }, children: [isLastMessageFromMe && contents && (_jsxs(Typography, { component: "span", sx: { fontWeight: 600, color: 'text.primary', opacity: 0.8 }, children: ["You:", " "] })), contents || 'No message preview available'] }) }), isUnread && (_jsx(Box, { sx: { ml: 1, display: 'flex', alignItems: 'flex-start', pt: 0.5 }, children: _jsx(Chip, { size: "small", label: "New", sx: { height: 22, fontSize: '0.7rem', fontWeight: 600, bgcolor: 'primary.main', color: 'white', '& .MuiChip-label': { px: 1 } } }) }))] }) }, pm.id)); }) })); }; const handleBack = () => { privateMessagesStateStore.setInAddMode(false); privateMessagesStateStore.setSelectedMessage(null); loadData(); }; const theme = useTheme(); if (inAddMode) return _jsx(NewPrivateMessage, { context: props.context, onSelectMessage: (pm) => { privateMessagesStateStore.setSelectedMessage(pm); privateMessagesStateStore.setInAddMode(false); }, onBack: handleBack }); if (selectedMessage) return _jsx(PrivateMessageDetails, { privateMessage: selectedMessage, context: props.context, onBack: handleBack, refreshKey: props.refreshKey, onMessageRead: loadData }); return (_jsxs(Paper, { elevation: 0, sx: { height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'background.default', borderRadius: 0 }, children: [_jsx(Box, { sx: { p: 3, borderBottom: '1px solid', borderColor: 'divider', bgcolor: 'background.paper', backdropFilter: 'blur(10px)' }, children: _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", children: [_jsxs(Box, { children: [_jsx(Typography, { variant: "h5", component: "h1", sx: { fontWeight: 700, color: 'text.primary', letterSpacing: '-0.02em', mb: 0.5 }, children: "Messages" }), _jsx(Typography, { variant: "body2", sx: { color: 'text.secondary', fontWeight: 500 }, children: privateMessages.length === 0 ? 'No conversations' : `${privateMessages.length} conversation${privateMessages.length === 1 ? '' : 's'}` })] }), _jsx(IconButton, { onClick: () => privateMessagesStateStore.setInAddMode(true), sx: { bgcolor: 'primary.main', color: 'white', width: 48, height: 48, boxShadow: '0 4px 12px rgba(25, 118, 210, 0.3)', transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', '&:hover': { bgcolor: 'primary.dark', boxShadow: '0 6px 16px rgba(25, 118, 210, 0.4)', transform: 'translateY(-1px)' }, '&:active': { transform: 'translateY(0)', boxShadow: '0 2px 8px rgba(25, 118, 210, 0.3)' } }, children: _jsx(AddIcon, { sx: { fontSize: 24 } }) })] }) }), _jsx(Box, { sx: { flex: 1, overflow: 'auto' }, children: isLoading ? (_jsx(Box, { sx: { p: 2 }, children: [...Array(3)].map((_, index) => (_jsx(Box, { sx: { px: 2, py: 0.5, mb: 1 }, children: _jsxs(Box, { sx: { p: 3, borderRadius: 3, bgcolor: 'background.paper', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.02)', display: 'flex', alignItems: 'flex-start' }, children: [_jsx(Skeleton, { variant: "circular", width: 56, height: 56, sx: { mr: 2, flexShrink: 0 } }), _jsxs(Box, { sx: { flex: 1 }, children: [_jsxs(Box, { sx: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }, children: [_jsx(Skeleton, { variant: "text", width: "45%", height: 24, sx: { borderRadius: 1 } }), _jsx(Skeleton, { variant: "text", width: "20%", height: 18, sx: { borderRadius: 1 } })] }), _jsx(Skeleton, { variant: "text", width: "85%", height: 20, sx: { borderRadius: 1 } }), _jsx(Skeleton, { variant: "text", width: "65%", height: 20, sx: { borderRadius: 1, mt: 0.5 } })] })] }) }, index))) })) : (getMessageList()) })] })); }, (prevProps, nextProps) => { // Only re-render if context.person.id changes or if we're explicitly forcing with refreshKey const personChanged = prevProps.context?.person?.id !== nextProps.context?.person?.id; const refreshKeyChanged = prevProps.refreshKey !== nextProps.refreshKey; if (personChanged) { return false; // Re-render } if (refreshKeyChanged) { return false; // Re-render } return true; // Skip re-render }); //# sourceMappingURL=PrivateMessages.js.map