@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
@@ -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
- const messages: MessageInterface[] = (props.conversationId) ? await ApiHelper.get("/messages/conversation/" + props.conversationId, "MessagingApi") : [];
25
- if (messages.length > 0) {
26
- const peopleIds = ArrayHelper.getIds(messages, "personId");
27
- const people = await ApiHelper.get("/people/basic?ids=" + peopleIds.join(","), "MembershipApi");
28
- messages.forEach(n => {
29
- n.person = ArrayHelper.getOne(people, "id", n.personId);
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) return <div id="notesScroll" style={{maxHeight: props.maxHeight, overflowY: "scroll"}}>{notes}</div>
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
- setTimeout(() => {
57
- const element = window?.document?.getElementById("notesScroll");
58
- if (element) element.scrollTop = element.scrollHeight;
59
- }, 100);
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
- {getNotesWrapper()}
65
- {messages && (<AddNote context={props.context} conversationId={props.conversationId} onUpdate={loadNotes} createConversation={props.createConversation} messageId={editMessageId} />)}
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 "../../helpers/ApiHelper";
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
- const [userChurches, setUserChurches] = useState(UserHelper.userChurches.filter(uc => uc.apis.length > 0));
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
- const label = Locale.label("wrapper.sureRemoveChurch").replace("{}", uc.church.name?.toUpperCase());
17
- if (window.confirm(label)) {
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={Locale.label("wrapper.deleteChurch")}
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 { Button, TextField, TableRow, TableCell, Table, TableBody } from "@mui/material";
4
- import React, { useEffect } from "react";
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 { SmallButton } from "../SmallButton";
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
- <div style={{ paddingLeft: 10, paddingRight: 10 }}>
91
- <span style={{ float: "right" }}>
92
- <SmallButton icon="chevron_left" text="Back" onClick={props.onBack} />
93
- </span>
94
- <b>{Locale.label("wrapper.newPrivateMessage")}</b>
95
- <div>{Locale.label("wrapper.searchForPerson")}</div>
96
-
97
- <TextField fullWidth label="Name" id="searchText" data-testid="search-input" name="searchText" type="text" placeholder="Name" value={searchText} onChange={handleChange}
98
- onKeyDown={(e) => {e.stopPropagation()}}
99
- InputProps={{ endAdornment: <Button variant="contained" id="searchButton" data-testid="search-button" onClick={handleSubmit}>{Locale.label("common.search")}</Button> }}
100
- />
101
- <br />
102
- <Table id="smallPeopleTable" size="small">
103
- <TableBody>{getPeople()}</TableBody>
104
- </Table>
105
- </div>
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
- <div style={{ paddingLeft: 10, paddingRight: 10 }}>
110
- <span style={{ float: "right" }}>
111
- <SmallButton icon="chevron_left" text="Back" onClick={props.onBack} />
112
- </span>
113
- <b>{Locale.label("wrapper.newPrivateMessage")}</b>
114
- <div>To: {selectedPerson.name.display}</div>
115
- <AddNote context={props.context} conversationId={null} onUpdate={handleNoteAdded} createConversation={createConversation} />
116
- </div>
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
  }