@churchapps/apphelper 0.4.17 → 0.4.18

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 (137) hide show
  1. package/dist/components/FormCardPayment.js +2 -2
  2. package/dist/components/FormCardPayment.js.map +1 -1
  3. package/dist/components/FormSubmissionEdit.d.ts.map +1 -1
  4. package/dist/components/FormSubmissionEdit.js +4 -5
  5. package/dist/components/FormSubmissionEdit.js.map +1 -1
  6. package/dist/components/PageHeader.d.ts +15 -0
  7. package/dist/components/PageHeader.d.ts.map +1 -0
  8. package/dist/components/PageHeader.js +41 -0
  9. package/dist/components/PageHeader.js.map +1 -0
  10. package/dist/components/PersonAvatar.d.ts +12 -0
  11. package/dist/components/PersonAvatar.d.ts.map +1 -0
  12. package/dist/components/PersonAvatar.js +55 -0
  13. package/dist/components/PersonAvatar.js.map +1 -0
  14. package/dist/components/header/SiteHeader.d.ts +2 -1
  15. package/dist/components/header/SiteHeader.d.ts.map +1 -1
  16. package/dist/components/header/SiteHeader.js +100 -4
  17. package/dist/components/header/SiteHeader.js.map +1 -1
  18. package/dist/components/header/SupportDrawer.js.map +1 -1
  19. package/dist/components/index.d.ts +2 -0
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/components/index.js +2 -0
  22. package/dist/components/index.js.map +1 -1
  23. package/dist/components/notes/AddNote.d.ts.map +1 -1
  24. package/dist/components/notes/AddNote.js +45 -7
  25. package/dist/components/notes/AddNote.js.map +1 -1
  26. package/dist/components/notes/Note.d.ts.map +1 -1
  27. package/dist/components/notes/Note.js +6 -6
  28. package/dist/components/notes/Note.js.map +1 -1
  29. package/dist/components/notes/Notes.d.ts.map +1 -1
  30. package/dist/components/notes/Notes.js +120 -20
  31. package/dist/components/notes/Notes.js.map +1 -1
  32. package/dist/components/wrapper/ChurchList.d.ts.map +1 -1
  33. package/dist/components/wrapper/ChurchList.js +44 -6
  34. package/dist/components/wrapper/ChurchList.js.map +1 -1
  35. package/dist/components/wrapper/NewPrivateMessage.d.ts.map +1 -1
  36. package/dist/components/wrapper/NewPrivateMessage.js +28 -21
  37. package/dist/components/wrapper/NewPrivateMessage.js.map +1 -1
  38. package/dist/components/wrapper/Notifications.d.ts.map +1 -1
  39. package/dist/components/wrapper/Notifications.js +47 -20
  40. package/dist/components/wrapper/Notifications.js.map +1 -1
  41. package/dist/components/wrapper/PrivateMessageDetails.d.ts +1 -0
  42. package/dist/components/wrapper/PrivateMessageDetails.d.ts.map +1 -1
  43. package/dist/components/wrapper/PrivateMessageDetails.js +53 -4
  44. package/dist/components/wrapper/PrivateMessageDetails.js.map +1 -1
  45. package/dist/components/wrapper/PrivateMessages.d.ts.map +1 -1
  46. package/dist/components/wrapper/PrivateMessages.js +360 -41
  47. package/dist/components/wrapper/PrivateMessages.js.map +1 -1
  48. package/dist/components/wrapper/UserMenu.d.ts.map +1 -1
  49. package/dist/components/wrapper/UserMenu.js +163 -26
  50. package/dist/components/wrapper/UserMenu.js.map +1 -1
  51. package/dist/components/wrapper/index.d.ts +2 -1
  52. package/dist/components/wrapper/index.d.ts.map +1 -1
  53. package/dist/components/wrapper/index.js +2 -1
  54. package/dist/components/wrapper/index.js.map +1 -1
  55. package/dist/helpers/ArrayHelper.d.ts.map +1 -1
  56. package/dist/helpers/ArrayHelper.js +0 -1
  57. package/dist/helpers/ArrayHelper.js.map +1 -1
  58. package/dist/helpers/ErrorHelper.js +1 -1
  59. package/dist/helpers/ErrorHelper.js.map +1 -1
  60. package/dist/helpers/EventHelper.d.ts.map +1 -1
  61. package/dist/helpers/EventHelper.js +0 -3
  62. package/dist/helpers/EventHelper.js.map +1 -1
  63. package/dist/helpers/Locale.d.ts +1 -1
  64. package/dist/helpers/Locale.d.ts.map +1 -1
  65. package/dist/helpers/Locale.js +7 -2
  66. package/dist/helpers/Locale.js.map +1 -1
  67. package/dist/helpers/NotificationService.d.ts +56 -0
  68. package/dist/helpers/NotificationService.d.ts.map +1 -0
  69. package/dist/helpers/NotificationService.js +176 -0
  70. package/dist/helpers/NotificationService.js.map +1 -0
  71. package/dist/helpers/SocketHelper.d.ts.map +1 -1
  72. package/dist/helpers/SocketHelper.js +22 -17
  73. package/dist/helpers/SocketHelper.js.map +1 -1
  74. package/dist/helpers/UserHelper.js +2 -2
  75. package/dist/helpers/UserHelper.js.map +1 -1
  76. package/dist/helpers/index.d.ts +2 -0
  77. package/dist/helpers/index.d.ts.map +1 -1
  78. package/dist/helpers/index.js +1 -0
  79. package/dist/helpers/index.js.map +1 -1
  80. package/dist/hooks/index.d.ts +2 -0
  81. package/dist/hooks/index.d.ts.map +1 -1
  82. package/dist/hooks/index.js +1 -0
  83. package/dist/hooks/index.js.map +1 -1
  84. package/dist/hooks/useNotifications.d.ts +30 -0
  85. package/dist/hooks/useNotifications.d.ts.map +1 -0
  86. package/dist/hooks/useNotifications.js +79 -0
  87. package/dist/hooks/useNotifications.js.map +1 -0
  88. package/dist/public/css/styles.css +6 -2
  89. package/package.json +1 -1
  90. package/public/css/styles.css +6 -2
  91. package/src/components/FormCardPayment.tsx +2 -2
  92. package/src/components/FormSubmissionEdit.tsx +5 -6
  93. package/src/components/PageHeader.tsx +107 -0
  94. package/src/components/PersonAvatar.tsx +78 -0
  95. package/src/components/header/SiteHeader.tsx +131 -8
  96. package/src/components/header/SupportDrawer.tsx +1 -1
  97. package/src/components/index.tsx +2 -0
  98. package/src/components/notes/AddNote.tsx +105 -19
  99. package/src/components/notes/Note.tsx +43 -22
  100. package/src/components/notes/Notes.tsx +160 -21
  101. package/src/components/wrapper/ChurchList.tsx +45 -5
  102. package/src/components/wrapper/NewPrivateMessage.tsx +181 -44
  103. package/src/components/wrapper/Notifications.tsx +164 -29
  104. package/src/components/wrapper/PrivateMessageDetails.tsx +100 -13
  105. package/src/components/wrapper/PrivateMessages.tsx +539 -65
  106. package/src/components/wrapper/UserMenu.tsx +217 -34
  107. package/src/components/wrapper/index.tsx +3 -2
  108. package/src/helpers/ArrayHelper.ts +0 -1
  109. package/src/helpers/ErrorHelper.ts +1 -1
  110. package/src/helpers/EventHelper.ts +0 -3
  111. package/src/helpers/Locale.ts +7 -2
  112. package/src/helpers/NotificationService.ts +211 -0
  113. package/src/helpers/SocketHelper.ts +23 -17
  114. package/src/helpers/UserHelper.ts +2 -2
  115. package/src/helpers/index.ts +2 -0
  116. package/src/hooks/index.ts +2 -0
  117. package/src/hooks/useNotifications.ts +94 -0
  118. package/dist/components/wrapper/Drawers.d.ts +0 -5
  119. package/dist/components/wrapper/Drawers.d.ts.map +0 -1
  120. package/dist/components/wrapper/Drawers.js +0 -49
  121. package/dist/components/wrapper/Drawers.js.map +0 -1
  122. package/dist/components/wrapper/SiteWrapper.d.ts +0 -15
  123. package/dist/components/wrapper/SiteWrapper.d.ts.map +0 -1
  124. package/dist/components/wrapper/SiteWrapper.js +0 -60
  125. package/dist/components/wrapper/SiteWrapper.js.map +0 -1
  126. package/dist/components/wrapper/TabPanel.d.ts +0 -9
  127. package/dist/components/wrapper/TabPanel.d.ts.map +0 -1
  128. package/dist/components/wrapper/TabPanel.js +0 -17
  129. package/dist/components/wrapper/TabPanel.js.map +0 -1
  130. package/dist/helpers/ApiHelper.d.ts +0 -18
  131. package/dist/helpers/ApiHelper.d.ts.map +0 -1
  132. package/dist/helpers/ApiHelper.js +0 -119
  133. package/dist/helpers/ApiHelper.js.map +0 -1
  134. package/src/components/wrapper/Drawers.tsx +0 -62
  135. package/src/components/wrapper/SiteWrapper.tsx +0 -110
  136. package/src/components/wrapper/TabPanel.tsx +0 -32
  137. 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,28 +171,118 @@ 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
+ open={showPM}
184
+ onClose={() => {
185
+ modalStateStore.setShowPM(false);
186
+ }}
187
+ maxWidth="md"
188
+ fullWidth
189
+ PaperProps={{
190
+ sx: {
191
+ height: '80vh',
192
+ maxHeight: '700px',
193
+ display: 'flex',
194
+ flexDirection: 'column'
195
+ }
196
+ }}
197
+ >
198
+ <DialogTitle>{getLabel("wrapper.messages", "Messages")}</DialogTitle>
199
+ <DialogContent
200
+ sx={{
201
+ flex: 1,
202
+ display: 'flex',
203
+ flexDirection: 'column',
204
+ p: 0,
205
+ overflow: 'hidden',
206
+ minHeight: 0
207
+ }}
208
+ >
209
+ <PrivateMessages context={props.context} refreshKey={currentRefreshKey} onUpdate={stableOnUpdate} />
210
+ </DialogContent>
211
+ </Dialog>
212
+
213
+ <Dialog
214
+ open={showNotifications}
215
+ onClose={() => {
216
+ modalStateStore.setShowNotifications(false);
217
+ }}
218
+ maxWidth="md"
219
+ fullWidth
220
+ >
221
+ <DialogTitle>{getLabel("wrapper.notifications", "Notifications")}</DialogTitle>
222
+ <DialogContent>
223
+ <Notifications context={props.context} appName={props.appName} onUpdate={props.loadCounts} onNavigate={props.onNavigate} />
224
+ </DialogContent>
225
+ </Dialog>
226
+ </>
227
+ );
127
228
  }
128
229
 
129
- const totalNotifcations = props.notificationCounts.notificationCount + props.notificationCounts.pmCount;
230
+ const totalNotifcations = directNotificationCounts.notificationCount + directNotificationCounts.pmCount;
130
231
 
232
+ // Use a ref to track if we should update refresh key
233
+ const stableRefreshKeyRef = React.useRef(refreshKey);
234
+
235
+ // Set up WebSocket handlers to update refreshKey when messages arrive
131
236
  React.useEffect(() => {
132
- console.log("THE COUNTS CHANGED")
133
- setRefreshKey(Math.random());
134
- }, [props.notificationCounts]);
237
+ if (!props.context?.person?.id) return;
238
+
239
+ const handleMessageUpdate = (data: any) => {
240
+ // Only update refreshKey if a modal is open to trigger child updates
241
+ if (modalStateStore.showPM || modalStateStore.showNotifications) {
242
+ const newKey = Math.random();
243
+ setRefreshKey(newKey);
244
+ stableRefreshKeyRef.current = newKey;
245
+ }
246
+ };
247
+
248
+ const handlePrivateMessage = (data: any) => {
249
+ // Only update refreshKey if PM modal is open
250
+ if (modalStateStore.showPM) {
251
+ const newKey = Math.random();
252
+ setRefreshKey(newKey);
253
+ stableRefreshKeyRef.current = newKey;
254
+ }
255
+ };
256
+
257
+ const handleNotification = (data: any) => {
258
+ // Update refreshKey if any modal is open to trigger child updates
259
+ if (modalStateStore.showPM || modalStateStore.showNotifications) {
260
+ const newKey = Math.random();
261
+ setRefreshKey(newKey);
262
+ stableRefreshKeyRef.current = newKey;
263
+ }
264
+ };
265
+
266
+ // Register WebSocket handlers
267
+ const messageHandlerId = `UserMenu-MessageUpdate-${props.context.person.id}`;
268
+ const privateMessageHandlerId = `UserMenu-PrivateMessage-${props.context.person.id}`;
269
+ const notificationHandlerId = `UserMenu-Notification-${props.context.person.id}`;
270
+
271
+ SocketHelper.addHandler("message", messageHandlerId, handleMessageUpdate);
272
+ SocketHelper.addHandler("privateMessage", privateMessageHandlerId, handlePrivateMessage);
273
+ SocketHelper.addHandler("notification", notificationHandlerId, handleNotification);
274
+
275
+ // Cleanup
276
+ return () => {
277
+ SocketHelper.removeHandler(messageHandlerId);
278
+ SocketHelper.removeHandler(privateMessageHandlerId);
279
+ SocketHelper.removeHandler(notificationHandlerId);
280
+ };
281
+ }, [props.context?.person?.id]); // Removed showPM, showNotifications dependencies
282
+
283
+
284
+ // Use current refresh key
285
+ const currentRefreshKey = refreshKey;
135
286
 
136
287
  return (
137
288
  <>
@@ -147,12 +298,44 @@ const UserMenuContent: React.FC<Props> = (props) => {
147
298
  {getModals()}
148
299
  </>
149
300
  );
150
- };
301
+ });
151
302
 
152
- export const UserMenu: React.FC<Props> = (props) => {
303
+ export const UserMenu: React.FC<Props> = React.memo((props) => {
153
304
  return (
154
305
  <CookiesProvider defaultSetOptions={{ path: '/' }}>
155
306
  <UserMenuContent {...props} />
156
307
  </CookiesProvider>
157
308
  );
158
- };
309
+ }, (prevProps, nextProps) => {
310
+ // Only re-render if essential props change, ignore notification count changes completely
311
+ if (prevProps.userName !== nextProps.userName) {
312
+ return false;
313
+ }
314
+
315
+ if (prevProps.profilePicture !== nextProps.profilePicture) {
316
+ return false;
317
+ }
318
+
319
+ if (prevProps.appName !== nextProps.appName) {
320
+ return false;
321
+ }
322
+
323
+ // Check if context has actually changed (deep comparison of relevant parts)
324
+ if (prevProps.context?.person?.id !== nextProps.context?.person?.id ||
325
+ prevProps.context?.userChurch?.church?.id !== nextProps.context?.userChurch?.church?.id) {
326
+ return false;
327
+ }
328
+
329
+ // Check if userChurches array changed
330
+ if (prevProps.userChurches?.length !== nextProps.userChurches?.length) {
331
+ return false;
332
+ }
333
+
334
+ // Check if loadCounts function reference changed (important for functionality)
335
+ if (prevProps.loadCounts !== nextProps.loadCounts) {
336
+ return false;
337
+ }
338
+
339
+ // Ignore both notificationCounts and onNavigate changes as they don't affect the component
340
+ return true; // Skip re-render
341
+ });
@@ -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
+ }