@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.
- package/dist/components/DisplayBox.js +1 -1
- package/dist/components/DisplayBox.js.map +1 -1
- package/dist/components/FormCardPayment.js +2 -2
- package/dist/components/FormCardPayment.js.map +1 -1
- package/dist/components/FormSubmissionEdit.d.ts.map +1 -1
- package/dist/components/FormSubmissionEdit.js +4 -5
- package/dist/components/FormSubmissionEdit.js.map +1 -1
- package/dist/components/InputBox.js +1 -1
- package/dist/components/InputBox.js.map +1 -1
- package/dist/components/Loading.js +1 -1
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/PageHeader.d.ts +15 -0
- package/dist/components/PageHeader.d.ts.map +1 -0
- package/dist/components/PageHeader.js +41 -0
- package/dist/components/PageHeader.js.map +1 -0
- package/dist/components/PersonAvatar.d.ts +12 -0
- package/dist/components/PersonAvatar.d.ts.map +1 -0
- package/dist/components/PersonAvatar.js +55 -0
- package/dist/components/PersonAvatar.js.map +1 -0
- package/dist/components/header/SiteHeader.d.ts +2 -1
- package/dist/components/header/SiteHeader.d.ts.map +1 -1
- package/dist/components/header/SiteHeader.js +100 -4
- package/dist/components/header/SiteHeader.js.map +1 -1
- package/dist/components/header/SupportDrawer.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/notes/AddNote.d.ts.map +1 -1
- package/dist/components/notes/AddNote.js +45 -7
- package/dist/components/notes/AddNote.js.map +1 -1
- package/dist/components/notes/Note.d.ts.map +1 -1
- package/dist/components/notes/Note.js +6 -6
- package/dist/components/notes/Note.js.map +1 -1
- package/dist/components/notes/Notes.d.ts.map +1 -1
- package/dist/components/notes/Notes.js +120 -20
- package/dist/components/notes/Notes.js.map +1 -1
- package/dist/components/wrapper/ChurchList.d.ts.map +1 -1
- package/dist/components/wrapper/ChurchList.js +44 -6
- package/dist/components/wrapper/ChurchList.js.map +1 -1
- package/dist/components/wrapper/NewPrivateMessage.d.ts.map +1 -1
- package/dist/components/wrapper/NewPrivateMessage.js +28 -21
- package/dist/components/wrapper/NewPrivateMessage.js.map +1 -1
- package/dist/components/wrapper/Notifications.d.ts.map +1 -1
- package/dist/components/wrapper/Notifications.js +47 -20
- package/dist/components/wrapper/Notifications.js.map +1 -1
- package/dist/components/wrapper/PrivateMessageDetails.d.ts +1 -0
- package/dist/components/wrapper/PrivateMessageDetails.d.ts.map +1 -1
- package/dist/components/wrapper/PrivateMessageDetails.js +53 -4
- package/dist/components/wrapper/PrivateMessageDetails.js.map +1 -1
- package/dist/components/wrapper/PrivateMessages.d.ts.map +1 -1
- package/dist/components/wrapper/PrivateMessages.js +360 -41
- package/dist/components/wrapper/PrivateMessages.js.map +1 -1
- package/dist/components/wrapper/UserMenu.d.ts.map +1 -1
- package/dist/components/wrapper/UserMenu.js +164 -27
- package/dist/components/wrapper/UserMenu.js.map +1 -1
- package/dist/components/wrapper/index.d.ts +2 -1
- package/dist/components/wrapper/index.d.ts.map +1 -1
- package/dist/components/wrapper/index.js +2 -1
- package/dist/components/wrapper/index.js.map +1 -1
- package/dist/helpers/ArrayHelper.d.ts.map +1 -1
- package/dist/helpers/ArrayHelper.js +0 -1
- package/dist/helpers/ArrayHelper.js.map +1 -1
- package/dist/helpers/ErrorHelper.js +1 -1
- package/dist/helpers/ErrorHelper.js.map +1 -1
- package/dist/helpers/EventHelper.d.ts.map +1 -1
- package/dist/helpers/EventHelper.js +0 -3
- package/dist/helpers/EventHelper.js.map +1 -1
- package/dist/helpers/Locale.d.ts +1 -1
- package/dist/helpers/Locale.d.ts.map +1 -1
- package/dist/helpers/Locale.js +7 -2
- package/dist/helpers/Locale.js.map +1 -1
- package/dist/helpers/NotificationService.d.ts +56 -0
- package/dist/helpers/NotificationService.d.ts.map +1 -0
- package/dist/helpers/NotificationService.js +176 -0
- package/dist/helpers/NotificationService.js.map +1 -0
- package/dist/helpers/SocketHelper.d.ts.map +1 -1
- package/dist/helpers/SocketHelper.js +22 -17
- package/dist/helpers/SocketHelper.js.map +1 -1
- package/dist/helpers/UserHelper.js +2 -2
- package/dist/helpers/UserHelper.js.map +1 -1
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/index.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useNotifications.d.ts +30 -0
- package/dist/hooks/useNotifications.d.ts.map +1 -0
- package/dist/hooks/useNotifications.js +79 -0
- package/dist/hooks/useNotifications.js.map +1 -0
- package/dist/public/css/styles.css +6 -2
- package/package.json +1 -1
- package/public/css/styles.css +6 -2
- package/src/components/DisplayBox.tsx +8 -8
- package/src/components/FormCardPayment.tsx +2 -2
- package/src/components/FormSubmissionEdit.tsx +5 -6
- package/src/components/InputBox.tsx +8 -8
- package/src/components/Loading.tsx +1 -1
- package/src/components/PageHeader.tsx +111 -0
- package/src/components/PersonAvatar.tsx +78 -0
- package/src/components/header/SiteHeader.tsx +133 -10
- package/src/components/header/SupportDrawer.tsx +1 -1
- package/src/components/index.tsx +2 -0
- package/src/components/notes/AddNote.tsx +105 -19
- package/src/components/notes/Note.tsx +43 -22
- package/src/components/notes/Notes.tsx +160 -21
- package/src/components/wrapper/ChurchList.tsx +45 -5
- package/src/components/wrapper/NewPrivateMessage.tsx +181 -44
- package/src/components/wrapper/Notifications.tsx +165 -29
- package/src/components/wrapper/PrivateMessageDetails.tsx +100 -13
- package/src/components/wrapper/PrivateMessages.tsx +539 -65
- package/src/components/wrapper/UserMenu.tsx +223 -38
- package/src/components/wrapper/index.tsx +3 -2
- package/src/helpers/ArrayHelper.ts +0 -1
- package/src/helpers/ErrorHelper.ts +1 -1
- package/src/helpers/EventHelper.ts +0 -3
- package/src/helpers/Locale.ts +7 -2
- package/src/helpers/NotificationService.ts +211 -0
- package/src/helpers/SocketHelper.ts +23 -17
- package/src/helpers/UserHelper.ts +2 -2
- package/src/helpers/index.ts +2 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useNotifications.ts +94 -0
- package/dist/components/wrapper/Drawers.d.ts +0 -5
- package/dist/components/wrapper/Drawers.d.ts.map +0 -1
- package/dist/components/wrapper/Drawers.js +0 -49
- package/dist/components/wrapper/Drawers.js.map +0 -1
- package/dist/components/wrapper/SiteWrapper.d.ts +0 -15
- package/dist/components/wrapper/SiteWrapper.d.ts.map +0 -1
- package/dist/components/wrapper/SiteWrapper.js +0 -60
- package/dist/components/wrapper/SiteWrapper.js.map +0 -1
- package/dist/components/wrapper/TabPanel.d.ts +0 -9
- package/dist/components/wrapper/TabPanel.d.ts.map +0 -1
- package/dist/components/wrapper/TabPanel.js +0 -17
- package/dist/components/wrapper/TabPanel.js.map +0 -1
- package/dist/helpers/ApiHelper.d.ts +0 -18
- package/dist/helpers/ApiHelper.d.ts.map +0 -1
- package/dist/helpers/ApiHelper.js +0 -119
- package/dist/helpers/ApiHelper.js.map +0 -1
- package/src/components/wrapper/Drawers.tsx +0 -62
- package/src/components/wrapper/SiteWrapper.tsx +0 -110
- package/src/components/wrapper/TabPanel.tsx +0 -32
- 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
|
-
|
|
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 [
|
|
31
|
-
const [
|
|
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={
|
|
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={
|
|
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={
|
|
63
|
-
else result.push(<NavItem url={`${CommonEnvironmentHelper.ChumsRoot}/login?jwt=${jwt}&churchId=${churchId}&returnUrl=/profile`} key="/profile" label={
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
result.push(<NavItem label={
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 =
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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="
|
|
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";
|
|
@@ -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
|
|
package/src/helpers/Locale.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|