@churchapps/apphelper 0.4.50 → 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.
Files changed (102) hide show
  1. package/dist/components/FormCardPayment.d.ts +1 -1
  2. package/dist/components/FormCardPayment.d.ts.map +1 -1
  3. package/dist/components/FormCardPayment.js +27 -10
  4. package/dist/components/FormCardPayment.js.map +1 -1
  5. package/dist/components/Loading.js +36 -36
  6. package/dist/components/notes/Notes.js +27 -27
  7. package/dist/helpers/index.d.ts +1 -1
  8. package/dist/helpers/index.d.ts.map +1 -1
  9. package/dist/helpers/index.js +1 -1
  10. package/dist/helpers/index.js.map +1 -1
  11. package/dist/public/css/cropper.css +309 -309
  12. package/dist/public/css/styles.css +111 -111
  13. package/package.json +72 -73
  14. package/public/css/cropper.css +309 -309
  15. package/public/css/styles.css +111 -111
  16. package/public/locales/de.json +269 -269
  17. package/public/locales/en.json +276 -276
  18. package/public/locales/es.json +272 -272
  19. package/public/locales/fr.json +269 -269
  20. package/public/locales/hi.json +269 -269
  21. package/public/locales/it.json +269 -269
  22. package/public/locales/ko.json +269 -269
  23. package/public/locales/no.json +269 -269
  24. package/public/locales/pt.json +269 -269
  25. package/public/locales/ru.json +269 -269
  26. package/public/locales/tl.json +269 -269
  27. package/public/locales/zh.json +269 -269
  28. package/src/components/DisplayBox.tsx +83 -83
  29. package/src/components/ErrorMessages.tsx +28 -28
  30. package/src/components/ExportLink.tsx +81 -81
  31. package/src/components/FloatingSupport.tsx +18 -18
  32. package/src/components/FormCardPayment.tsx +184 -169
  33. package/src/components/FormSubmissionEdit.tsx +168 -168
  34. package/src/components/HelpIcon.tsx +12 -12
  35. package/src/components/ImageEditor.tsx +161 -161
  36. package/src/components/InputBox.tsx +96 -96
  37. package/src/components/Loading.tsx +77 -77
  38. package/src/components/PageHeader.tsx +110 -110
  39. package/src/components/PersonAvatar.tsx +77 -77
  40. package/src/components/QuestionEdit.tsx +99 -99
  41. package/src/components/SmallButton.tsx +42 -42
  42. package/src/components/SupportModal.tsx +32 -32
  43. package/src/components/TabPanel.tsx +28 -28
  44. package/src/components/gallery/GalleryModal.tsx +173 -173
  45. package/src/components/gallery/StockPhotos.tsx +95 -95
  46. package/src/components/gallery/index.ts +1 -1
  47. package/src/components/header/Banner.tsx +11 -11
  48. package/src/components/header/PrimaryMenu.tsx +100 -100
  49. package/src/components/header/SecondaryMenu.tsx +23 -23
  50. package/src/components/header/SecondaryMenuAlt.tsx +40 -40
  51. package/src/components/header/SiteHeader.tsx +207 -207
  52. package/src/components/header/SupportDrawer.tsx +111 -111
  53. package/src/components/header/index.tsx +2 -2
  54. package/src/components/index.tsx +20 -20
  55. package/src/components/notes/AddNote.tsx +180 -180
  56. package/src/components/notes/Note.tsx +68 -68
  57. package/src/components/notes/Notes.tsx +208 -208
  58. package/src/components/notes/index.ts +3 -3
  59. package/src/components/wrapper/AppList.tsx +19 -19
  60. package/src/components/wrapper/ChurchList.tsx +154 -154
  61. package/src/components/wrapper/NavItem.tsx +47 -47
  62. package/src/components/wrapper/NewPrivateMessage.tsx +253 -253
  63. package/src/components/wrapper/Notifications.tsx +223 -223
  64. package/src/components/wrapper/PrivateMessageDetails.tsx +112 -112
  65. package/src/components/wrapper/PrivateMessages.tsx +576 -576
  66. package/src/components/wrapper/UserMenu.tsx +383 -383
  67. package/src/components/wrapper/index.tsx +8 -8
  68. package/src/helpers/AnalyticsHelper.ts +32 -32
  69. package/src/helpers/AppearanceHelper.ts +73 -73
  70. package/src/helpers/ArrayHelper.ts +87 -87
  71. package/src/helpers/CurrencyHelper.ts +10 -10
  72. package/src/helpers/DateHelper.ts +104 -104
  73. package/src/helpers/ErrorHelper.ts +43 -43
  74. package/src/helpers/EventHelper.ts +49 -49
  75. package/src/helpers/FileHelper.ts +31 -31
  76. package/src/helpers/Locale.ts +457 -457
  77. package/src/helpers/NotificationService.ts +296 -296
  78. package/src/helpers/PersonHelper.ts +62 -62
  79. package/src/helpers/SlugHelper.ts +37 -37
  80. package/src/helpers/SocketHelper.ts +296 -296
  81. package/src/helpers/UniqueIdHelper.ts +36 -36
  82. package/src/helpers/UserHelper.ts +104 -104
  83. package/src/helpers/createEmotionCache.ts +17 -17
  84. package/src/helpers/index.ts +58 -50
  85. package/src/hooks/index.ts +3 -3
  86. package/src/hooks/useMountedState.ts +16 -16
  87. package/src/hooks/useNotifications.ts +93 -93
  88. package/src/index.ts +2 -2
  89. package/src/types/interface-extensions.d.ts +12 -0
  90. package/tsconfig.json +31 -31
  91. package/dist/public/locales/de.json +0 -270
  92. package/dist/public/locales/en.json +0 -277
  93. package/dist/public/locales/es.json +0 -272
  94. package/dist/public/locales/fr.json +0 -270
  95. package/dist/public/locales/hi.json +0 -270
  96. package/dist/public/locales/it.json +0 -270
  97. package/dist/public/locales/ko.json +0 -270
  98. package/dist/public/locales/no.json +0 -270
  99. package/dist/public/locales/pt.json +0 -270
  100. package/dist/public/locales/ru.json +0 -270
  101. package/dist/public/locales/tl.json +0 -270
  102. 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
+ });