UNPKG

@churchapps/apphelper

Version:

Library of helper functions for React and NextJS ChurchApps

264 lines 17.1 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import React from "react"; import { ApiHelper, UserHelper, CommonEnvironmentHelper } from "@churchapps/helpers"; import { Avatar, Menu, Typography, Icon, Button, Box, Badge, Dialog, DialogContent, DialogTitle } from "@mui/material"; import { NavItem, AppList } from "."; import { ChurchList } from "./ChurchList"; import { TabPanel } from "../TabPanel"; import { Locale } from "../../helpers"; import { PrivateMessages } from "./PrivateMessages"; import { Notifications } from "./Notifications"; import { useCookies, CookiesProvider } from "react-cookie"; import { NotificationService } from "../../helpers/NotificationService"; import { SocketHelper } from "../../helpers"; // Create a persistent store for modal state that survives component re-renders const modalStateStore = { showPM: false, showNotifications: false, listeners: new Set(), setShowPM(value) { this.showPM = value; this.listeners.forEach((listener) => listener()); }, setShowNotifications(value) { this.showNotifications = value; this.listeners.forEach((listener) => listener()); }, subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } }; const UserMenuContent = React.memo((props) => { const userName = props.userName; const [anchorEl, setAnchorEl] = React.useState(null); const [, forceUpdate] = React.useReducer(x => x + 1, 0); const [refreshKey, setRefreshKey] = React.useState(() => Math.random()); const [, , removeCookie] = useCookies(["lastChurchId"]); const [directNotificationCounts, setDirectNotificationCounts] = React.useState(() => props.notificationCounts || { notificationCount: 0, pmCount: 0 }); const open = Boolean(anchorEl); // Subscribe to modal state changes React.useEffect(() => { return modalStateStore.subscribe(forceUpdate); }, [forceUpdate]); // Subscribe directly to NotificationService to update badge counts without re-renders React.useEffect(() => { const notificationService = NotificationService.getInstance(); const unsubscribe = notificationService.subscribe((newCounts) => { setDirectNotificationCounts(newCounts); }); // Initialize with current counts if (notificationService.isReady()) { setDirectNotificationCounts(notificationService.getCounts()); } return unsubscribe; }, []); const showPM = modalStateStore.showPM; const showNotifications = modalStateStore.showNotifications; // Create a stable callback for onUpdate that doesn't depend on props const stableOnUpdate = React.useCallback(() => { // Use NotificationService directly to avoid dependency on props.loadCounts NotificationService.getInstance().refresh(); }, []); const handleClick = (e) => { e.preventDefault(); setAnchorEl(e.currentTarget); }; const handleClose = () => { setTabIndex(0); setAnchorEl(null); }; const handleSwitchChurch = () => { removeCookie("lastChurchId", { path: "/" }); setTabIndex(2); }; const getMainLinks = () => { const jwt = ApiHelper.getConfig("MembershipApi").jwt; const churchId = UserHelper.currentUserChurch.church.id; const result = []; result.push(_jsx(NavItem, { onClick: () => { modalStateStore.setShowPM(true); }, label: getLabel("wrapper.messages", "Messages"), icon: "mail", onNavigate: props.onNavigate, badgeCount: directNotificationCounts.pmCount }, "/messages")); result.push(_jsx(NavItem, { onClick: () => { modalStateStore.setShowNotifications(true); }, label: getLabel("wrapper.notifications", "Notifications"), icon: "notifications", onNavigate: props.onNavigate, badgeCount: directNotificationCounts.notificationCount }, "/notifications")); result.push(_jsx(NavItem, { label: getLabel("wrapper.editProfile", "Edit Profile"), icon: "person", onClick: () => { setTabIndex(3); } }, "EditProfile")); // Create logout URL with current page as return URL const currentPath = typeof window !== "undefined" ? window.location.pathname + window.location.search : "/"; const logoutUrl = `/login?action=logout&returnUrl=${encodeURIComponent(currentPath)}`; result.push(_jsx(NavItem, { url: logoutUrl, label: getLabel("wrapper.logout", "Logout"), icon: "logout", onNavigate: props.onNavigate }, "/logout")); result.push(_jsx("div", { style: { borderTop: "1px solid #CCC", paddingTop: 2, paddingBottom: 2 } }, "divider")); result.push(_jsx(NavItem, { label: getLabel("wrapper.switchApp", "Switch App"), icon: "apps", onClick: () => { setTabIndex(1); } }, "Switch App")); result.push(_jsx(NavItem, { label: getLabel("wrapper.switchChurch", "Switch Church"), icon: "church", onClick: handleSwitchChurch }, "Switch Church")); return result; }; const getProfilePic = () => { if (props.profilePicture) return props.profilePicture; else return "/images/sample-profile.png"; }; const paperProps = { elevation: 0, sx: { overflow: "visible", filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.32))", mt: 1.5, "& .MuiAvatar-root": { width: 32, height: 32, ml: -0.5, mr: 1 }, minWidth: 450 } }; const handleItemClick = (e) => { // Handle menu item clicks if needed }; const [tabIndex, setTabIndex] = React.useState(0); // Helper function to get label with fallback const getLabel = (key, fallback) => { const label = Locale.label(key); return label && label !== key ? label : fallback; }; const getTabs = () => { return (_jsxs(Box, { sx: { borderBottom: 1, borderColor: "divider" }, children: [_jsx(TabPanel, { value: tabIndex, index: 0, children: getMainLinks() }), _jsxs(TabPanel, { value: tabIndex, index: 1, children: [_jsx(NavItem, { label: "Back", icon: "arrow_back", onClick: () => { setTabIndex(0); } }, "AppBack"), _jsx(AppList, { currentUserChurch: props.context?.userChurch, appName: props.appName, onNavigate: props.onNavigate })] }), _jsx(TabPanel, { value: tabIndex, index: 2, children: _jsxs("div", { style: { maxHeight: "70vh", overflowY: "auto" }, children: [_jsx(NavItem, { label: "Back", icon: "arrow_back", onClick: () => { setTabIndex(0); } }, "ChurchBack"), (() => { // Check if userChurches is actually the userChurch object if (props.context?.userChurches && !Array.isArray(props.context.userChurches) && props.context.userChurches.id) { console.error("UserMenu - ERROR: context.userChurches contains a single church object instead of an array!"); const churchArray = props.context.userChurch ? [props.context.userChurch] : []; return _jsx(ChurchList, { userChurches: churchArray, currentUserChurch: props.context?.userChurch, context: props.context, onDelete: handleClose, onChurchChange: () => { handleClose(); // Don't navigate - just close the menu and let the context update trigger re-renders } }); } if (!props.context?.userChurches) { return _jsx(Typography, { sx: { p: 2, color: "text.secondary" }, children: "Loading churches..." }); } else if (!Array.isArray(props.context.userChurches)) { return _jsx(Typography, { sx: { p: 2, color: "text.secondary", fontWeight: "bold" }, children: "Error: Invalid church data format" }); } else if (props.context.userChurches.length === 0) { return _jsx(Typography, { sx: { p: 2, color: "text.secondary" }, children: "No churches available" }); } else { // Ensure we always pass an array const churchesArray = Array.isArray(props.context.userChurches) ? props.context.userChurches : [props.context.userChurches]; return _jsx(ChurchList, { userChurches: churchesArray, currentUserChurch: props.context?.userChurch, context: props.context, onDelete: handleClose, onChurchChange: () => { handleClose(); // Don't navigate - just close the menu and let the context update trigger re-renders } }); } })()] }) }), _jsxs(TabPanel, { value: tabIndex, index: 3, children: [_jsx(NavItem, { label: "Back", icon: "arrow_back", onClick: () => { setTabIndex(0); } }, "EditBack"), _jsx(NavItem, { url: "/profile", label: getLabel("wrapper.editAccount", "Edit Account"), icon: "settings", onNavigate: props.onNavigate }, "/profile"), getChurchProfileLink()] })] })); }; const getChurchProfileLink = () => { const personId = props.context?.person?.id; if (!personId) return null; const isB1App = props.appName === "B1.church" || props.appName === "B1App" || props.appName === "B1"; if (isB1App) return _jsx(NavItem, { url: `/my/community/${personId}`, label: getLabel("wrapper.editChurchProfile", "Edit Church Profile"), icon: "church", onNavigate: props.onNavigate }); else { const jwt = ApiHelper.getConfig("MembershipApi").jwt; const churchId = props.context?.userChurch?.church?.id; const subDomain = props.context?.userChurch?.church?.subDomain; const b1Url = CommonEnvironmentHelper.B1Root.replace("{key}", subDomain); const returnUrl = encodeURIComponent(`/my/community/${personId}`); return _jsx(NavItem, { url: `${b1Url}/login?jwt=${jwt}&churchId=${churchId}&returnUrl=${returnUrl}`, external: true, label: getLabel("wrapper.editChurchProfile", "Edit Church Profile"), icon: "church", onNavigate: props.onNavigate }); } }; const getModals = () => { return (_jsxs(_Fragment, { children: [_jsxs(Dialog, { id: "private-messages-modal", open: showPM, onClose: () => { modalStateStore.setShowPM(false); }, maxWidth: "md", fullWidth: true, PaperProps: { sx: { height: "80vh", maxHeight: "700px", display: "flex", flexDirection: "column" } }, children: [_jsx(DialogTitle, { id: "private-messages-title", children: getLabel("wrapper.messages", "Messages") }), _jsx(DialogContent, { sx: { flex: 1, display: "flex", flexDirection: "column", p: 0, overflow: "hidden", minHeight: 0 }, children: _jsx(PrivateMessages, { context: props.context, refreshKey: currentRefreshKey, onUpdate: stableOnUpdate }) })] }), _jsxs(Dialog, { id: "notifications-modal", open: showNotifications, onClose: () => { modalStateStore.setShowNotifications(false); }, maxWidth: "md", fullWidth: true, children: [_jsx(DialogTitle, { id: "notifications-title", children: getLabel("wrapper.notifications", "Notifications") }), _jsx(DialogContent, { children: _jsx(Notifications, { context: props.context, appName: props.appName, onUpdate: props.loadCounts, onNavigate: props.onNavigate }) })] })] })); }; const totalNotifcations = directNotificationCounts.notificationCount + directNotificationCounts.pmCount; // Use a ref to track if we should update refresh key const stableRefreshKeyRef = React.useRef(refreshKey); // Set up WebSocket handlers to update refreshKey when messages arrive React.useEffect(() => { if (!props.context?.person?.id) return; const handleMessageUpdate = (data) => { // Only update refreshKey if a modal is open to trigger child updates if (modalStateStore.showPM || modalStateStore.showNotifications) { const newKey = Math.random(); setRefreshKey(newKey); stableRefreshKeyRef.current = newKey; } }; const handlePrivateMessage = (data) => { // Only update refreshKey if PM modal is open if (modalStateStore.showPM) { const newKey = Math.random(); setRefreshKey(newKey); stableRefreshKeyRef.current = newKey; } }; const handleNotification = (data) => { // Update refreshKey if any modal is open to trigger child updates if (modalStateStore.showPM || modalStateStore.showNotifications) { const newKey = Math.random(); setRefreshKey(newKey); stableRefreshKeyRef.current = newKey; } }; // Register WebSocket handlers const messageHandlerId = `UserMenu-MessageUpdate-${props.context.person.id}`; const privateMessageHandlerId = `UserMenu-PrivateMessage-${props.context.person.id}`; const notificationHandlerId = `UserMenu-Notification-${props.context.person.id}`; SocketHelper.addHandler("message", messageHandlerId, handleMessageUpdate); SocketHelper.addHandler("privateMessage", privateMessageHandlerId, handlePrivateMessage); SocketHelper.addHandler("notification", notificationHandlerId, handleNotification); // Cleanup return () => { SocketHelper.removeHandler(messageHandlerId); SocketHelper.removeHandler(privateMessageHandlerId); SocketHelper.removeHandler(notificationHandlerId); }; }, [props.context?.person?.id]); // Removed showPM, showNotifications dependencies // Use current refresh key const currentRefreshKey = refreshKey; return (_jsxs(_Fragment, { children: [_jsx(Button, { id: "user-menu-button", onClick: handleClick, color: "inherit", "aria-controls": open ? "account-menu" : undefined, "aria-haspopup": "true", "aria-expanded": open ? "true" : undefined, style: { textTransform: "none" }, endIcon: _jsx(Icon, { children: "expand_more" }), children: _jsx(Badge, { id: "user-menu-notification-badge", badgeContent: totalNotifcations, color: "error", invisible: totalNotifcations === 0, children: _jsx(Avatar, { id: "user-menu-avatar", src: getProfilePic(), sx: { width: 32, height: 32, marginRight: 1 } }) }) }), _jsx(Menu, { anchorEl: anchorEl, id: "user-menu-dropdown", open: open, onClose: handleClose, onClick: (e) => { handleItemClick(e); }, slotProps: { paper: paperProps }, transformOrigin: { horizontal: "right", vertical: "top" }, anchorOrigin: { horizontal: "right", vertical: "bottom" }, sx: { "& .MuiBox-root": { borderBottom: 0 } }, children: getTabs() }), getModals()] })); }); export const UserMenu = React.memo((props) => { return (_jsx(CookiesProvider, { defaultSetOptions: { path: "/" }, children: _jsx(UserMenuContent, { ...props }) })); }, (prevProps, nextProps) => { // Only re-render if essential props change, ignore notification count changes completely if (prevProps.userName !== nextProps.userName) { return false; } if (prevProps.profilePicture !== nextProps.profilePicture) { return false; } if (prevProps.appName !== nextProps.appName) { return false; } // Check if context has actually changed (deep comparison of relevant parts) if (prevProps.context?.person?.id !== nextProps.context?.person?.id || prevProps.context?.userChurch?.church?.id !== nextProps.context?.userChurch?.church?.id) { return false; } // Check if userChurches array changed in context if (prevProps.context?.userChurches?.length !== nextProps.context?.userChurches?.length) { return false; } // Check if loadCounts function reference changed (important for functionality) if (prevProps.loadCounts !== nextProps.loadCounts) { return false; } // Ignore both notificationCounts and onNavigate changes as they don't affect the component return true; // Skip re-render }); //# sourceMappingURL=UserMenu.js.map