@churchapps/apphelper 0.4.17 → 0.4.19

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 (146) hide show
  1. package/dist/components/DisplayBox.js +1 -1
  2. package/dist/components/DisplayBox.js.map +1 -1
  3. package/dist/components/FormCardPayment.js +2 -2
  4. package/dist/components/FormCardPayment.js.map +1 -1
  5. package/dist/components/FormSubmissionEdit.d.ts.map +1 -1
  6. package/dist/components/FormSubmissionEdit.js +4 -5
  7. package/dist/components/FormSubmissionEdit.js.map +1 -1
  8. package/dist/components/InputBox.js +1 -1
  9. package/dist/components/InputBox.js.map +1 -1
  10. package/dist/components/Loading.js +1 -1
  11. package/dist/components/Loading.js.map +1 -1
  12. package/dist/components/PageHeader.d.ts +15 -0
  13. package/dist/components/PageHeader.d.ts.map +1 -0
  14. package/dist/components/PageHeader.js +41 -0
  15. package/dist/components/PageHeader.js.map +1 -0
  16. package/dist/components/PersonAvatar.d.ts +12 -0
  17. package/dist/components/PersonAvatar.d.ts.map +1 -0
  18. package/dist/components/PersonAvatar.js +55 -0
  19. package/dist/components/PersonAvatar.js.map +1 -0
  20. package/dist/components/header/SiteHeader.d.ts +2 -1
  21. package/dist/components/header/SiteHeader.d.ts.map +1 -1
  22. package/dist/components/header/SiteHeader.js +100 -4
  23. package/dist/components/header/SiteHeader.js.map +1 -1
  24. package/dist/components/header/SupportDrawer.js.map +1 -1
  25. package/dist/components/index.d.ts +2 -0
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/components/index.js +2 -0
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/notes/AddNote.d.ts.map +1 -1
  30. package/dist/components/notes/AddNote.js +45 -7
  31. package/dist/components/notes/AddNote.js.map +1 -1
  32. package/dist/components/notes/Note.d.ts.map +1 -1
  33. package/dist/components/notes/Note.js +6 -6
  34. package/dist/components/notes/Note.js.map +1 -1
  35. package/dist/components/notes/Notes.d.ts.map +1 -1
  36. package/dist/components/notes/Notes.js +120 -20
  37. package/dist/components/notes/Notes.js.map +1 -1
  38. package/dist/components/wrapper/ChurchList.d.ts.map +1 -1
  39. package/dist/components/wrapper/ChurchList.js +44 -6
  40. package/dist/components/wrapper/ChurchList.js.map +1 -1
  41. package/dist/components/wrapper/NewPrivateMessage.d.ts.map +1 -1
  42. package/dist/components/wrapper/NewPrivateMessage.js +28 -21
  43. package/dist/components/wrapper/NewPrivateMessage.js.map +1 -1
  44. package/dist/components/wrapper/Notifications.d.ts.map +1 -1
  45. package/dist/components/wrapper/Notifications.js +47 -20
  46. package/dist/components/wrapper/Notifications.js.map +1 -1
  47. package/dist/components/wrapper/PrivateMessageDetails.d.ts +1 -0
  48. package/dist/components/wrapper/PrivateMessageDetails.d.ts.map +1 -1
  49. package/dist/components/wrapper/PrivateMessageDetails.js +53 -4
  50. package/dist/components/wrapper/PrivateMessageDetails.js.map +1 -1
  51. package/dist/components/wrapper/PrivateMessages.d.ts.map +1 -1
  52. package/dist/components/wrapper/PrivateMessages.js +360 -41
  53. package/dist/components/wrapper/PrivateMessages.js.map +1 -1
  54. package/dist/components/wrapper/UserMenu.d.ts.map +1 -1
  55. package/dist/components/wrapper/UserMenu.js +164 -27
  56. package/dist/components/wrapper/UserMenu.js.map +1 -1
  57. package/dist/components/wrapper/index.d.ts +2 -1
  58. package/dist/components/wrapper/index.d.ts.map +1 -1
  59. package/dist/components/wrapper/index.js +2 -1
  60. package/dist/components/wrapper/index.js.map +1 -1
  61. package/dist/helpers/ArrayHelper.d.ts.map +1 -1
  62. package/dist/helpers/ArrayHelper.js +0 -1
  63. package/dist/helpers/ArrayHelper.js.map +1 -1
  64. package/dist/helpers/ErrorHelper.js +1 -1
  65. package/dist/helpers/ErrorHelper.js.map +1 -1
  66. package/dist/helpers/EventHelper.d.ts.map +1 -1
  67. package/dist/helpers/EventHelper.js +0 -3
  68. package/dist/helpers/EventHelper.js.map +1 -1
  69. package/dist/helpers/Locale.d.ts +1 -1
  70. package/dist/helpers/Locale.d.ts.map +1 -1
  71. package/dist/helpers/Locale.js +7 -2
  72. package/dist/helpers/Locale.js.map +1 -1
  73. package/dist/helpers/NotificationService.d.ts +56 -0
  74. package/dist/helpers/NotificationService.d.ts.map +1 -0
  75. package/dist/helpers/NotificationService.js +176 -0
  76. package/dist/helpers/NotificationService.js.map +1 -0
  77. package/dist/helpers/SocketHelper.d.ts.map +1 -1
  78. package/dist/helpers/SocketHelper.js +22 -17
  79. package/dist/helpers/SocketHelper.js.map +1 -1
  80. package/dist/helpers/UserHelper.js +2 -2
  81. package/dist/helpers/UserHelper.js.map +1 -1
  82. package/dist/helpers/index.d.ts +2 -0
  83. package/dist/helpers/index.d.ts.map +1 -1
  84. package/dist/helpers/index.js +1 -0
  85. package/dist/helpers/index.js.map +1 -1
  86. package/dist/hooks/index.d.ts +2 -0
  87. package/dist/hooks/index.d.ts.map +1 -1
  88. package/dist/hooks/index.js +1 -0
  89. package/dist/hooks/index.js.map +1 -1
  90. package/dist/hooks/useNotifications.d.ts +30 -0
  91. package/dist/hooks/useNotifications.d.ts.map +1 -0
  92. package/dist/hooks/useNotifications.js +79 -0
  93. package/dist/hooks/useNotifications.js.map +1 -0
  94. package/dist/public/css/styles.css +6 -2
  95. package/package.json +1 -1
  96. package/public/css/styles.css +6 -2
  97. package/src/components/DisplayBox.tsx +8 -8
  98. package/src/components/FormCardPayment.tsx +2 -2
  99. package/src/components/FormSubmissionEdit.tsx +5 -6
  100. package/src/components/InputBox.tsx +8 -8
  101. package/src/components/Loading.tsx +1 -1
  102. package/src/components/PageHeader.tsx +111 -0
  103. package/src/components/PersonAvatar.tsx +78 -0
  104. package/src/components/header/SiteHeader.tsx +133 -10
  105. package/src/components/header/SupportDrawer.tsx +1 -1
  106. package/src/components/index.tsx +2 -0
  107. package/src/components/notes/AddNote.tsx +105 -19
  108. package/src/components/notes/Note.tsx +43 -22
  109. package/src/components/notes/Notes.tsx +160 -21
  110. package/src/components/wrapper/ChurchList.tsx +45 -5
  111. package/src/components/wrapper/NewPrivateMessage.tsx +181 -44
  112. package/src/components/wrapper/Notifications.tsx +165 -29
  113. package/src/components/wrapper/PrivateMessageDetails.tsx +100 -13
  114. package/src/components/wrapper/PrivateMessages.tsx +539 -65
  115. package/src/components/wrapper/UserMenu.tsx +223 -38
  116. package/src/components/wrapper/index.tsx +3 -2
  117. package/src/helpers/ArrayHelper.ts +0 -1
  118. package/src/helpers/ErrorHelper.ts +1 -1
  119. package/src/helpers/EventHelper.ts +0 -3
  120. package/src/helpers/Locale.ts +7 -2
  121. package/src/helpers/NotificationService.ts +211 -0
  122. package/src/helpers/SocketHelper.ts +23 -17
  123. package/src/helpers/UserHelper.ts +2 -2
  124. package/src/helpers/index.ts +2 -0
  125. package/src/hooks/index.ts +2 -0
  126. package/src/hooks/useNotifications.ts +94 -0
  127. package/dist/components/wrapper/Drawers.d.ts +0 -5
  128. package/dist/components/wrapper/Drawers.d.ts.map +0 -1
  129. package/dist/components/wrapper/Drawers.js +0 -49
  130. package/dist/components/wrapper/Drawers.js.map +0 -1
  131. package/dist/components/wrapper/SiteWrapper.d.ts +0 -15
  132. package/dist/components/wrapper/SiteWrapper.d.ts.map +0 -1
  133. package/dist/components/wrapper/SiteWrapper.js +0 -60
  134. package/dist/components/wrapper/SiteWrapper.js.map +0 -1
  135. package/dist/components/wrapper/TabPanel.d.ts +0 -9
  136. package/dist/components/wrapper/TabPanel.d.ts.map +0 -1
  137. package/dist/components/wrapper/TabPanel.js +0 -17
  138. package/dist/components/wrapper/TabPanel.js.map +0 -1
  139. package/dist/helpers/ApiHelper.d.ts +0 -18
  140. package/dist/helpers/ApiHelper.d.ts.map +0 -1
  141. package/dist/helpers/ApiHelper.js +0 -119
  142. package/dist/helpers/ApiHelper.js.map +0 -1
  143. package/src/components/wrapper/Drawers.tsx +0 -62
  144. package/src/components/wrapper/SiteWrapper.tsx +0 -110
  145. package/src/components/wrapper/TabPanel.tsx +0 -32
  146. package/src/helpers/ApiHelper.ts +0 -127
@@ -10,6 +10,8 @@ import { Locale } from "../../helpers";
10
10
  import { PrivateMessages } from "./PrivateMessages";
11
11
  import { Notifications } from "./Notifications";
12
12
  import { useCookies, CookiesProvider } from "react-cookie";
13
+ import { NotificationService } from "../../helpers/NotificationService";
14
+ import { SocketHelper } from "../../helpers";
13
15
 
14
16
 
15
17
  interface Props {
@@ -24,15 +26,66 @@ interface Props {
24
26
  onNavigate: (url: string) => void;
25
27
  }
26
28
 
27
- const UserMenuContent: React.FC<Props> = (props) => {
29
+ // Create a persistent store for modal state that survives component re-renders
30
+ const modalStateStore = {
31
+ showPM: false,
32
+ showNotifications: false,
33
+ listeners: new Set<() => void>(),
34
+
35
+ setShowPM(value: boolean) {
36
+ this.showPM = value;
37
+ this.listeners.forEach((listener: () => void) => listener());
38
+ },
39
+
40
+ setShowNotifications(value: boolean) {
41
+ this.showNotifications = value;
42
+ this.listeners.forEach((listener: () => void) => listener());
43
+ },
44
+
45
+ subscribe(listener: () => void) {
46
+ this.listeners.add(listener);
47
+ return () => this.listeners.delete(listener);
48
+ }
49
+ };
50
+
51
+ const UserMenuContent: React.FC<Props> = React.memo((props) => {
28
52
  const userName = props.userName;
29
53
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
30
- const [showPM, setShowPM] = React.useState(false);
31
- const [showNotifications, setShowNotifications] = React.useState(false);
32
- const [refreshKey, setRefreshKey] = React.useState(0);
54
+ const [, forceUpdate] = React.useReducer(x => x + 1, 0);
55
+ const [refreshKey, setRefreshKey] = React.useState(() => Math.random());
33
56
  const [, , removeCookie] = useCookies(["lastChurchId"]);
57
+ const [directNotificationCounts, setDirectNotificationCounts] = React.useState(() => props.notificationCounts || { notificationCount: 0, pmCount: 0 });
34
58
  const open = Boolean(anchorEl);
35
59
 
60
+ // Subscribe to modal state changes
61
+ React.useEffect(() => {
62
+ return modalStateStore.subscribe(forceUpdate);
63
+ }, [forceUpdate]);
64
+
65
+ // Subscribe directly to NotificationService to update badge counts without re-renders
66
+ React.useEffect(() => {
67
+ const notificationService = NotificationService.getInstance();
68
+ const unsubscribe = notificationService.subscribe((newCounts) => {
69
+ setDirectNotificationCounts(newCounts);
70
+ });
71
+
72
+ // Initialize with current counts
73
+ if (notificationService.isReady()) {
74
+ setDirectNotificationCounts(notificationService.getCounts());
75
+ }
76
+
77
+ return unsubscribe;
78
+ }, []);
79
+
80
+ const showPM = modalStateStore.showPM;
81
+ const showNotifications = modalStateStore.showNotifications;
82
+
83
+ // Create a stable callback for onUpdate that doesn't depend on props
84
+ const stableOnUpdate = React.useCallback(() => {
85
+ // Use NotificationService directly to avoid dependency on props.loadCounts
86
+ NotificationService.getInstance().refresh();
87
+ }, []);
88
+
36
89
 
37
90
  const handleClick = (e: React.MouseEvent<HTMLElement>) => {
38
91
  e.preventDefault();
@@ -54,17 +107,25 @@ const UserMenuContent: React.FC<Props> = (props) => {
54
107
  const churchId = UserHelper.currentUserChurch.church.id;
55
108
  let result: React.ReactElement[] = [];
56
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
+ };
57
115
 
58
- result.push(<NavItem onClick={() => {setShowPM(true)}} label={Locale.label("wrapper.messages")} icon="mail" key="/messages" onNavigate={props.onNavigate} badgeCount={props.notificationCounts.pmCount} />);
116
+ result.push(<NavItem onClick={() => {modalStateStore.setShowPM(true)}} label={getLabel("wrapper.messages", "Messages")} icon="mail" key="/messages" onNavigate={props.onNavigate} badgeCount={directNotificationCounts.pmCount} />);
59
117
 
60
- result.push(<NavItem onClick={() => {setShowNotifications(true)}} label={Locale.label("wrapper.notifications")} icon="notifications" key="/notifications" onNavigate={props.onNavigate} badgeCount={props.notificationCounts.notificationCount} />);
118
+ result.push(<NavItem onClick={() => {modalStateStore.setShowNotifications(true)}} label={getLabel("wrapper.notifications", "Notifications")} icon="notifications" key="/notifications" onNavigate={props.onNavigate} badgeCount={directNotificationCounts.notificationCount} />);
61
119
 
62
- if (props.appName === "CHUMS") result.push(<NavItem url={"/profile"} key="/profile" label={Locale.label("wrapper.profile")} icon="person" onNavigate={props.onNavigate} />);
63
- else result.push(<NavItem url={`${CommonEnvironmentHelper.ChumsRoot}/login?jwt=${jwt}&churchId=${churchId}&returnUrl=/profile`} key="/profile" label={Locale.label("wrapper.profile")} icon="person" external={true} onNavigate={props.onNavigate} />);
64
- result.push(<NavItem url="/logout" label={Locale.label("wrapper.logout")} icon="logout" key="/logout" onNavigate={props.onNavigate} />);
65
- result.push(<div style={{borderTop:"1px solid #CCC", paddingTop:2, paddingBottom:2}}></div>)
66
- result.push(<NavItem label="Switch App" key={Locale.label("wrapper.switchApp")} icon="apps" onClick={() => { setTabIndex(1); }} />);
67
- result.push(<NavItem label={Locale.label("wrapper.switchChurch")} key="Switch Church" icon="church" onClick={handleSwitchChurch} />);
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} />);
68
129
  return result;
69
130
  }
70
131
 
@@ -85,7 +146,7 @@ const UserMenuContent: React.FC<Props> = (props) => {
85
146
  };
86
147
 
87
148
  const handleItemClick = (e: React.MouseEvent<HTMLDivElement>) => {
88
- console.log(e);
149
+ // Handle menu item clicks if needed
89
150
  }
90
151
 
91
152
  const [tabIndex, setTabIndex] = React.useState(0);
@@ -110,49 +171,173 @@ const UserMenuContent: React.FC<Props> = (props) => {
110
171
  );
111
172
 
112
173
  const getModals = () => {
113
- if (showPM) return (
114
- <Dialog open onClose={() => setShowPM(false)} maxWidth="md" fullWidth>
115
- <DialogTitle>{Locale.label("wrapper.messages")}</DialogTitle>
116
- <DialogContent>
117
- <PrivateMessages context={props.context} refreshKey={refreshKey} onUpdate={props.loadCounts} />
118
- </DialogContent>
119
- </Dialog>);
120
- else if (showNotifications) return (<Dialog open onClose={() => setShowNotifications(false)} maxWidth="md" fullWidth>
121
- <DialogTitle>{Locale.label("wrapper.notifications")}</DialogTitle>
122
- <DialogContent>
123
- <Notifications context={props.context} appName={props.appName} onUpdate={props.loadCounts} onNavigate={props.onNavigate} />
124
- </DialogContent>
125
- </Dialog>);
126
- else return <></>;
174
+ // Helper function to get label with fallback
175
+ const getLabel = (key: string, fallback: string) => {
176
+ const label = Locale.label(key);
177
+ return label && label !== key ? label : fallback;
178
+ };
179
+
180
+ return (
181
+ <>
182
+ <Dialog
183
+ id="private-messages-modal"
184
+ open={showPM}
185
+ onClose={() => {
186
+ modalStateStore.setShowPM(false);
187
+ }}
188
+ maxWidth="md"
189
+ fullWidth
190
+ PaperProps={{
191
+ sx: {
192
+ height: '80vh',
193
+ maxHeight: '700px',
194
+ display: 'flex',
195
+ flexDirection: 'column'
196
+ }
197
+ }}
198
+ >
199
+ <DialogTitle id="private-messages-title">{getLabel("wrapper.messages", "Messages")}</DialogTitle>
200
+ <DialogContent
201
+ sx={{
202
+ flex: 1,
203
+ display: 'flex',
204
+ flexDirection: 'column',
205
+ p: 0,
206
+ overflow: 'hidden',
207
+ minHeight: 0
208
+ }}
209
+ >
210
+ <PrivateMessages context={props.context} refreshKey={currentRefreshKey} onUpdate={stableOnUpdate} />
211
+ </DialogContent>
212
+ </Dialog>
213
+
214
+ <Dialog
215
+ id="notifications-modal"
216
+ open={showNotifications}
217
+ onClose={() => {
218
+ modalStateStore.setShowNotifications(false);
219
+ }}
220
+ maxWidth="md"
221
+ fullWidth
222
+ >
223
+ <DialogTitle id="notifications-title">{getLabel("wrapper.notifications", "Notifications")}</DialogTitle>
224
+ <DialogContent>
225
+ <Notifications context={props.context} appName={props.appName} onUpdate={props.loadCounts} onNavigate={props.onNavigate} />
226
+ </DialogContent>
227
+ </Dialog>
228
+ </>
229
+ );
127
230
  }
128
231
 
129
- const totalNotifcations = props.notificationCounts.notificationCount + props.notificationCounts.pmCount;
232
+ const totalNotifcations = directNotificationCounts.notificationCount + directNotificationCounts.pmCount;
130
233
 
234
+ // Use a ref to track if we should update refresh key
235
+ const stableRefreshKeyRef = React.useRef(refreshKey);
236
+
237
+ // Set up WebSocket handlers to update refreshKey when messages arrive
131
238
  React.useEffect(() => {
132
- console.log("THE COUNTS CHANGED")
133
- setRefreshKey(Math.random());
134
- }, [props.notificationCounts]);
239
+ if (!props.context?.person?.id) return;
240
+
241
+ const handleMessageUpdate = (data: any) => {
242
+ // Only update refreshKey if a modal is open to trigger child updates
243
+ if (modalStateStore.showPM || modalStateStore.showNotifications) {
244
+ const newKey = Math.random();
245
+ setRefreshKey(newKey);
246
+ stableRefreshKeyRef.current = newKey;
247
+ }
248
+ };
249
+
250
+ const handlePrivateMessage = (data: any) => {
251
+ // Only update refreshKey if PM modal is open
252
+ if (modalStateStore.showPM) {
253
+ const newKey = Math.random();
254
+ setRefreshKey(newKey);
255
+ stableRefreshKeyRef.current = newKey;
256
+ }
257
+ };
258
+
259
+ const handleNotification = (data: any) => {
260
+ // Update refreshKey if any modal is open to trigger child updates
261
+ if (modalStateStore.showPM || modalStateStore.showNotifications) {
262
+ const newKey = Math.random();
263
+ setRefreshKey(newKey);
264
+ stableRefreshKeyRef.current = newKey;
265
+ }
266
+ };
267
+
268
+ // Register WebSocket handlers
269
+ const messageHandlerId = `UserMenu-MessageUpdate-${props.context.person.id}`;
270
+ const privateMessageHandlerId = `UserMenu-PrivateMessage-${props.context.person.id}`;
271
+ const notificationHandlerId = `UserMenu-Notification-${props.context.person.id}`;
272
+
273
+ SocketHelper.addHandler("message", messageHandlerId, handleMessageUpdate);
274
+ SocketHelper.addHandler("privateMessage", privateMessageHandlerId, handlePrivateMessage);
275
+ SocketHelper.addHandler("notification", notificationHandlerId, handleNotification);
276
+
277
+ // Cleanup
278
+ return () => {
279
+ SocketHelper.removeHandler(messageHandlerId);
280
+ SocketHelper.removeHandler(privateMessageHandlerId);
281
+ SocketHelper.removeHandler(notificationHandlerId);
282
+ };
283
+ }, [props.context?.person?.id]); // Removed showPM, showNotifications dependencies
284
+
285
+
286
+ // Use current refresh key
287
+ const currentRefreshKey = refreshKey;
135
288
 
136
289
  return (
137
290
  <>
138
- <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>}>
139
- <Badge badgeContent={totalNotifcations} color="error" invisible={totalNotifcations===0}>
140
- <Avatar src={getProfilePic()} sx={{ width: 32, height: 32, marginRight: 1 }}></Avatar>
291
+ <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>}>
292
+ <Badge id="user-menu-notification-badge" badgeContent={totalNotifcations} color="error" invisible={totalNotifcations===0}>
293
+ <Avatar id="user-menu-avatar" src={getProfilePic()} sx={{ width: 32, height: 32, marginRight: 1 }}></Avatar>
141
294
  </Badge>
142
295
  </Button>
143
296
 
144
- <Menu anchorEl={anchorEl} id="account-menu" 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 } }}>
297
+ <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 } }}>
145
298
  {getTabs()}
146
299
  </Menu>
147
300
  {getModals()}
148
301
  </>
149
302
  );
150
- };
303
+ });
151
304
 
152
- export const UserMenu: React.FC<Props> = (props) => {
305
+ export const UserMenu: React.FC<Props> = React.memo((props) => {
153
306
  return (
154
307
  <CookiesProvider defaultSetOptions={{ path: '/' }}>
155
308
  <UserMenuContent {...props} />
156
309
  </CookiesProvider>
157
310
  );
158
- };
311
+ }, (prevProps, nextProps) => {
312
+ // Only re-render if essential props change, ignore notification count changes completely
313
+ if (prevProps.userName !== nextProps.userName) {
314
+ return false;
315
+ }
316
+
317
+ if (prevProps.profilePicture !== nextProps.profilePicture) {
318
+ return false;
319
+ }
320
+
321
+ if (prevProps.appName !== nextProps.appName) {
322
+ return false;
323
+ }
324
+
325
+ // Check if context has actually changed (deep comparison of relevant parts)
326
+ if (prevProps.context?.person?.id !== nextProps.context?.person?.id ||
327
+ prevProps.context?.userChurch?.church?.id !== nextProps.context?.userChurch?.church?.id) {
328
+ return false;
329
+ }
330
+
331
+ // Check if userChurches array changed
332
+ if (prevProps.userChurches?.length !== nextProps.userChurches?.length) {
333
+ return false;
334
+ }
335
+
336
+ // Check if loadCounts function reference changed (important for functionality)
337
+ if (prevProps.loadCounts !== nextProps.loadCounts) {
338
+ return false;
339
+ }
340
+
341
+ // Ignore both notificationCounts and onNavigate changes as they don't affect the component
342
+ return true; // Skip re-render
343
+ });
@@ -1,7 +1,8 @@
1
1
  export { NavItem } from "./NavItem";
2
2
  export { AppList } from "./AppList";
3
- export { SiteWrapper } from "./SiteWrapper";
4
3
  export { ChurchList } from "./ChurchList";
5
4
  export { UserMenu } from "./UserMenu";
6
- export {NewPrivateMessage} from "./NewPrivateMessage";
5
+ export { NewPrivateMessage } from "./NewPrivateMessage";
6
+ export { PrivateMessages } from "./PrivateMessages";
7
7
  export { PrivateMessageDetails } from "./PrivateMessageDetails";
8
+ export { Notifications } from "./Notifications";
@@ -17,7 +17,6 @@ export class ArrayHelper {
17
17
  const valB = b[propertyName];
18
18
  if (valA < valB) return descending ? 1 : -1;
19
19
  else return descending ? -1 : 1;
20
- //console.log(valA, valB, valA < valB, )
21
20
  });
22
21
  }
23
22
 
@@ -1,5 +1,5 @@
1
1
  import { ErrorLogInterface, ErrorAppDataInterface } from "@churchapps/helpers";
2
- import { ApiHelper } from "./ApiHelper";
2
+ import { ApiHelper } from "@churchapps/helpers";
3
3
 
4
4
 
5
5
  export class ErrorHelper {
@@ -21,7 +21,6 @@ export class EventHelper {
21
21
  static getFullRRule = (event:EventInterface) => {
22
22
  let rrule = RRule.fromString(event.recurrenceRule);
23
23
  rrule.options.dtstart = new Date(event.start);
24
- console.log("START", rrule.options.dtstart, event.start, rrule)
25
24
  return rrule;
26
25
  }
27
26
 
@@ -30,7 +29,6 @@ export class EventHelper {
30
29
  if (events[i].exceptionDates?.length>0)
31
30
  {
32
31
  const parsedDates = events[i].exceptionDates.map(d=>new Date(d).toISOString());
33
- //console.log("Compare", events[i].start.toISOString(), parsedDates, parsedDates.indexOf(events[i].start.toISOString()));
34
32
  if (parsedDates.indexOf(events[i].start.toISOString())>-1) events.splice(i,1);
35
33
  }
36
34
  }
@@ -39,7 +37,6 @@ export class EventHelper {
39
37
  static getPartialRRuleString = (options:ParsedOptions) => {
40
38
  const parts = new RRule(options).toString().split("RRULE:");
41
39
  const result = parts.length===2 ? parts[1] : "";
42
- console.log("getPartialRRuleString", options, new RRule(options).toString(), result);
43
40
  return result;
44
41
  }
45
42
 
@@ -103,7 +103,12 @@ export class Locale {
103
103
  }
104
104
 
105
105
  // Keep the old method for backward compatibility
106
- static label(key: string): string {
107
- return this.t(key);
106
+ static label(key: string, fallback?: string): string {
107
+ const translation = this.t(key);
108
+ // If translation equals the key, it means no translation was found
109
+ if (translation === key && fallback) {
110
+ return fallback;
111
+ }
112
+ return translation;
108
113
  }
109
114
  }
@@ -0,0 +1,211 @@
1
+ import { SocketHelper } from "./SocketHelper";
2
+ import { ApiHelper, UserContextInterface } from "@churchapps/helpers";
3
+
4
+ export interface NotificationCounts {
5
+ notificationCount: number;
6
+ pmCount: number;
7
+ }
8
+
9
+ export class NotificationService {
10
+ private static instance: NotificationService;
11
+ private counts: NotificationCounts = { notificationCount: 0, pmCount: 0 };
12
+ private listeners: Array<(counts: NotificationCounts) => void> = [];
13
+ private isInitialized: boolean = false;
14
+ private currentPersonId: string | null = null;
15
+ private loadTimeout: any | null = null;
16
+
17
+ private constructor() {}
18
+
19
+ static getInstance(): NotificationService {
20
+ if (!NotificationService.instance) {
21
+ NotificationService.instance = new NotificationService();
22
+ }
23
+ return NotificationService.instance;
24
+ }
25
+
26
+ /**
27
+ * Initialize the notification service with user context
28
+ */
29
+ async initialize(context: UserContextInterface): Promise<void> {
30
+ if (this.isInitialized) return;
31
+
32
+ try {
33
+ // Store current person ID for conversation counting
34
+ this.currentPersonId = context?.person?.id || null;
35
+
36
+ // Initialize WebSocket connection
37
+ await SocketHelper.init();
38
+
39
+ // Set person/church context for websocket
40
+ if (context?.person?.id && context?.userChurch?.church?.id) {
41
+ SocketHelper.setPersonChurch({
42
+ personId: context.person.id,
43
+ churchId: context.userChurch.church.id
44
+ });
45
+ }
46
+
47
+ // Register handlers for notification updates
48
+ this.registerWebSocketHandlers();
49
+
50
+ // Load initial notification counts
51
+ await this.loadNotificationCounts();
52
+
53
+ this.isInitialized = true;
54
+
55
+ } catch (error) {
56
+ console.error("❌ Failed to initialize NotificationService:", error);
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Register websocket handlers for real-time notification updates
63
+ */
64
+ private registerWebSocketHandlers(): void {
65
+ // Handler for new private messages
66
+ SocketHelper.addHandler("privateMessage", "NotificationService-PM", (data: any) => {
67
+ console.log('🔔 NotificationService: New private message received, updating counts');
68
+ this.debouncedLoadNotificationCounts();
69
+ });
70
+
71
+ // Handler for general notifications
72
+ SocketHelper.addHandler("notification", "NotificationService-Notification", (data: any) => {
73
+ console.log('🔔 NotificationService: New notification received, updating counts');
74
+ this.debouncedLoadNotificationCounts();
75
+ });
76
+
77
+ // Handler for message updates that could affect notification counts
78
+ SocketHelper.addHandler("message", "NotificationService-MessageUpdate", (data: any) => {
79
+ // Only update counts if the message update involves the current person
80
+ if (data?.message?.personId === this.currentPersonId ||
81
+ data?.notifyPersonId === this.currentPersonId) {
82
+ console.log('🔔 NotificationService: Message update affecting current user, updating counts');
83
+ this.debouncedLoadNotificationCounts();
84
+ }
85
+ });
86
+
87
+ // Handler for reconnect events
88
+ SocketHelper.addHandler("reconnect", "NotificationService-Reconnect", (data: any) => {
89
+ console.log('🔔 NotificationService: WebSocket reconnected, refreshing counts');
90
+ this.loadNotificationCounts(); // Don't debounce reconnect - need immediate update
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Load notification counts from the API with debouncing
96
+ */
97
+ private debouncedLoadNotificationCounts(): void {
98
+ if (this.loadTimeout) {
99
+ clearTimeout(this.loadTimeout);
100
+ }
101
+
102
+ this.loadTimeout = setTimeout(() => {
103
+ this.loadNotificationCounts();
104
+ }, 300); // 300ms debounce
105
+ }
106
+
107
+ /**
108
+ * Load notification counts from the API
109
+ */
110
+ async loadNotificationCounts(): Promise<void> {
111
+ try {
112
+ // Use the unreadCount endpoint which returns both notification and PM counts
113
+ const counts = await ApiHelper.get("/notifications/unreadCount", "MessagingApi");
114
+
115
+ const newCounts = {
116
+ notificationCount: counts?.notificationCount || 0,
117
+ pmCount: counts?.pmCount || 0
118
+ };
119
+
120
+ // Update counts and notify listeners
121
+ this.updateCounts(newCounts);
122
+
123
+ } catch (error) {
124
+ console.error("❌ Failed to load notification counts:", error);
125
+ // Don't throw - just log the error and keep existing counts
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Update counts and notify all listeners
131
+ */
132
+ private updateCounts(newCounts: NotificationCounts): void {
133
+ const countsChanged =
134
+ this.counts.notificationCount !== newCounts.notificationCount ||
135
+ this.counts.pmCount !== newCounts.pmCount;
136
+
137
+ if (countsChanged) {
138
+ this.counts = { ...newCounts };
139
+
140
+ // Notify all listeners
141
+ this.listeners.forEach(listener => {
142
+ try {
143
+ listener(this.counts);
144
+ } catch (error) {
145
+ console.error("❌ Error in notification listener:", error);
146
+ }
147
+ });
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Subscribe to notification count changes
153
+ */
154
+ subscribe(listener: (counts: NotificationCounts) => void): () => void {
155
+ this.listeners.push(listener);
156
+
157
+ // Immediately call with current counts
158
+ listener(this.counts);
159
+
160
+ // Return unsubscribe function
161
+ return () => {
162
+ this.listeners = this.listeners.filter(l => l !== listener);
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Get current notification counts
168
+ */
169
+ getCounts(): NotificationCounts {
170
+ return { ...this.counts };
171
+ }
172
+
173
+ /**
174
+ * Manually refresh notification counts
175
+ */
176
+ async refresh(): Promise<void> {
177
+ await this.loadNotificationCounts();
178
+ }
179
+
180
+ /**
181
+ * Cleanup the service
182
+ */
183
+ cleanup(): void {
184
+ // Clear any pending timeout
185
+ if (this.loadTimeout) {
186
+ clearTimeout(this.loadTimeout);
187
+ this.loadTimeout = null;
188
+ }
189
+
190
+ // Remove websocket handlers
191
+ SocketHelper.removeHandler("NotificationService-PM");
192
+ SocketHelper.removeHandler("NotificationService-Notification");
193
+ SocketHelper.removeHandler("NotificationService-MessageUpdate");
194
+ SocketHelper.removeHandler("NotificationService-Reconnect");
195
+
196
+ // Clear listeners
197
+ this.listeners = [];
198
+
199
+ // Reset state
200
+ this.counts = { notificationCount: 0, pmCount: 0 };
201
+ this.currentPersonId = null;
202
+ this.isInitialized = false;
203
+ }
204
+
205
+ /**
206
+ * Check if service is initialized
207
+ */
208
+ isReady(): boolean {
209
+ return this.isInitialized && SocketHelper.isConnected();
210
+ }
211
+ }