@churchapps/apphelper 0.4.49 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/FormCardPayment.d.ts +1 -1
- package/dist/components/FormCardPayment.d.ts.map +1 -1
- package/dist/components/FormCardPayment.js +27 -10
- package/dist/components/FormCardPayment.js.map +1 -1
- package/dist/components/Loading.js +36 -36
- package/dist/components/notes/Notes.js +27 -27
- package/dist/helpers/index.d.ts +1 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +1 -1
- package/dist/helpers/index.js.map +1 -1
- package/dist/public/css/cropper.css +309 -309
- package/dist/public/css/styles.css +111 -111
- package/package.json +72 -73
- package/public/css/cropper.css +309 -309
- package/public/css/styles.css +111 -111
- package/public/locales/de.json +269 -269
- package/public/locales/en.json +276 -276
- package/public/locales/es.json +272 -272
- package/public/locales/fr.json +269 -269
- package/public/locales/hi.json +269 -269
- package/public/locales/it.json +269 -269
- package/public/locales/ko.json +269 -269
- package/public/locales/no.json +269 -269
- package/public/locales/pt.json +269 -269
- package/public/locales/ru.json +269 -269
- package/public/locales/tl.json +269 -269
- package/public/locales/zh.json +269 -269
- package/src/components/DisplayBox.tsx +83 -83
- package/src/components/ErrorMessages.tsx +28 -28
- package/src/components/ExportLink.tsx +81 -81
- package/src/components/FloatingSupport.tsx +18 -18
- package/src/components/FormCardPayment.tsx +184 -169
- package/src/components/FormSubmissionEdit.tsx +168 -168
- package/src/components/HelpIcon.tsx +12 -12
- package/src/components/ImageEditor.tsx +161 -161
- package/src/components/InputBox.tsx +96 -96
- package/src/components/Loading.tsx +77 -77
- package/src/components/PageHeader.tsx +110 -110
- package/src/components/PersonAvatar.tsx +77 -77
- package/src/components/QuestionEdit.tsx +99 -99
- package/src/components/SmallButton.tsx +42 -42
- package/src/components/SupportModal.tsx +32 -32
- package/src/components/TabPanel.tsx +28 -28
- package/src/components/gallery/GalleryModal.tsx +173 -173
- package/src/components/gallery/StockPhotos.tsx +95 -95
- package/src/components/gallery/index.ts +1 -1
- package/src/components/header/Banner.tsx +11 -11
- package/src/components/header/PrimaryMenu.tsx +100 -100
- package/src/components/header/SecondaryMenu.tsx +23 -23
- package/src/components/header/SecondaryMenuAlt.tsx +40 -40
- package/src/components/header/SiteHeader.tsx +207 -207
- package/src/components/header/SupportDrawer.tsx +111 -111
- package/src/components/header/index.tsx +2 -2
- package/src/components/index.tsx +20 -20
- package/src/components/notes/AddNote.tsx +180 -180
- package/src/components/notes/Note.tsx +68 -68
- package/src/components/notes/Notes.tsx +208 -208
- package/src/components/notes/index.ts +3 -3
- package/src/components/wrapper/AppList.tsx +19 -19
- package/src/components/wrapper/ChurchList.tsx +154 -154
- package/src/components/wrapper/NavItem.tsx +47 -47
- package/src/components/wrapper/NewPrivateMessage.tsx +253 -253
- package/src/components/wrapper/Notifications.tsx +223 -223
- package/src/components/wrapper/PrivateMessageDetails.tsx +112 -112
- package/src/components/wrapper/PrivateMessages.tsx +576 -576
- package/src/components/wrapper/UserMenu.tsx +383 -383
- package/src/components/wrapper/index.tsx +8 -8
- package/src/helpers/AnalyticsHelper.ts +32 -32
- package/src/helpers/AppearanceHelper.ts +73 -73
- package/src/helpers/ArrayHelper.ts +87 -87
- package/src/helpers/CurrencyHelper.ts +10 -10
- package/src/helpers/DateHelper.ts +104 -104
- package/src/helpers/ErrorHelper.ts +43 -43
- package/src/helpers/EventHelper.ts +49 -49
- package/src/helpers/FileHelper.ts +31 -31
- package/src/helpers/Locale.ts +457 -457
- package/src/helpers/NotificationService.ts +296 -296
- package/src/helpers/PersonHelper.ts +62 -62
- package/src/helpers/SlugHelper.ts +37 -37
- package/src/helpers/SocketHelper.ts +296 -296
- package/src/helpers/UniqueIdHelper.ts +36 -36
- package/src/helpers/UserHelper.ts +104 -104
- package/src/helpers/createEmotionCache.ts +17 -17
- package/src/helpers/index.ts +58 -50
- package/src/hooks/index.ts +3 -3
- package/src/hooks/useMountedState.ts +16 -16
- package/src/hooks/useNotifications.ts +93 -93
- package/src/index.ts +2 -2
- package/src/types/interface-extensions.d.ts +12 -0
- package/tsconfig.json +31 -31
- package/dist/public/locales/de.json +0 -270
- package/dist/public/locales/en.json +0 -277
- package/dist/public/locales/es.json +0 -272
- package/dist/public/locales/fr.json +0 -270
- package/dist/public/locales/hi.json +0 -270
- package/dist/public/locales/it.json +0 -270
- package/dist/public/locales/ko.json +0 -270
- package/dist/public/locales/no.json +0 -270
- package/dist/public/locales/pt.json +0 -270
- package/dist/public/locales/ru.json +0 -270
- package/dist/public/locales/tl.json +0 -270
- package/dist/public/locales/zh.json +0 -270
|
@@ -1,383 +1,383 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { ApiHelper, UserHelper, LoginUserChurchInterface, UserContextInterface, CommonEnvironmentHelper } from "@churchapps/helpers";
|
|
5
|
-
import { Avatar, Menu, Typography, Icon, Button, Box, Badge, Dialog, DialogContent, DialogTitle } from "@mui/material";
|
|
6
|
-
import { NavItem, AppList } from ".";
|
|
7
|
-
import { ChurchList } from "./ChurchList";
|
|
8
|
-
import { TabPanel } from "../TabPanel";
|
|
9
|
-
import { Locale } from "../../helpers";
|
|
10
|
-
import { PrivateMessages } from "./PrivateMessages";
|
|
11
|
-
import { Notifications } from "./Notifications";
|
|
12
|
-
import { useCookies, CookiesProvider } from "react-cookie";
|
|
13
|
-
import { NotificationService } from "../../helpers/NotificationService";
|
|
14
|
-
import { SocketHelper } from "../../helpers";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
interface Props {
|
|
18
|
-
notificationCounts: { notificationCount: number, pmCount: number };
|
|
19
|
-
loadCounts: () => void;
|
|
20
|
-
userName: string;
|
|
21
|
-
profilePicture: string;
|
|
22
|
-
context: UserContextInterface;
|
|
23
|
-
appName: string;
|
|
24
|
-
onNavigate: (url: string) => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Create a persistent store for modal state that survives component re-renders
|
|
28
|
-
const modalStateStore = {
|
|
29
|
-
showPM: false,
|
|
30
|
-
showNotifications: false,
|
|
31
|
-
listeners: new Set<() => void>(),
|
|
32
|
-
|
|
33
|
-
setShowPM(value: boolean) {
|
|
34
|
-
this.showPM = value;
|
|
35
|
-
this.listeners.forEach((listener: () => void) => listener());
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
setShowNotifications(value: boolean) {
|
|
39
|
-
this.showNotifications = value;
|
|
40
|
-
this.listeners.forEach((listener: () => void) => listener());
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
subscribe(listener: () => void) {
|
|
44
|
-
this.listeners.add(listener);
|
|
45
|
-
return () => this.listeners.delete(listener);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const UserMenuContent: React.FC<Props> = React.memo((props) => {
|
|
50
|
-
const userName = props.userName;
|
|
51
|
-
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
52
|
-
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
|
|
53
|
-
const [refreshKey, setRefreshKey] = React.useState(() => Math.random());
|
|
54
|
-
const [, , removeCookie] = useCookies(["lastChurchId"]);
|
|
55
|
-
const [directNotificationCounts, setDirectNotificationCounts] = React.useState(() => props.notificationCounts || { notificationCount: 0, pmCount: 0 });
|
|
56
|
-
const open = Boolean(anchorEl);
|
|
57
|
-
|
|
58
|
-
// Subscribe to modal state changes
|
|
59
|
-
React.useEffect(() => {
|
|
60
|
-
return modalStateStore.subscribe(forceUpdate);
|
|
61
|
-
}, [forceUpdate]);
|
|
62
|
-
|
|
63
|
-
// Subscribe directly to NotificationService to update badge counts without re-renders
|
|
64
|
-
React.useEffect(() => {
|
|
65
|
-
const notificationService = NotificationService.getInstance();
|
|
66
|
-
const unsubscribe = notificationService.subscribe((newCounts) => {
|
|
67
|
-
setDirectNotificationCounts(newCounts);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Initialize with current counts
|
|
71
|
-
if (notificationService.isReady()) {
|
|
72
|
-
setDirectNotificationCounts(notificationService.getCounts());
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return unsubscribe;
|
|
76
|
-
}, []);
|
|
77
|
-
|
|
78
|
-
const showPM = modalStateStore.showPM;
|
|
79
|
-
const showNotifications = modalStateStore.showNotifications;
|
|
80
|
-
|
|
81
|
-
// Create a stable callback for onUpdate that doesn't depend on props
|
|
82
|
-
const stableOnUpdate = React.useCallback(() => {
|
|
83
|
-
// Use NotificationService directly to avoid dependency on props.loadCounts
|
|
84
|
-
NotificationService.getInstance().refresh();
|
|
85
|
-
}, []);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
89
|
-
e.preventDefault();
|
|
90
|
-
setAnchorEl(e.currentTarget);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const handleClose = () => {
|
|
94
|
-
setTabIndex(0);
|
|
95
|
-
setAnchorEl(null);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const handleSwitchChurch = () => {
|
|
99
|
-
console.log('UserMenu - handleSwitchChurch called');
|
|
100
|
-
removeCookie("lastChurchId", { path: "/" });
|
|
101
|
-
setTabIndex(2);
|
|
102
|
-
console.log('UserMenu - tabIndex set to 2');
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const getMainLinks = () => {
|
|
106
|
-
const jwt = ApiHelper.getConfig("MembershipApi").jwt;
|
|
107
|
-
const churchId = UserHelper.currentUserChurch.church.id;
|
|
108
|
-
let result: React.ReactElement[] = [];
|
|
109
|
-
|
|
110
|
-
// Helper function to get label with fallback
|
|
111
|
-
const getLabel = (key: string, fallback: string) => {
|
|
112
|
-
const label = Locale.label(key);
|
|
113
|
-
return label && label !== key ? label : fallback;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
result.push(<NavItem onClick={() => {modalStateStore.setShowPM(true)}} label={getLabel("wrapper.messages", "Messages")} icon="mail" key="/messages" onNavigate={props.onNavigate} badgeCount={directNotificationCounts.pmCount} />);
|
|
117
|
-
|
|
118
|
-
result.push(<NavItem onClick={() => {modalStateStore.setShowNotifications(true)}} label={getLabel("wrapper.notifications", "Notifications")} icon="notifications" key="/notifications" onNavigate={props.onNavigate} badgeCount={directNotificationCounts.notificationCount} />);
|
|
119
|
-
|
|
120
|
-
if (props.appName === "CHUMS") result.push(<NavItem url={"/profile"} key="/profile" label={getLabel("wrapper.profile", "Profile")} icon="person" onNavigate={props.onNavigate} />);
|
|
121
|
-
else result.push(<NavItem url={`${CommonEnvironmentHelper.ChumsRoot}/login?jwt=${jwt}&churchId=${churchId}&returnUrl=/profile`} key="/profile" label={getLabel("wrapper.profile", "Profile")} icon="person" external={true} onNavigate={props.onNavigate} />);
|
|
122
|
-
// Create logout URL with current page as return URL
|
|
123
|
-
const currentPath = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
|
|
124
|
-
const logoutUrl = `/login?action=logout&returnUrl=${encodeURIComponent(currentPath)}`;
|
|
125
|
-
result.push(<NavItem url={logoutUrl} label={getLabel("wrapper.logout", "Logout")} icon="logout" key="/logout" onNavigate={props.onNavigate} />);
|
|
126
|
-
result.push(<div key="divider" style={{borderTop:"1px solid #CCC", paddingTop:2, paddingBottom:2}}></div>)
|
|
127
|
-
result.push(<NavItem label={getLabel("wrapper.switchApp", "Switch App")} key="Switch App" icon="apps" onClick={() => { setTabIndex(1); }} />);
|
|
128
|
-
result.push(<NavItem label={getLabel("wrapper.switchChurch", "Switch Church")} key="Switch Church" icon="church" onClick={handleSwitchChurch} />);
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const getProfilePic = () => {
|
|
133
|
-
if (props.profilePicture) return props.profilePicture
|
|
134
|
-
else return "/images/sample-profile.png";
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const paperProps = {
|
|
138
|
-
elevation: 0,
|
|
139
|
-
sx: {
|
|
140
|
-
overflow: "visible",
|
|
141
|
-
filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.32))",
|
|
142
|
-
mt: 1.5,
|
|
143
|
-
"& .MuiAvatar-root": { width: 32, height: 32, ml: -0.5, mr: 1 },
|
|
144
|
-
minWidth: 450
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const handleItemClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
149
|
-
// Handle menu item clicks if needed
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const [tabIndex, setTabIndex] = React.useState(0);
|
|
153
|
-
|
|
154
|
-
const getTabs = () => {
|
|
155
|
-
console.log('UserMenu getTabs - Current tabIndex:', tabIndex);
|
|
156
|
-
return (
|
|
157
|
-
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
|
158
|
-
<TabPanel value={tabIndex} index={0}>
|
|
159
|
-
{getMainLinks()}
|
|
160
|
-
</TabPanel>
|
|
161
|
-
<TabPanel value={tabIndex} index={1}>
|
|
162
|
-
<NavItem label="Back" key="AppBack" icon="arrow_back" onClick={() => { setTabIndex(0); }} />
|
|
163
|
-
<AppList currentUserChurch={props.context?.userChurch} appName={props.appName} onNavigate={props.onNavigate} />
|
|
164
|
-
</TabPanel>
|
|
165
|
-
<TabPanel value={tabIndex} index={2}>
|
|
166
|
-
<div style={{ maxHeight: '70vh', overflowY: "auto" }}>
|
|
167
|
-
<NavItem label="Back" key="ChurchBack" icon="arrow_back" onClick={() => { setTabIndex(0); }} />
|
|
168
|
-
{(() => {
|
|
169
|
-
console.log('UserMenu Church Tab - Rendering church list section');
|
|
170
|
-
console.log('UserMenu Church Tab - Full context:', props.context);
|
|
171
|
-
console.log('UserMenu Church Tab - context.userChurches:', props.context?.userChurches);
|
|
172
|
-
console.log('UserMenu Church Tab - context.userChurches[0]:', props.context?.userChurches?.[0]);
|
|
173
|
-
console.log('UserMenu Church Tab - context.userChurch:', props.context?.userChurch);
|
|
174
|
-
console.log('UserMenu Church Tab - userChurches type:', typeof props.context?.userChurches);
|
|
175
|
-
console.log('UserMenu Church Tab - userChurches is array?', Array.isArray(props.context?.userChurches));
|
|
176
|
-
console.log('UserMenu Church Tab - userChurches length:', props.context?.userChurches?.length);
|
|
177
|
-
|
|
178
|
-
// Check if userChurches is actually the userChurch object
|
|
179
|
-
if (props.context?.userChurches && !Array.isArray(props.context.userChurches) && (props.context.userChurches as any).id) {
|
|
180
|
-
console.error('UserMenu - ERROR: context.userChurches contains a single church object instead of an array!');
|
|
181
|
-
console.log('UserMenu - Attempting to use context.userChurch as single item array');
|
|
182
|
-
const churchArray = props.context.userChurch ? [props.context.userChurch] : [];
|
|
183
|
-
return <ChurchList userChurches={churchArray} currentUserChurch={props.context?.userChurch} context={props.context} onDelete={handleClose} onChurchChange={() => {
|
|
184
|
-
handleClose();
|
|
185
|
-
// Don't navigate - just close the menu and let the context update trigger re-renders
|
|
186
|
-
}} />;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (!props.context?.userChurches) {
|
|
190
|
-
return <Typography sx={{ p: 2, color: 'text.secondary' }}>Loading churches...</Typography>;
|
|
191
|
-
} else if (!Array.isArray(props.context.userChurches)) {
|
|
192
|
-
return <Typography sx={{ p: 2, color: 'text.secondary', fontWeight: 'bold' }}>Error: Invalid church data format</Typography>;
|
|
193
|
-
} else if (props.context.userChurches.length === 0) {
|
|
194
|
-
return <Typography sx={{ p: 2, color: 'text.secondary' }}>No churches available</Typography>;
|
|
195
|
-
} else {
|
|
196
|
-
// Ensure we always pass an array
|
|
197
|
-
const churchesArray = Array.isArray(props.context.userChurches)
|
|
198
|
-
? props.context.userChurches
|
|
199
|
-
: [props.context.userChurches];
|
|
200
|
-
console.log('UserMenu - Passing to ChurchList:', churchesArray);
|
|
201
|
-
return <ChurchList userChurches={churchesArray} currentUserChurch={props.context?.userChurch} context={props.context} onDelete={handleClose} onChurchChange={() => {
|
|
202
|
-
handleClose();
|
|
203
|
-
// Don't navigate - just close the menu and let the context update trigger re-renders
|
|
204
|
-
}} />;
|
|
205
|
-
}
|
|
206
|
-
})()}
|
|
207
|
-
</div>
|
|
208
|
-
</TabPanel>
|
|
209
|
-
</Box>
|
|
210
|
-
);
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const getModals = () => {
|
|
214
|
-
// Helper function to get label with fallback
|
|
215
|
-
const getLabel = (key: string, fallback: string) => {
|
|
216
|
-
const label = Locale.label(key);
|
|
217
|
-
return label && label !== key ? label : fallback;
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
return (
|
|
221
|
-
<>
|
|
222
|
-
<Dialog
|
|
223
|
-
id="private-messages-modal"
|
|
224
|
-
open={showPM}
|
|
225
|
-
onClose={() => {
|
|
226
|
-
modalStateStore.setShowPM(false);
|
|
227
|
-
}}
|
|
228
|
-
maxWidth="md"
|
|
229
|
-
fullWidth
|
|
230
|
-
PaperProps={{
|
|
231
|
-
sx: {
|
|
232
|
-
height: '80vh',
|
|
233
|
-
maxHeight: '700px',
|
|
234
|
-
display: 'flex',
|
|
235
|
-
flexDirection: 'column'
|
|
236
|
-
}
|
|
237
|
-
}}
|
|
238
|
-
>
|
|
239
|
-
<DialogTitle id="private-messages-title">{getLabel("wrapper.messages", "Messages")}</DialogTitle>
|
|
240
|
-
<DialogContent
|
|
241
|
-
sx={{
|
|
242
|
-
flex: 1,
|
|
243
|
-
display: 'flex',
|
|
244
|
-
flexDirection: 'column',
|
|
245
|
-
p: 0,
|
|
246
|
-
overflow: 'hidden',
|
|
247
|
-
minHeight: 0
|
|
248
|
-
}}
|
|
249
|
-
>
|
|
250
|
-
<PrivateMessages context={props.context} refreshKey={currentRefreshKey} onUpdate={stableOnUpdate} />
|
|
251
|
-
</DialogContent>
|
|
252
|
-
</Dialog>
|
|
253
|
-
|
|
254
|
-
<Dialog
|
|
255
|
-
id="notifications-modal"
|
|
256
|
-
open={showNotifications}
|
|
257
|
-
onClose={() => {
|
|
258
|
-
modalStateStore.setShowNotifications(false);
|
|
259
|
-
}}
|
|
260
|
-
maxWidth="md"
|
|
261
|
-
fullWidth
|
|
262
|
-
>
|
|
263
|
-
<DialogTitle id="notifications-title">{getLabel("wrapper.notifications", "Notifications")}</DialogTitle>
|
|
264
|
-
<DialogContent>
|
|
265
|
-
<Notifications context={props.context} appName={props.appName} onUpdate={props.loadCounts} onNavigate={props.onNavigate} />
|
|
266
|
-
</DialogContent>
|
|
267
|
-
</Dialog>
|
|
268
|
-
</>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const totalNotifcations = directNotificationCounts.notificationCount + directNotificationCounts.pmCount;
|
|
273
|
-
|
|
274
|
-
// Use a ref to track if we should update refresh key
|
|
275
|
-
const stableRefreshKeyRef = React.useRef(refreshKey);
|
|
276
|
-
|
|
277
|
-
// Set up WebSocket handlers to update refreshKey when messages arrive
|
|
278
|
-
React.useEffect(() => {
|
|
279
|
-
if (!props.context?.person?.id) return;
|
|
280
|
-
|
|
281
|
-
const handleMessageUpdate = (data: any) => {
|
|
282
|
-
// Only update refreshKey if a modal is open to trigger child updates
|
|
283
|
-
if (modalStateStore.showPM || modalStateStore.showNotifications) {
|
|
284
|
-
const newKey = Math.random();
|
|
285
|
-
setRefreshKey(newKey);
|
|
286
|
-
stableRefreshKeyRef.current = newKey;
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const handlePrivateMessage = (data: any) => {
|
|
291
|
-
// Only update refreshKey if PM modal is open
|
|
292
|
-
if (modalStateStore.showPM) {
|
|
293
|
-
const newKey = Math.random();
|
|
294
|
-
setRefreshKey(newKey);
|
|
295
|
-
stableRefreshKeyRef.current = newKey;
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const handleNotification = (data: any) => {
|
|
300
|
-
// Update refreshKey if any modal is open to trigger child updates
|
|
301
|
-
if (modalStateStore.showPM || modalStateStore.showNotifications) {
|
|
302
|
-
const newKey = Math.random();
|
|
303
|
-
setRefreshKey(newKey);
|
|
304
|
-
stableRefreshKeyRef.current = newKey;
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
// Register WebSocket handlers
|
|
309
|
-
const messageHandlerId = `UserMenu-MessageUpdate-${props.context.person.id}`;
|
|
310
|
-
const privateMessageHandlerId = `UserMenu-PrivateMessage-${props.context.person.id}`;
|
|
311
|
-
const notificationHandlerId = `UserMenu-Notification-${props.context.person.id}`;
|
|
312
|
-
|
|
313
|
-
SocketHelper.addHandler("message", messageHandlerId, handleMessageUpdate);
|
|
314
|
-
SocketHelper.addHandler("privateMessage", privateMessageHandlerId, handlePrivateMessage);
|
|
315
|
-
SocketHelper.addHandler("notification", notificationHandlerId, handleNotification);
|
|
316
|
-
|
|
317
|
-
// Cleanup
|
|
318
|
-
return () => {
|
|
319
|
-
SocketHelper.removeHandler(messageHandlerId);
|
|
320
|
-
SocketHelper.removeHandler(privateMessageHandlerId);
|
|
321
|
-
SocketHelper.removeHandler(notificationHandlerId);
|
|
322
|
-
};
|
|
323
|
-
}, [props.context?.person?.id]); // Removed showPM, showNotifications dependencies
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// Use current refresh key
|
|
327
|
-
const currentRefreshKey = refreshKey;
|
|
328
|
-
|
|
329
|
-
return (
|
|
330
|
-
<>
|
|
331
|
-
<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={<Icon>expand_more</Icon>}>
|
|
332
|
-
<Badge id="user-menu-notification-badge" badgeContent={totalNotifcations} color="error" invisible={totalNotifcations===0}>
|
|
333
|
-
<Avatar id="user-menu-avatar" src={getProfilePic()} sx={{ width: 32, height: 32, marginRight: 1 }}></Avatar>
|
|
334
|
-
</Badge>
|
|
335
|
-
</Button>
|
|
336
|
-
|
|
337
|
-
<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 } }}>
|
|
338
|
-
{getTabs()}
|
|
339
|
-
</Menu>
|
|
340
|
-
{getModals()}
|
|
341
|
-
</>
|
|
342
|
-
);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
export const UserMenu: React.FC<Props> = React.memo((props) => {
|
|
346
|
-
return (
|
|
347
|
-
<CookiesProvider defaultSetOptions={{ path: '/' }}>
|
|
348
|
-
<UserMenuContent {...props} />
|
|
349
|
-
</CookiesProvider>
|
|
350
|
-
);
|
|
351
|
-
}, (prevProps, nextProps) => {
|
|
352
|
-
// Only re-render if essential props change, ignore notification count changes completely
|
|
353
|
-
if (prevProps.userName !== nextProps.userName) {
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (prevProps.profilePicture !== nextProps.profilePicture) {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (prevProps.appName !== nextProps.appName) {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Check if context has actually changed (deep comparison of relevant parts)
|
|
366
|
-
if (prevProps.context?.person?.id !== nextProps.context?.person?.id ||
|
|
367
|
-
prevProps.context?.userChurch?.church?.id !== nextProps.context?.userChurch?.church?.id) {
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Check if userChurches array changed in context
|
|
372
|
-
if (prevProps.context?.userChurches?.length !== nextProps.context?.userChurches?.length) {
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Check if loadCounts function reference changed (important for functionality)
|
|
377
|
-
if (prevProps.loadCounts !== nextProps.loadCounts) {
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Ignore both notificationCounts and onNavigate changes as they don't affect the component
|
|
382
|
-
return true; // Skip re-render
|
|
383
|
-
});
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { ApiHelper, UserHelper, LoginUserChurchInterface, UserContextInterface, CommonEnvironmentHelper } from "@churchapps/helpers";
|
|
5
|
+
import { Avatar, Menu, Typography, Icon, Button, Box, Badge, Dialog, DialogContent, DialogTitle } from "@mui/material";
|
|
6
|
+
import { NavItem, AppList } from ".";
|
|
7
|
+
import { ChurchList } from "./ChurchList";
|
|
8
|
+
import { TabPanel } from "../TabPanel";
|
|
9
|
+
import { Locale } from "../../helpers";
|
|
10
|
+
import { PrivateMessages } from "./PrivateMessages";
|
|
11
|
+
import { Notifications } from "./Notifications";
|
|
12
|
+
import { useCookies, CookiesProvider } from "react-cookie";
|
|
13
|
+
import { NotificationService } from "../../helpers/NotificationService";
|
|
14
|
+
import { SocketHelper } from "../../helpers";
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
notificationCounts: { notificationCount: number, pmCount: number };
|
|
19
|
+
loadCounts: () => void;
|
|
20
|
+
userName: string;
|
|
21
|
+
profilePicture: string;
|
|
22
|
+
context: UserContextInterface;
|
|
23
|
+
appName: string;
|
|
24
|
+
onNavigate: (url: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Create a persistent store for modal state that survives component re-renders
|
|
28
|
+
const modalStateStore = {
|
|
29
|
+
showPM: false,
|
|
30
|
+
showNotifications: false,
|
|
31
|
+
listeners: new Set<() => void>(),
|
|
32
|
+
|
|
33
|
+
setShowPM(value: boolean) {
|
|
34
|
+
this.showPM = value;
|
|
35
|
+
this.listeners.forEach((listener: () => void) => listener());
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
setShowNotifications(value: boolean) {
|
|
39
|
+
this.showNotifications = value;
|
|
40
|
+
this.listeners.forEach((listener: () => void) => listener());
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
subscribe(listener: () => void) {
|
|
44
|
+
this.listeners.add(listener);
|
|
45
|
+
return () => this.listeners.delete(listener);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const UserMenuContent: React.FC<Props> = React.memo((props) => {
|
|
50
|
+
const userName = props.userName;
|
|
51
|
+
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
52
|
+
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
|
|
53
|
+
const [refreshKey, setRefreshKey] = React.useState(() => Math.random());
|
|
54
|
+
const [, , removeCookie] = useCookies(["lastChurchId"]);
|
|
55
|
+
const [directNotificationCounts, setDirectNotificationCounts] = React.useState(() => props.notificationCounts || { notificationCount: 0, pmCount: 0 });
|
|
56
|
+
const open = Boolean(anchorEl);
|
|
57
|
+
|
|
58
|
+
// Subscribe to modal state changes
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
return modalStateStore.subscribe(forceUpdate);
|
|
61
|
+
}, [forceUpdate]);
|
|
62
|
+
|
|
63
|
+
// Subscribe directly to NotificationService to update badge counts without re-renders
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
const notificationService = NotificationService.getInstance();
|
|
66
|
+
const unsubscribe = notificationService.subscribe((newCounts) => {
|
|
67
|
+
setDirectNotificationCounts(newCounts);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Initialize with current counts
|
|
71
|
+
if (notificationService.isReady()) {
|
|
72
|
+
setDirectNotificationCounts(notificationService.getCounts());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return unsubscribe;
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const showPM = modalStateStore.showPM;
|
|
79
|
+
const showNotifications = modalStateStore.showNotifications;
|
|
80
|
+
|
|
81
|
+
// Create a stable callback for onUpdate that doesn't depend on props
|
|
82
|
+
const stableOnUpdate = React.useCallback(() => {
|
|
83
|
+
// Use NotificationService directly to avoid dependency on props.loadCounts
|
|
84
|
+
NotificationService.getInstance().refresh();
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
setAnchorEl(e.currentTarget);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleClose = () => {
|
|
94
|
+
setTabIndex(0);
|
|
95
|
+
setAnchorEl(null);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleSwitchChurch = () => {
|
|
99
|
+
console.log('UserMenu - handleSwitchChurch called');
|
|
100
|
+
removeCookie("lastChurchId", { path: "/" });
|
|
101
|
+
setTabIndex(2);
|
|
102
|
+
console.log('UserMenu - tabIndex set to 2');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getMainLinks = () => {
|
|
106
|
+
const jwt = ApiHelper.getConfig("MembershipApi").jwt;
|
|
107
|
+
const churchId = UserHelper.currentUserChurch.church.id;
|
|
108
|
+
let result: React.ReactElement[] = [];
|
|
109
|
+
|
|
110
|
+
// Helper function to get label with fallback
|
|
111
|
+
const getLabel = (key: string, fallback: string) => {
|
|
112
|
+
const label = Locale.label(key);
|
|
113
|
+
return label && label !== key ? label : fallback;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
result.push(<NavItem onClick={() => {modalStateStore.setShowPM(true)}} label={getLabel("wrapper.messages", "Messages")} icon="mail" key="/messages" onNavigate={props.onNavigate} badgeCount={directNotificationCounts.pmCount} />);
|
|
117
|
+
|
|
118
|
+
result.push(<NavItem onClick={() => {modalStateStore.setShowNotifications(true)}} label={getLabel("wrapper.notifications", "Notifications")} icon="notifications" key="/notifications" onNavigate={props.onNavigate} badgeCount={directNotificationCounts.notificationCount} />);
|
|
119
|
+
|
|
120
|
+
if (props.appName === "CHUMS") result.push(<NavItem url={"/profile"} key="/profile" label={getLabel("wrapper.profile", "Profile")} icon="person" onNavigate={props.onNavigate} />);
|
|
121
|
+
else result.push(<NavItem url={`${CommonEnvironmentHelper.ChumsRoot}/login?jwt=${jwt}&churchId=${churchId}&returnUrl=/profile`} key="/profile" label={getLabel("wrapper.profile", "Profile")} icon="person" external={true} onNavigate={props.onNavigate} />);
|
|
122
|
+
// Create logout URL with current page as return URL
|
|
123
|
+
const currentPath = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
|
|
124
|
+
const logoutUrl = `/login?action=logout&returnUrl=${encodeURIComponent(currentPath)}`;
|
|
125
|
+
result.push(<NavItem url={logoutUrl} label={getLabel("wrapper.logout", "Logout")} icon="logout" key="/logout" onNavigate={props.onNavigate} />);
|
|
126
|
+
result.push(<div key="divider" style={{borderTop:"1px solid #CCC", paddingTop:2, paddingBottom:2}}></div>)
|
|
127
|
+
result.push(<NavItem label={getLabel("wrapper.switchApp", "Switch App")} key="Switch App" icon="apps" onClick={() => { setTabIndex(1); }} />);
|
|
128
|
+
result.push(<NavItem label={getLabel("wrapper.switchChurch", "Switch Church")} key="Switch Church" icon="church" onClick={handleSwitchChurch} />);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const getProfilePic = () => {
|
|
133
|
+
if (props.profilePicture) return props.profilePicture
|
|
134
|
+
else return "/images/sample-profile.png";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const paperProps = {
|
|
138
|
+
elevation: 0,
|
|
139
|
+
sx: {
|
|
140
|
+
overflow: "visible",
|
|
141
|
+
filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.32))",
|
|
142
|
+
mt: 1.5,
|
|
143
|
+
"& .MuiAvatar-root": { width: 32, height: 32, ml: -0.5, mr: 1 },
|
|
144
|
+
minWidth: 450
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handleItemClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
149
|
+
// Handle menu item clicks if needed
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const [tabIndex, setTabIndex] = React.useState(0);
|
|
153
|
+
|
|
154
|
+
const getTabs = () => {
|
|
155
|
+
console.log('UserMenu getTabs - Current tabIndex:', tabIndex);
|
|
156
|
+
return (
|
|
157
|
+
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
|
158
|
+
<TabPanel value={tabIndex} index={0}>
|
|
159
|
+
{getMainLinks()}
|
|
160
|
+
</TabPanel>
|
|
161
|
+
<TabPanel value={tabIndex} index={1}>
|
|
162
|
+
<NavItem label="Back" key="AppBack" icon="arrow_back" onClick={() => { setTabIndex(0); }} />
|
|
163
|
+
<AppList currentUserChurch={props.context?.userChurch} appName={props.appName} onNavigate={props.onNavigate} />
|
|
164
|
+
</TabPanel>
|
|
165
|
+
<TabPanel value={tabIndex} index={2}>
|
|
166
|
+
<div style={{ maxHeight: '70vh', overflowY: "auto" }}>
|
|
167
|
+
<NavItem label="Back" key="ChurchBack" icon="arrow_back" onClick={() => { setTabIndex(0); }} />
|
|
168
|
+
{(() => {
|
|
169
|
+
console.log('UserMenu Church Tab - Rendering church list section');
|
|
170
|
+
console.log('UserMenu Church Tab - Full context:', props.context);
|
|
171
|
+
console.log('UserMenu Church Tab - context.userChurches:', props.context?.userChurches);
|
|
172
|
+
console.log('UserMenu Church Tab - context.userChurches[0]:', props.context?.userChurches?.[0]);
|
|
173
|
+
console.log('UserMenu Church Tab - context.userChurch:', props.context?.userChurch);
|
|
174
|
+
console.log('UserMenu Church Tab - userChurches type:', typeof props.context?.userChurches);
|
|
175
|
+
console.log('UserMenu Church Tab - userChurches is array?', Array.isArray(props.context?.userChurches));
|
|
176
|
+
console.log('UserMenu Church Tab - userChurches length:', props.context?.userChurches?.length);
|
|
177
|
+
|
|
178
|
+
// Check if userChurches is actually the userChurch object
|
|
179
|
+
if (props.context?.userChurches && !Array.isArray(props.context.userChurches) && (props.context.userChurches as any).id) {
|
|
180
|
+
console.error('UserMenu - ERROR: context.userChurches contains a single church object instead of an array!');
|
|
181
|
+
console.log('UserMenu - Attempting to use context.userChurch as single item array');
|
|
182
|
+
const churchArray = props.context.userChurch ? [props.context.userChurch] : [];
|
|
183
|
+
return <ChurchList userChurches={churchArray} currentUserChurch={props.context?.userChurch} context={props.context} onDelete={handleClose} onChurchChange={() => {
|
|
184
|
+
handleClose();
|
|
185
|
+
// Don't navigate - just close the menu and let the context update trigger re-renders
|
|
186
|
+
}} />;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!props.context?.userChurches) {
|
|
190
|
+
return <Typography sx={{ p: 2, color: 'text.secondary' }}>Loading churches...</Typography>;
|
|
191
|
+
} else if (!Array.isArray(props.context.userChurches)) {
|
|
192
|
+
return <Typography sx={{ p: 2, color: 'text.secondary', fontWeight: 'bold' }}>Error: Invalid church data format</Typography>;
|
|
193
|
+
} else if (props.context.userChurches.length === 0) {
|
|
194
|
+
return <Typography sx={{ p: 2, color: 'text.secondary' }}>No churches available</Typography>;
|
|
195
|
+
} else {
|
|
196
|
+
// Ensure we always pass an array
|
|
197
|
+
const churchesArray = Array.isArray(props.context.userChurches)
|
|
198
|
+
? props.context.userChurches
|
|
199
|
+
: [props.context.userChurches];
|
|
200
|
+
console.log('UserMenu - Passing to ChurchList:', churchesArray);
|
|
201
|
+
return <ChurchList userChurches={churchesArray} currentUserChurch={props.context?.userChurch} context={props.context} onDelete={handleClose} onChurchChange={() => {
|
|
202
|
+
handleClose();
|
|
203
|
+
// Don't navigate - just close the menu and let the context update trigger re-renders
|
|
204
|
+
}} />;
|
|
205
|
+
}
|
|
206
|
+
})()}
|
|
207
|
+
</div>
|
|
208
|
+
</TabPanel>
|
|
209
|
+
</Box>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const getModals = () => {
|
|
214
|
+
// Helper function to get label with fallback
|
|
215
|
+
const getLabel = (key: string, fallback: string) => {
|
|
216
|
+
const label = Locale.label(key);
|
|
217
|
+
return label && label !== key ? label : fallback;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
<Dialog
|
|
223
|
+
id="private-messages-modal"
|
|
224
|
+
open={showPM}
|
|
225
|
+
onClose={() => {
|
|
226
|
+
modalStateStore.setShowPM(false);
|
|
227
|
+
}}
|
|
228
|
+
maxWidth="md"
|
|
229
|
+
fullWidth
|
|
230
|
+
PaperProps={{
|
|
231
|
+
sx: {
|
|
232
|
+
height: '80vh',
|
|
233
|
+
maxHeight: '700px',
|
|
234
|
+
display: 'flex',
|
|
235
|
+
flexDirection: 'column'
|
|
236
|
+
}
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<DialogTitle id="private-messages-title">{getLabel("wrapper.messages", "Messages")}</DialogTitle>
|
|
240
|
+
<DialogContent
|
|
241
|
+
sx={{
|
|
242
|
+
flex: 1,
|
|
243
|
+
display: 'flex',
|
|
244
|
+
flexDirection: 'column',
|
|
245
|
+
p: 0,
|
|
246
|
+
overflow: 'hidden',
|
|
247
|
+
minHeight: 0
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
<PrivateMessages context={props.context} refreshKey={currentRefreshKey} onUpdate={stableOnUpdate} />
|
|
251
|
+
</DialogContent>
|
|
252
|
+
</Dialog>
|
|
253
|
+
|
|
254
|
+
<Dialog
|
|
255
|
+
id="notifications-modal"
|
|
256
|
+
open={showNotifications}
|
|
257
|
+
onClose={() => {
|
|
258
|
+
modalStateStore.setShowNotifications(false);
|
|
259
|
+
}}
|
|
260
|
+
maxWidth="md"
|
|
261
|
+
fullWidth
|
|
262
|
+
>
|
|
263
|
+
<DialogTitle id="notifications-title">{getLabel("wrapper.notifications", "Notifications")}</DialogTitle>
|
|
264
|
+
<DialogContent>
|
|
265
|
+
<Notifications context={props.context} appName={props.appName} onUpdate={props.loadCounts} onNavigate={props.onNavigate} />
|
|
266
|
+
</DialogContent>
|
|
267
|
+
</Dialog>
|
|
268
|
+
</>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const totalNotifcations = directNotificationCounts.notificationCount + directNotificationCounts.pmCount;
|
|
273
|
+
|
|
274
|
+
// Use a ref to track if we should update refresh key
|
|
275
|
+
const stableRefreshKeyRef = React.useRef(refreshKey);
|
|
276
|
+
|
|
277
|
+
// Set up WebSocket handlers to update refreshKey when messages arrive
|
|
278
|
+
React.useEffect(() => {
|
|
279
|
+
if (!props.context?.person?.id) return;
|
|
280
|
+
|
|
281
|
+
const handleMessageUpdate = (data: any) => {
|
|
282
|
+
// Only update refreshKey if a modal is open to trigger child updates
|
|
283
|
+
if (modalStateStore.showPM || modalStateStore.showNotifications) {
|
|
284
|
+
const newKey = Math.random();
|
|
285
|
+
setRefreshKey(newKey);
|
|
286
|
+
stableRefreshKeyRef.current = newKey;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const handlePrivateMessage = (data: any) => {
|
|
291
|
+
// Only update refreshKey if PM modal is open
|
|
292
|
+
if (modalStateStore.showPM) {
|
|
293
|
+
const newKey = Math.random();
|
|
294
|
+
setRefreshKey(newKey);
|
|
295
|
+
stableRefreshKeyRef.current = newKey;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const handleNotification = (data: any) => {
|
|
300
|
+
// Update refreshKey if any modal is open to trigger child updates
|
|
301
|
+
if (modalStateStore.showPM || modalStateStore.showNotifications) {
|
|
302
|
+
const newKey = Math.random();
|
|
303
|
+
setRefreshKey(newKey);
|
|
304
|
+
stableRefreshKeyRef.current = newKey;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Register WebSocket handlers
|
|
309
|
+
const messageHandlerId = `UserMenu-MessageUpdate-${props.context.person.id}`;
|
|
310
|
+
const privateMessageHandlerId = `UserMenu-PrivateMessage-${props.context.person.id}`;
|
|
311
|
+
const notificationHandlerId = `UserMenu-Notification-${props.context.person.id}`;
|
|
312
|
+
|
|
313
|
+
SocketHelper.addHandler("message", messageHandlerId, handleMessageUpdate);
|
|
314
|
+
SocketHelper.addHandler("privateMessage", privateMessageHandlerId, handlePrivateMessage);
|
|
315
|
+
SocketHelper.addHandler("notification", notificationHandlerId, handleNotification);
|
|
316
|
+
|
|
317
|
+
// Cleanup
|
|
318
|
+
return () => {
|
|
319
|
+
SocketHelper.removeHandler(messageHandlerId);
|
|
320
|
+
SocketHelper.removeHandler(privateMessageHandlerId);
|
|
321
|
+
SocketHelper.removeHandler(notificationHandlerId);
|
|
322
|
+
};
|
|
323
|
+
}, [props.context?.person?.id]); // Removed showPM, showNotifications dependencies
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
// Use current refresh key
|
|
327
|
+
const currentRefreshKey = refreshKey;
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<>
|
|
331
|
+
<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={<Icon>expand_more</Icon>}>
|
|
332
|
+
<Badge id="user-menu-notification-badge" badgeContent={totalNotifcations} color="error" invisible={totalNotifcations===0}>
|
|
333
|
+
<Avatar id="user-menu-avatar" src={getProfilePic()} sx={{ width: 32, height: 32, marginRight: 1 }}></Avatar>
|
|
334
|
+
</Badge>
|
|
335
|
+
</Button>
|
|
336
|
+
|
|
337
|
+
<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 } }}>
|
|
338
|
+
{getTabs()}
|
|
339
|
+
</Menu>
|
|
340
|
+
{getModals()}
|
|
341
|
+
</>
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
export const UserMenu: React.FC<Props> = React.memo((props) => {
|
|
346
|
+
return (
|
|
347
|
+
<CookiesProvider defaultSetOptions={{ path: '/' }}>
|
|
348
|
+
<UserMenuContent {...props} />
|
|
349
|
+
</CookiesProvider>
|
|
350
|
+
);
|
|
351
|
+
}, (prevProps, nextProps) => {
|
|
352
|
+
// Only re-render if essential props change, ignore notification count changes completely
|
|
353
|
+
if (prevProps.userName !== nextProps.userName) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (prevProps.profilePicture !== nextProps.profilePicture) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (prevProps.appName !== nextProps.appName) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check if context has actually changed (deep comparison of relevant parts)
|
|
366
|
+
if (prevProps.context?.person?.id !== nextProps.context?.person?.id ||
|
|
367
|
+
prevProps.context?.userChurch?.church?.id !== nextProps.context?.userChurch?.church?.id) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check if userChurches array changed in context
|
|
372
|
+
if (prevProps.context?.userChurches?.length !== nextProps.context?.userChurches?.length) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Check if loadCounts function reference changed (important for functionality)
|
|
377
|
+
if (prevProps.loadCounts !== nextProps.loadCounts) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Ignore both notificationCounts and onNavigate changes as they don't affect the component
|
|
382
|
+
return true; // Skip re-render
|
|
383
|
+
});
|