@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.
- 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/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 +163 -26
- 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/FormCardPayment.tsx +2 -2
- package/src/components/FormSubmissionEdit.tsx +5 -6
- package/src/components/PageHeader.tsx +107 -0
- package/src/components/PersonAvatar.tsx +78 -0
- package/src/components/header/SiteHeader.tsx +131 -8
- 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 +164 -29
- package/src/components/wrapper/PrivateMessageDetails.tsx +100 -13
- package/src/components/wrapper/PrivateMessages.tsx +539 -65
- package/src/components/wrapper/UserMenu.tsx +217 -34
- 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
|
@@ -19,18 +19,72 @@ export function Notes(props: Props) {
|
|
|
19
19
|
|
|
20
20
|
const [messages, setMessages] = React.useState<MessageInterface[]>(null)
|
|
21
21
|
const [editMessageId, setEditMessageId] = React.useState(null)
|
|
22
|
+
const [isInitialLoad, setIsInitialLoad] = React.useState(true)
|
|
23
|
+
const [previousMessageCount, setPreviousMessageCount] = React.useState(0)
|
|
24
|
+
|
|
25
|
+
// Add CSS for custom scrollbar styling
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
const styleId = 'notes-scrollbar-styles';
|
|
28
|
+
if (!document.getElementById(styleId)) {
|
|
29
|
+
const style = document.createElement('style');
|
|
30
|
+
style.id = styleId;
|
|
31
|
+
style.textContent = `
|
|
32
|
+
.notes-scroll-container {
|
|
33
|
+
scrollbar-width: thin;
|
|
34
|
+
scrollbar-color: rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.1);
|
|
35
|
+
}
|
|
36
|
+
.notes-scroll-container::-webkit-scrollbar {
|
|
37
|
+
width: 12px;
|
|
38
|
+
background: transparent;
|
|
39
|
+
}
|
|
40
|
+
.notes-scroll-container::-webkit-scrollbar-track {
|
|
41
|
+
background: rgba(0, 0, 0, 0.1);
|
|
42
|
+
border-radius: 6px;
|
|
43
|
+
margin: 4px;
|
|
44
|
+
}
|
|
45
|
+
.notes-scroll-container::-webkit-scrollbar-thumb {
|
|
46
|
+
background: rgba(0, 0, 0, 0.3);
|
|
47
|
+
border-radius: 6px;
|
|
48
|
+
border: 2px solid transparent;
|
|
49
|
+
background-clip: content-box;
|
|
50
|
+
}
|
|
51
|
+
.notes-scroll-container::-webkit-scrollbar-thumb:hover {
|
|
52
|
+
background: rgba(0, 0, 0, 0.5);
|
|
53
|
+
background-clip: content-box;
|
|
54
|
+
}
|
|
55
|
+
.notes-scroll-container::-webkit-scrollbar-corner {
|
|
56
|
+
background: transparent;
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
document.head.appendChild(style);
|
|
60
|
+
}
|
|
61
|
+
}, []);
|
|
22
62
|
|
|
23
63
|
const loadNotes = async () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
64
|
+
try {
|
|
65
|
+
const messages: MessageInterface[] = (props.conversationId) ? await ApiHelper.get("/messages/conversation/" + props.conversationId, "MessagingApi") : [];
|
|
66
|
+
if (messages.length > 0) {
|
|
67
|
+
const peopleIds = ArrayHelper.getIds(messages, "personId");
|
|
68
|
+
const people = await ApiHelper.get("/people/basic?ids=" + peopleIds.join(","), "MembershipApi");
|
|
69
|
+
messages.forEach(n => {
|
|
70
|
+
n.person = ArrayHelper.getOne(people, "id", n.personId);
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
setMessages(messages);
|
|
74
|
+
setEditMessageId(null);
|
|
75
|
+
|
|
76
|
+
// Mark as no longer initial load after first load
|
|
77
|
+
if (isInitialLoad) {
|
|
78
|
+
setIsInitialLoad(false);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("❌ Failed to load messages for conversation:", props.conversationId, error);
|
|
82
|
+
// Don't clear messages on error - keep showing existing messages
|
|
83
|
+
// Only set isInitialLoad to false if this was the first load attempt
|
|
84
|
+
if (isInitialLoad) {
|
|
85
|
+
setIsInitialLoad(false);
|
|
86
|
+
}
|
|
31
87
|
}
|
|
32
|
-
setMessages(messages);
|
|
33
|
-
setEditMessageId(null);
|
|
34
88
|
};
|
|
35
89
|
|
|
36
90
|
const getNotes = () => {
|
|
@@ -45,25 +99,110 @@ export function Notes(props: Props) {
|
|
|
45
99
|
|
|
46
100
|
const getNotesWrapper = () => {
|
|
47
101
|
const notes = getNotes();
|
|
48
|
-
if (props.maxHeight)
|
|
102
|
+
if (props.maxHeight) {
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
id="notesScroll"
|
|
106
|
+
style={{
|
|
107
|
+
flex: 1,
|
|
108
|
+
minHeight: 0,
|
|
109
|
+
overflowY: "auto",
|
|
110
|
+
overflowX: "hidden",
|
|
111
|
+
padding: "8px 12px",
|
|
112
|
+
scrollBehavior: "smooth",
|
|
113
|
+
height: "100%"
|
|
114
|
+
}}
|
|
115
|
+
className="notes-scroll-container"
|
|
116
|
+
data-testid="message-scroll-area"
|
|
117
|
+
>
|
|
118
|
+
<div style={{
|
|
119
|
+
display: "flex",
|
|
120
|
+
flexDirection: "column",
|
|
121
|
+
gap: "8px",
|
|
122
|
+
minHeight: "min-content"
|
|
123
|
+
}}>
|
|
124
|
+
{notes}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
49
129
|
else return notes;
|
|
50
130
|
}
|
|
51
131
|
|
|
52
132
|
React.useEffect(() => { loadNotes() }, [props.conversationId, props.refreshKey]); //eslint-disable-line
|
|
53
133
|
|
|
134
|
+
// Simply reload notes when refreshKey changes
|
|
135
|
+
// This is triggered by the parent component when WebSocket messages arrive
|
|
136
|
+
|
|
137
|
+
// Auto-scroll to bottom only when new messages are added (not on initial load)
|
|
54
138
|
React.useEffect(() => {
|
|
55
|
-
if (props.maxHeight && messages?.length>0) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
139
|
+
if (props.maxHeight && messages?.length > 0 && !isInitialLoad) {
|
|
140
|
+
const currentMessageCount = messages.length;
|
|
141
|
+
|
|
142
|
+
// Only auto-scroll if messages were added
|
|
143
|
+
if (currentMessageCount > previousMessageCount) {
|
|
144
|
+
// Use requestAnimationFrame for smoother scrolling
|
|
145
|
+
requestAnimationFrame(() => {
|
|
146
|
+
const element = window?.document?.getElementById("notesScroll");
|
|
147
|
+
if (element) {
|
|
148
|
+
element.scrollTop = element.scrollHeight;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setPreviousMessageCount(currentMessageCount);
|
|
154
|
+
} else if (messages?.length > 0 && isInitialLoad) {
|
|
155
|
+
// On initial load, just set the previous count without scrolling
|
|
156
|
+
setPreviousMessageCount(messages.length);
|
|
60
157
|
}
|
|
61
|
-
}, [messages, props.maxHeight]);
|
|
158
|
+
}, [messages, props.maxHeight, isInitialLoad, previousMessageCount]);
|
|
62
159
|
|
|
63
|
-
let result =
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
160
|
+
let result = props.maxHeight ? (
|
|
161
|
+
<div style={{
|
|
162
|
+
height: "100%",
|
|
163
|
+
display: "flex",
|
|
164
|
+
flexDirection: "column",
|
|
165
|
+
overflow: "hidden",
|
|
166
|
+
minHeight: 0
|
|
167
|
+
}}>
|
|
168
|
+
{/* Messages area - scrollable */}
|
|
169
|
+
<div style={{
|
|
170
|
+
flex: 1,
|
|
171
|
+
minHeight: 0,
|
|
172
|
+
display: "flex",
|
|
173
|
+
flexDirection: "column",
|
|
174
|
+
overflow: "hidden"
|
|
175
|
+
}}>
|
|
176
|
+
{getNotesWrapper()}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Input area - always visible at bottom */}
|
|
180
|
+
{messages && (
|
|
181
|
+
<div style={{
|
|
182
|
+
flexShrink: 0,
|
|
183
|
+
borderTop: "1px solid #e0e0e0",
|
|
184
|
+
backgroundColor: "#fafafa",
|
|
185
|
+
padding: "12px",
|
|
186
|
+
minHeight: "auto",
|
|
187
|
+
maxHeight: "200px"
|
|
188
|
+
}}>
|
|
189
|
+
<AddNote
|
|
190
|
+
context={props.context}
|
|
191
|
+
conversationId={props.conversationId}
|
|
192
|
+
onUpdate={loadNotes}
|
|
193
|
+
createConversation={props.createConversation}
|
|
194
|
+
messageId={editMessageId}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<>
|
|
201
|
+
{getNotesWrapper()}
|
|
202
|
+
{messages && (<AddNote context={props.context} conversationId={props.conversationId} onUpdate={loadNotes} createConversation={props.createConversation} messageId={editMessageId} />)}
|
|
203
|
+
</>
|
|
204
|
+
);
|
|
205
|
+
|
|
67
206
|
if (props.noDisplayBox) return result;
|
|
68
|
-
else return (<DisplayBox id="notesBox" data-testid="notes-box" headerIcon="sticky_note_2" headerText={Locale.label("notes.notes")}>{result}</DisplayBox>);
|
|
207
|
+
else return (<DisplayBox id="notesBox" data-testid="notes-box" headerIcon="sticky_note_2" headerText={Locale.label("notes.notes", "Notes")}>{result}</DisplayBox>);
|
|
69
208
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState } from "react";
|
|
4
4
|
import { LoginUserChurchInterface, UserContextInterface, ArrayHelper } from "@churchapps/helpers";
|
|
5
|
-
import { ApiHelper } from "
|
|
5
|
+
import { ApiHelper } from "@churchapps/helpers";
|
|
6
6
|
import { UserHelper } from "../../helpers/UserHelper";
|
|
7
7
|
import { NavItem } from "./NavItem";
|
|
8
8
|
import { Locale } from "../../helpers";
|
|
@@ -10,11 +10,45 @@ import { Locale } from "../../helpers";
|
|
|
10
10
|
export interface Props { userChurches: LoginUserChurchInterface[], currentUserChurch: LoginUserChurchInterface, context: UserContextInterface, onDelete?: () => void }
|
|
11
11
|
|
|
12
12
|
export const ChurchList: React.FC<Props> = props => {
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
const [userChurches, setUserChurches] = useState(() => {
|
|
15
|
+
try {
|
|
16
|
+
// Handle both array and single object cases
|
|
17
|
+
let churches = props.userChurches;
|
|
18
|
+
if (!Array.isArray(churches)) {
|
|
19
|
+
churches = churches ? [churches] : [];
|
|
20
|
+
}
|
|
21
|
+
return churches.filter(uc => uc && uc.apis && uc.apis.length > 0);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error filtering userChurches:', error);
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Update local state when props change
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
try {
|
|
31
|
+
// Handle both array and single object cases
|
|
32
|
+
let churches = props.userChurches;
|
|
33
|
+
if (!Array.isArray(churches)) {
|
|
34
|
+
churches = churches ? [churches] : [];
|
|
35
|
+
}
|
|
36
|
+
setUserChurches(churches.filter(uc => uc && uc.apis && uc.apis.length > 0));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error updating userChurches:', error);
|
|
39
|
+
setUserChurches([]);
|
|
40
|
+
}
|
|
41
|
+
}, [props.userChurches]);
|
|
14
42
|
|
|
15
43
|
const handleDelete = async (uc: LoginUserChurchInterface) => {
|
|
16
|
-
|
|
17
|
-
|
|
44
|
+
// Helper function to get label with fallback
|
|
45
|
+
const getLabel = (key: string, fallback: string) => {
|
|
46
|
+
const label = Locale.label(key);
|
|
47
|
+
return label && label !== key ? label : fallback;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const confirmMessage = getLabel("wrapper.sureRemoveChurch", "Are you sure you wish to delete this church? You will no longer be a member of {}.").replace("{}", uc.church.name?.toUpperCase());
|
|
51
|
+
if (window.confirm(confirmMessage)) {
|
|
18
52
|
await ApiHelper.delete(`/userchurch/record/${props.context.user.id}/${uc.church.id}/${uc.person.id}`, "MembershipApi");
|
|
19
53
|
await ApiHelper.delete(`/rolemembers/self/${uc.church.id}/${props.context.user.id}`, "MembershipApi");
|
|
20
54
|
// remove the same from userChurches
|
|
@@ -24,6 +58,12 @@ export const ChurchList: React.FC<Props> = props => {
|
|
|
24
58
|
}
|
|
25
59
|
}
|
|
26
60
|
|
|
61
|
+
// Helper function to get label with fallback
|
|
62
|
+
const getLabel = (key: string, fallback: string) => {
|
|
63
|
+
const label = Locale.label(key);
|
|
64
|
+
return label && label !== key ? label : fallback;
|
|
65
|
+
};
|
|
66
|
+
|
|
27
67
|
let result: React.ReactElement[] = [];
|
|
28
68
|
userChurches.forEach(uc => {
|
|
29
69
|
const userChurch = uc;
|
|
@@ -35,7 +75,7 @@ export const ChurchList: React.FC<Props> = props => {
|
|
|
35
75
|
label={churchName || "Unknown"}
|
|
36
76
|
icon="church"
|
|
37
77
|
deleteIcon={uc.church.id !== props.currentUserChurch.church.id ? "delete" : null}
|
|
38
|
-
deleteLabel={
|
|
78
|
+
deleteLabel={getLabel("wrapper.deleteChurch", "Delete")}
|
|
39
79
|
deleteFunction={() => { handleDelete(uc); }}
|
|
40
80
|
/>);
|
|
41
81
|
});
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
TextField,
|
|
6
|
+
Paper,
|
|
7
|
+
Box,
|
|
8
|
+
Typography,
|
|
9
|
+
Stack,
|
|
10
|
+
IconButton,
|
|
11
|
+
List,
|
|
12
|
+
ListItem,
|
|
13
|
+
ListItemAvatar,
|
|
14
|
+
ListItemText,
|
|
15
|
+
ListItemButton,
|
|
16
|
+
InputAdornment,
|
|
17
|
+
Divider,
|
|
18
|
+
Skeleton,
|
|
19
|
+
Avatar,
|
|
20
|
+
useTheme
|
|
21
|
+
} from "@mui/material";
|
|
22
|
+
import {
|
|
23
|
+
ArrowBack as ArrowBackIcon,
|
|
24
|
+
Search as SearchIcon,
|
|
25
|
+
PersonSearch as PersonSearchIcon,
|
|
26
|
+
Chat as ChatIcon
|
|
27
|
+
} from "@mui/icons-material";
|
|
28
|
+
import React, { useEffect, useState } from "react";
|
|
5
29
|
import { ApiHelper, Locale, PersonHelper } from "../../helpers";
|
|
6
30
|
import { ConversationInterface, PersonInterface, PrivateMessageInterface, UserContextInterface } from "@churchapps/helpers";
|
|
7
31
|
import { AddNote } from "../notes/AddNote";
|
|
8
|
-
import {
|
|
32
|
+
import { PersonAvatar } from "../PersonAvatar";
|
|
9
33
|
|
|
10
34
|
interface Props {
|
|
11
35
|
context: UserContextInterface;
|
|
@@ -20,11 +44,6 @@ export const NewPrivateMessage: React.FC<Props> = (props) => {
|
|
|
20
44
|
const [searchResults, setSearchResults] = React.useState([]);
|
|
21
45
|
const [selectedPerson, setSelectedPerson] = React.useState<PersonInterface>(null);
|
|
22
46
|
|
|
23
|
-
const handleSubmit = (e: React.MouseEvent) => {
|
|
24
|
-
if (e !== null) e.preventDefault();
|
|
25
|
-
let term = escape(searchText.trim());
|
|
26
|
-
ApiHelper.get("/people/search?term=" + term, "MembershipApi").then(data => setSearchResults(data));
|
|
27
|
-
}
|
|
28
47
|
|
|
29
48
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
30
49
|
setSearchText(e.currentTarget.value);
|
|
@@ -51,24 +70,13 @@ export const NewPrivateMessage: React.FC<Props> = (props) => {
|
|
|
51
70
|
setSelectedPerson(person);
|
|
52
71
|
}
|
|
53
72
|
|
|
54
|
-
const getPeople = () => {
|
|
55
|
-
let result = [];
|
|
56
|
-
for (let i = 0; i < searchResults.length; i++) {
|
|
57
|
-
const p = searchResults[i];
|
|
58
|
-
result.push(<TableRow key={p.id}>
|
|
59
|
-
<TableCell><img src={PersonHelper.getPhotoUrl(p)} alt="avatar" /></TableCell>
|
|
60
|
-
<TableCell><a href="about:blank" onClick={(e) => { e.preventDefault(); handlePersonSelected(p) }}>{p.name.display}</a></TableCell>
|
|
61
|
-
</TableRow>);
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
73
|
|
|
66
74
|
const handleNoteAdded = () => {
|
|
67
75
|
handlePersonSelected(selectedPerson);
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
const createConversation = async () => {
|
|
71
|
-
const conv: ConversationInterface = { allowAnonymousPosts: false, contentType: "privateMessage", contentId: props.context.person.id, title: props.context.person.name.display + " " + Locale.label("wrapper.privateMessage"), visibility: "hidden" }
|
|
79
|
+
const conv: ConversationInterface = { allowAnonymousPosts: false, contentType: "privateMessage", contentId: props.context.person.id, title: props.context.person.name.display + " " + Locale.label("wrapper.privateMessage", "Private Message"), visibility: "hidden" }
|
|
72
80
|
const result: ConversationInterface[] = await ApiHelper.post("/conversations", [conv], "MessagingApi");
|
|
73
81
|
|
|
74
82
|
const pm: PrivateMessageInterface = {
|
|
@@ -86,34 +94,163 @@ export const NewPrivateMessage: React.FC<Props> = (props) => {
|
|
|
86
94
|
}, [props.selectedPerson]);
|
|
87
95
|
|
|
88
96
|
|
|
97
|
+
const theme = useTheme();
|
|
98
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
99
|
+
|
|
100
|
+
const handleSearchSubmit = async (e: React.MouseEvent) => {
|
|
101
|
+
if (e !== null) e.preventDefault();
|
|
102
|
+
if (!searchText.trim()) return;
|
|
103
|
+
|
|
104
|
+
setIsSearching(true);
|
|
105
|
+
let term = escape(searchText.trim());
|
|
106
|
+
const data = await ApiHelper.get("/people/search?term=" + term, "MembershipApi");
|
|
107
|
+
setSearchResults(data);
|
|
108
|
+
setIsSearching(false);
|
|
109
|
+
};
|
|
110
|
+
|
|
89
111
|
if (!selectedPerson) return (
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
<Paper elevation={0} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
113
|
+
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
|
114
|
+
<Stack direction="row" alignItems="center" spacing={2}>
|
|
115
|
+
<IconButton onClick={props.onBack}>
|
|
116
|
+
<ArrowBackIcon />
|
|
117
|
+
</IconButton>
|
|
118
|
+
<Typography variant="h6" component="h2">
|
|
119
|
+
{Locale.label("wrapper.newPrivateMessage", "New Private Message")}
|
|
120
|
+
</Typography>
|
|
121
|
+
</Stack>
|
|
122
|
+
</Box>
|
|
123
|
+
|
|
124
|
+
<Box sx={{ p: 3 }}>
|
|
125
|
+
<Stack spacing={3}>
|
|
126
|
+
<Box>
|
|
127
|
+
<Typography variant="body1" color="textSecondary" gutterBottom>
|
|
128
|
+
{Locale.label("wrapper.searchForPerson", "Search for a person to message")}
|
|
129
|
+
</Typography>
|
|
130
|
+
<TextField
|
|
131
|
+
fullWidth
|
|
132
|
+
placeholder="Search by name..."
|
|
133
|
+
id="searchText"
|
|
134
|
+
data-testid="search-input"
|
|
135
|
+
value={searchText}
|
|
136
|
+
onChange={handleChange}
|
|
137
|
+
onKeyDown={(e) => {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
if (e.key === 'Enter') handleSearchSubmit(null);
|
|
140
|
+
}}
|
|
141
|
+
InputProps={{
|
|
142
|
+
startAdornment: (
|
|
143
|
+
<InputAdornment position="start">
|
|
144
|
+
<PersonSearchIcon color="action" />
|
|
145
|
+
</InputAdornment>
|
|
146
|
+
),
|
|
147
|
+
endAdornment: (
|
|
148
|
+
<InputAdornment position="end">
|
|
149
|
+
<Button
|
|
150
|
+
variant="contained"
|
|
151
|
+
size="small"
|
|
152
|
+
onClick={handleSearchSubmit}
|
|
153
|
+
disabled={!searchText.trim() || isSearching}
|
|
154
|
+
>
|
|
155
|
+
{Locale.label("common.search", "Search")}
|
|
156
|
+
</Button>
|
|
157
|
+
</InputAdornment>
|
|
158
|
+
)
|
|
159
|
+
}}
|
|
160
|
+
sx={{ mt: 1 }}
|
|
161
|
+
/>
|
|
162
|
+
</Box>
|
|
163
|
+
|
|
164
|
+
{isSearching && (
|
|
165
|
+
<Box>
|
|
166
|
+
{[...Array(3)].map((_, index) => (
|
|
167
|
+
<Box key={index} sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
168
|
+
<Skeleton variant="circular" width={48} height={48} sx={{ mr: 2 }} />
|
|
169
|
+
<Skeleton variant="text" width="60%" height={24} />
|
|
170
|
+
</Box>
|
|
171
|
+
))}
|
|
172
|
+
</Box>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{!isSearching && searchResults.length > 0 && (
|
|
176
|
+
<Box>
|
|
177
|
+
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
|
|
178
|
+
{searchResults.length} {searchResults.length === 1 ? 'person' : 'people'} found
|
|
179
|
+
</Typography>
|
|
180
|
+
<List sx={{ bgcolor: 'background.paper', borderRadius: 1 }}>
|
|
181
|
+
{searchResults.map((person, index) => (
|
|
182
|
+
<React.Fragment key={person.id}>
|
|
183
|
+
<ListItemButton
|
|
184
|
+
onClick={() => handlePersonSelected(person)}
|
|
185
|
+
sx={{ py: 2 }}
|
|
186
|
+
>
|
|
187
|
+
<ListItemAvatar>
|
|
188
|
+
<PersonAvatar person={person} size="small" />
|
|
189
|
+
</ListItemAvatar>
|
|
190
|
+
<ListItemText
|
|
191
|
+
primary={person.name.display}
|
|
192
|
+
secondary={person.contactInfo?.email || ''}
|
|
193
|
+
/>
|
|
194
|
+
</ListItemButton>
|
|
195
|
+
{index < searchResults.length - 1 && <Divider />}
|
|
196
|
+
</React.Fragment>
|
|
197
|
+
))}
|
|
198
|
+
</List>
|
|
199
|
+
</Box>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{!isSearching && searchText && searchResults.length === 0 && (
|
|
203
|
+
<Box sx={{ textAlign: 'center', py: 4 }}>
|
|
204
|
+
<PersonSearchIcon sx={{ fontSize: 48, color: 'grey.400', mb: 2 }} />
|
|
205
|
+
<Typography variant="h6" color="textSecondary">
|
|
206
|
+
No people found
|
|
207
|
+
</Typography>
|
|
208
|
+
<Typography variant="body2" color="textSecondary">
|
|
209
|
+
Try searching with a different name
|
|
210
|
+
</Typography>
|
|
211
|
+
</Box>
|
|
212
|
+
)}
|
|
213
|
+
</Stack>
|
|
214
|
+
</Box>
|
|
215
|
+
</Paper>
|
|
106
216
|
);
|
|
107
217
|
else {
|
|
108
218
|
return (
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
219
|
+
<Paper elevation={0} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
220
|
+
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
|
221
|
+
<Stack direction="row" alignItems="center" spacing={2}>
|
|
222
|
+
<IconButton onClick={props.onBack}>
|
|
223
|
+
<ArrowBackIcon />
|
|
224
|
+
</IconButton>
|
|
225
|
+
<Typography variant="h6" component="h2">
|
|
226
|
+
{Locale.label("wrapper.newPrivateMessage", "New Private Message")}
|
|
227
|
+
</Typography>
|
|
228
|
+
</Stack>
|
|
229
|
+
</Box>
|
|
230
|
+
|
|
231
|
+
<Box sx={{ p: 3 }}>
|
|
232
|
+
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 3 }}>
|
|
233
|
+
<PersonAvatar person={selectedPerson} size="medium" />
|
|
234
|
+
<Box>
|
|
235
|
+
<Typography variant="subtitle1" fontWeight="medium">
|
|
236
|
+
{selectedPerson.name.display}
|
|
237
|
+
</Typography>
|
|
238
|
+
{selectedPerson.contactInfo?.email && (
|
|
239
|
+
<Typography variant="body2" color="textSecondary">
|
|
240
|
+
{selectedPerson.contactInfo.email}
|
|
241
|
+
</Typography>
|
|
242
|
+
)}
|
|
243
|
+
</Box>
|
|
244
|
+
</Stack>
|
|
245
|
+
<Divider sx={{ mb: 3 }} />
|
|
246
|
+
<AddNote
|
|
247
|
+
context={props.context}
|
|
248
|
+
conversationId={null}
|
|
249
|
+
onUpdate={handleNoteAdded}
|
|
250
|
+
createConversation={createConversation}
|
|
251
|
+
/>
|
|
252
|
+
</Box>
|
|
253
|
+
</Paper>
|
|
117
254
|
)
|
|
118
255
|
}
|
|
119
256
|
}
|