@churchapps/apphelper
Version:
Library of helper functions for React and NextJS ChurchApps
264 lines • 17.1 kB
JavaScript
"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