l-min-components 1.0.1205 → 1.0.1207
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "l-min-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1207",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/assets",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"preview": "vite preview"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"@reduxjs/toolkit": "^2.6.1",
|
|
16
17
|
"@slate-serializers/html": "^2.1.2",
|
|
17
18
|
"axios": "^1.4.0",
|
|
18
19
|
"axios-hooks": "^4.0.0",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"react-modal": "^3.16.1",
|
|
57
58
|
"react-multi-carousel": "^2.8.5",
|
|
58
59
|
"react-player": "^2.15.1",
|
|
60
|
+
"react-redux": "^9.2.0",
|
|
59
61
|
"react-responsive-carousel": "^3.2.23",
|
|
60
62
|
"react-router-dom": "^6.8.2",
|
|
61
63
|
"react-slick": "0.30.2",
|
|
@@ -25,6 +25,7 @@ import useHeader from "../header/getHeaderDetails";
|
|
|
25
25
|
import GracePeriod from "../deactivated";
|
|
26
26
|
import MobileLayout from "../mobileLayout";
|
|
27
27
|
import useTranslation from "../../hooks/useTranslation";
|
|
28
|
+
import useMessageKit from "../../hooks/messaging-kit";
|
|
28
29
|
|
|
29
30
|
const AppMainLayout = ({ children }) => {
|
|
30
31
|
const [isOpen, setIsOpen] = useState(true);
|
|
@@ -241,6 +242,8 @@ const AppMainLayout = ({ children }) => {
|
|
|
241
242
|
setIsTranslationsLoading,
|
|
242
243
|
} = useTranslation(wordBank);
|
|
243
244
|
|
|
245
|
+
useMessageKit(affiliatesActive, selectedAccount, affiliateAccount); // useMessageKit
|
|
246
|
+
|
|
244
247
|
return (
|
|
245
248
|
<OutletContext.Provider
|
|
246
249
|
value={{
|
|
@@ -272,8 +275,7 @@ const AppMainLayout = ({ children }) => {
|
|
|
272
275
|
setNewNotifications,
|
|
273
276
|
notificationMarkReadData,
|
|
274
277
|
handleGetNotificationMarkRead,
|
|
275
|
-
}}
|
|
276
|
-
>
|
|
278
|
+
}}>
|
|
277
279
|
{/* display mobile layout on device width*/}
|
|
278
280
|
{deviceWidth < 1200 ? (
|
|
279
281
|
<MobileLayout findText={findText} />
|
|
@@ -285,8 +287,7 @@ const AppMainLayout = ({ children }) => {
|
|
|
285
287
|
) : (
|
|
286
288
|
<Layout
|
|
287
289
|
coming={coming}
|
|
288
|
-
hasLayoutBackgroundImage={hasLayoutBackgroundImage}
|
|
289
|
-
>
|
|
290
|
+
hasLayoutBackgroundImage={hasLayoutBackgroundImage}>
|
|
290
291
|
<HeaderComponent
|
|
291
292
|
setNewNotifications={setNewNotifications}
|
|
292
293
|
findText={findText}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import useAxios from "axios-hooks";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents the details of the last message in a chat room overview.
|
|
6
|
+
* @typedef {object} LastMessage
|
|
7
|
+
* @property {string} id - The unique identifier of the message.
|
|
8
|
+
* @property {string} text - The text content of the message.
|
|
9
|
+
* @property {object} media - Media attachments associated with the message (structure might vary based on actual usage).
|
|
10
|
+
* @property {string} created_at - ISO 8601 timestamp string of when the message was created.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents the profile picture details associated with a chat room overview.
|
|
15
|
+
* @typedef {object} ProfilePicture
|
|
16
|
+
* @property {string} url - The URL of the profile picture.
|
|
17
|
+
* @property {string} mimetype - The MIME type of the picture (e.g., 'image/png').
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Represents an individual chat room within a course grouping (overview).
|
|
22
|
+
* @typedef {object} ChatRoom
|
|
23
|
+
* @property {string} id - The unique identifier for the chat room (UUID string).
|
|
24
|
+
* @property {string} account_id - The ID of the associated account (likely user or instructor).
|
|
25
|
+
* @property {string} profile_name - The display name for the chat room or the other participant.
|
|
26
|
+
* @property {string} account_type - The type of the associated account (e.g., 'INSTRUCTOR', 'STUDENT').
|
|
27
|
+
* @property {boolean} is_private - Indicates if the chat room is private (e.g., a direct message).
|
|
28
|
+
* @property {boolean} is_online - Indicates the online status associated with the room/participant.
|
|
29
|
+
* @property {boolean} pin_status - Indicates if this specific chat room is pinned by the current user.
|
|
30
|
+
* @property {string} created_at - ISO 8601 timestamp string of when the room was created.
|
|
31
|
+
* @property {number} unread_count - The number of unread messages in this specific room for the current user.
|
|
32
|
+
* @property {LastMessage | null} last_message - The last message object sent in the room, or null if no messages exist.
|
|
33
|
+
* @property {ProfilePicture | null} profile_picture - The profile picture object associated with the room/participant, or null if none exists.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Represents a grouping of chat rooms associated with a specific course.
|
|
38
|
+
* Corresponds to an element in the 'results' array from the API for room lists.
|
|
39
|
+
* @typedef {object} CourseChatGrouping
|
|
40
|
+
* @property {number} id - The unique identifier for the course grouping (likely the course ID).
|
|
41
|
+
* @property {string} name - The name of the course.
|
|
42
|
+
* @property {boolean} pin_status - Indicates if the overall course grouping is pinned by the current user.
|
|
43
|
+
* @property {number} total_unread_count - The aggregated total number of unread messages across all rooms listed in this grouping for the current user.
|
|
44
|
+
* @property {ChatRoom[]} rooms - An array of individual chat rooms associated with this course.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Information about the sender of a message within chat history.
|
|
49
|
+
* @typedef {object} SenderInfo
|
|
50
|
+
* @property {string} id - The unique identifier of the sender.
|
|
51
|
+
* @property {string} first_name - The first name of the sender.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detailed information about a media attachment within chat history.
|
|
56
|
+
* @typedef {object} MediaDetails
|
|
57
|
+
* @property {string} id - Unique identifier for the media file.
|
|
58
|
+
* @property {string} url - URL to access the media file stream.
|
|
59
|
+
* @property {string} name - The original filename of the media.
|
|
60
|
+
* @property {number} size - The size of the media file in bytes.
|
|
61
|
+
* @property {string} duration - The duration of the media (e.g., "HH:MM:SS" format).
|
|
62
|
+
* @property {string} mimetype - The MIME type of the media file (e.g., 'audio/x-wav').
|
|
63
|
+
* @property {string | null} last_used - ISO 8601 timestamp indicating the last usage, or null.
|
|
64
|
+
* @property {string} created_at - ISO 8601 timestamp string of when the media was uploaded/created.
|
|
65
|
+
* @property {string} updated_at - ISO 8601 timestamp string of the last update to the media record.
|
|
66
|
+
* @property {number} course_count - Count related to course usage.
|
|
67
|
+
* @property {string | null} thumbnail_url - URL for a thumbnail image, or null.
|
|
68
|
+
* @property {string} convertion_status - Status of any media conversion process.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Represents a single chat message within chat history.
|
|
73
|
+
* @typedef {object} ChatMessage
|
|
74
|
+
* @property {string} id - The unique identifier for the message (UUID string).
|
|
75
|
+
* @property {string | null} text - The text content of the message. Null if only media.
|
|
76
|
+
* @property {MediaDetails | object | null} media - Media details, an empty object, or null.
|
|
77
|
+
* @property {SenderInfo} sender - Information about the message sender.
|
|
78
|
+
* @property {boolean} is_read - Read status flag.
|
|
79
|
+
* @property {boolean} is_delivered - Delivery status flag.
|
|
80
|
+
* @property {string} created_at - ISO 8601 timestamp string when the message was created.
|
|
81
|
+
* @property {boolean} user_message - True if sent by the current user, false otherwise.
|
|
82
|
+
* @property {string} date - Specific date (YYYY-MM-DD format) message was sent.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Represents a grouping of messages sent on a specific date within chat history.
|
|
87
|
+
* @typedef {object} MessagesByDate
|
|
88
|
+
* @property {string} date - Display string for the date group (e.g., "today", "Apr 04").
|
|
89
|
+
* @property {ChatMessage[]} messages - Array of message objects for this date group.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Represents the chat history structure for a single room, used as values in the state.chats object.
|
|
94
|
+
* @typedef {object} RoomChatHistory
|
|
95
|
+
* @property {MessagesByDate[]} results - An array containing messages grouped by date for the specific room.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Represents the expected structure of the API response data payload
|
|
100
|
+
* containing messages grouped by date.
|
|
101
|
+
* @typedef {object} MessageApiResponseData
|
|
102
|
+
* @property {number} count - Total number of items available across all pages (if pagination applies to items, not groups).
|
|
103
|
+
* @property {string | null} next - URL for the next page of results (date groups), or null.
|
|
104
|
+
* @property {string | null} previous - URL for the previous page of results (date groups), or null.
|
|
105
|
+
* @property {MessagesByDate[]} results - Array containing messages grouped by date.
|
|
106
|
+
*/
|
|
107
|
+
const useMessageKit = (affiliatesActive, selectedAccount, affiliateAccount) => {
|
|
108
|
+
// get account type
|
|
109
|
+
const [, request] = useAxios(
|
|
110
|
+
{
|
|
111
|
+
method: "GET",
|
|
112
|
+
params: {
|
|
113
|
+
_account: selectedAccount && selectedAccount.id,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
manual: true,
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
//get account type
|
|
122
|
+
|
|
123
|
+
// initialize message store
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* React state hook managing application chat data.
|
|
127
|
+
*
|
|
128
|
+
* The `state` object holds chat rooms grouped by courses (`roomsByCourses`)
|
|
129
|
+
* and the detailed message history for individual chat rooms (`chats`).
|
|
130
|
+
* The `setState` function is used to update this state.
|
|
131
|
+
*
|
|
132
|
+
* @type {[
|
|
133
|
+
* { roomsByCourses: CourseChatGrouping[], chats: Record<string, RoomChatHistory> },
|
|
134
|
+
* React.Dispatch<React.SetStateAction<{ roomsByCourses: CourseChatGrouping[], chats: Record<string, RoomChatHistory> }>>
|
|
135
|
+
* ]} stateTuple - A tuple containing the state object and the dispatcher function. Note the type for `chats`.
|
|
136
|
+
* @property {CourseChatGrouping[]} stateTuple[0].roomsByCourses - Chat rooms grouped by their associated course. Initialized as empty array.
|
|
137
|
+
* @property {Record<string, RoomChatHistory>} stateTuple[0].chats - An object mapping Room IDs (string keys) to their respective chat history (`RoomChatHistory` objects).
|
|
138
|
+
* Example structure: { "roomId123": { results: [...] }, "roomId456": { results: [...] } }
|
|
139
|
+
*/
|
|
140
|
+
const [state, setState] = useState({
|
|
141
|
+
roomsByCourses: [],
|
|
142
|
+
chats: {},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// get room grouped by course list
|
|
146
|
+
const getMessageRoomsByCourses = async (nextPage) => {
|
|
147
|
+
if (!selectedAccount) return;
|
|
148
|
+
// selected account exists
|
|
149
|
+
let response;
|
|
150
|
+
if (String(selectedAccount.type).toLowerCase() === "enterprise") {
|
|
151
|
+
response = await request({
|
|
152
|
+
url: nextPage || "/notify/v1/enterprise/rooms/",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
String(selectedAccount.type).toLowerCase() === "instructor" &&
|
|
157
|
+
affiliateAccount
|
|
158
|
+
) {
|
|
159
|
+
response = await request({
|
|
160
|
+
url: nextPage || "/notify/v1/instructor/:enterprise_id/rooms/",
|
|
161
|
+
});
|
|
162
|
+
} // incomplete
|
|
163
|
+
if (String(selectedAccount.type).toLowerCase() === "personal") {
|
|
164
|
+
response = await request({
|
|
165
|
+
url: nextPage || "/notify/v1/rooms/",
|
|
166
|
+
});
|
|
167
|
+
} // incomplete
|
|
168
|
+
if (!response.data) return;
|
|
169
|
+
//found data
|
|
170
|
+
|
|
171
|
+
const { results: newCourseGroupings, next } = response.data;
|
|
172
|
+
setState((prevState) => {
|
|
173
|
+
const combined = [...prevState.roomsByCourses, ...newCourseGroupings];
|
|
174
|
+
// Use a Map to filter out duplicates based on the course grouping 'id'
|
|
175
|
+
const uniqueMap = new Map(combined.map((item) => [item.id, item]));
|
|
176
|
+
const uniqueCourseGroupings = Array.from(uniqueMap.values());
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
...prevState,
|
|
180
|
+
roomsByCourses: uniqueCourseGroupings,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
if (next) {
|
|
184
|
+
// Ensure the correct function name is called recursively
|
|
185
|
+
getMessageRoomsByCourses(next);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// get individual chats
|
|
190
|
+
const getIndividualChats = async (nextPage, roomId) => {
|
|
191
|
+
if (!selectedAccount) return;
|
|
192
|
+
|
|
193
|
+
let response;
|
|
194
|
+
if (String(selectedAccount.type).toLowerCase() === "enterprise") {
|
|
195
|
+
response = await request({
|
|
196
|
+
url: nextPage || "/notify/v1/enterprise/chats/",
|
|
197
|
+
params: {
|
|
198
|
+
_account: selectedAccount.id,
|
|
199
|
+
room_id: roomId,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!response.data) return;
|
|
205
|
+
|
|
206
|
+
/** @type {MessageApiResponseData} */
|
|
207
|
+
const responseData = response.data;
|
|
208
|
+
const { results: newDateGroups } = responseData;
|
|
209
|
+
|
|
210
|
+
setState((prevState) => {
|
|
211
|
+
let unreadCountToSubtract = 0;
|
|
212
|
+
|
|
213
|
+
// --- Update roomsByCourses ---
|
|
214
|
+
const updatedRoomsByCourses = prevState.roomsByCourses.map(
|
|
215
|
+
(courseGroup) => {
|
|
216
|
+
// Find the room within this course group
|
|
217
|
+
const roomIndex = courseGroup.rooms.findIndex(
|
|
218
|
+
(room) => room.id === roomId
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (roomIndex !== -1) {
|
|
222
|
+
// Found the room in this course group
|
|
223
|
+
const targetRoom = courseGroup.rooms[roomIndex];
|
|
224
|
+
unreadCountToSubtract = targetRoom.unread_count; // Store the count before resetting
|
|
225
|
+
|
|
226
|
+
// Create a new rooms array with the updated room
|
|
227
|
+
const updatedRooms = [
|
|
228
|
+
...courseGroup.rooms.slice(0, roomIndex),
|
|
229
|
+
{ ...targetRoom, unread_count: 0 }, // Reset unread count for the specific room
|
|
230
|
+
...courseGroup.rooms.slice(roomIndex + 1),
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
// Return a new course group object with updated total count and rooms
|
|
234
|
+
return {
|
|
235
|
+
...courseGroup,
|
|
236
|
+
// Ensure total_unread_count doesn't go below zero
|
|
237
|
+
total_unread_count: Math.max(
|
|
238
|
+
0,
|
|
239
|
+
courseGroup.total_unread_count - unreadCountToSubtract
|
|
240
|
+
),
|
|
241
|
+
rooms: updatedRooms,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// If the room wasn't in this group, return the group unchanged
|
|
246
|
+
return courseGroup;
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
// --- End Update roomsByCourses ---
|
|
250
|
+
|
|
251
|
+
// --- Update chats (existing logic) ---
|
|
252
|
+
const existingDateGroups = prevState.chats[roomId]?.results || [];
|
|
253
|
+
const allDateGroups = [...existingDateGroups, ...newDateGroups];
|
|
254
|
+
|
|
255
|
+
// Group messages by date and de-duplicate messages within each date using their ID
|
|
256
|
+
const messagesGroupedByDate = {};
|
|
257
|
+
allDateGroups.forEach((dateGroup) => {
|
|
258
|
+
if (!messagesGroupedByDate[dateGroup.date]) {
|
|
259
|
+
// Use a Map for efficient de-duplication by message ID
|
|
260
|
+
messagesGroupedByDate[dateGroup.date] = {
|
|
261
|
+
date: dateGroup.date, // Store the date string
|
|
262
|
+
messagesMap: new Map(),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
dateGroup.messages.forEach((message) => {
|
|
266
|
+
messagesGroupedByDate[dateGroup.date].messagesMap.set(
|
|
267
|
+
message.id,
|
|
268
|
+
message
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Reconstruct the final array of date groups with unique messages
|
|
274
|
+
const finalDateGroups = Object.values(messagesGroupedByDate).map(
|
|
275
|
+
(group) => ({
|
|
276
|
+
date: group.date,
|
|
277
|
+
// Convert Map values back to an array and sort messages by creation time
|
|
278
|
+
messages: Array.from(group.messagesMap.values()).sort(
|
|
279
|
+
(a, b) => new Date(a.created_at) - new Date(b.created_at)
|
|
280
|
+
),
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Optional: Sort the date groups themselves if needed (e.g., chronologically)
|
|
285
|
+
// finalDateGroups.sort((a, b) => /* comparison logic based on date */);
|
|
286
|
+
|
|
287
|
+
// --- Return combined state update ---
|
|
288
|
+
return {
|
|
289
|
+
...prevState,
|
|
290
|
+
roomsByCourses: updatedRoomsByCourses, // Include the updated course/room list
|
|
291
|
+
chats: {
|
|
292
|
+
...prevState.chats,
|
|
293
|
+
// Update the structure to match RoomChatHistory: { results: [...] }
|
|
294
|
+
[roomId]: {
|
|
295
|
+
// Preserve potential other properties if RoomChatHistory expands later
|
|
296
|
+
...(prevState.chats[roomId] || {}),
|
|
297
|
+
results: finalDateGroups,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
!nextPage && (await readRoomMessages(roomId));
|
|
303
|
+
// Pass roomId correctly in the recursive call
|
|
304
|
+
if (responseData.next) getIndividualChats(responseData.next, roomId);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// read messages
|
|
308
|
+
const readRoomMessages = async (roomId) => {
|
|
309
|
+
return await request({
|
|
310
|
+
url: `/notify/v1/enterprise/chats/${roomId}/read_message/`,
|
|
311
|
+
account_id: selectedAccount.id,
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
(async () => {
|
|
316
|
+
await getMessageRoomsByCourses();
|
|
317
|
+
await getIndividualChats(null, "5be9b48281ac4c2885d3b719654ed59d"); // only call to get individual chat
|
|
318
|
+
})();
|
|
319
|
+
}, [selectedAccount]);
|
|
320
|
+
|
|
321
|
+
console.log(state, "messages"); // Keep console log for debugging if needed
|
|
322
|
+
// send messages to account based on account type
|
|
323
|
+
|
|
324
|
+
// Return the state and potentially the functions if they need to be called externally
|
|
325
|
+
return { state, getIndividualChats, getMessageRoomsByCourses };
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export default useMessageKit;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createSlice } from "@reduxjs/toolkit";
|
|
2
|
+
|
|
3
|
+
const initialState = {
|
|
4
|
+
messageList: [],
|
|
5
|
+
chats: [],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const messageSlice = createSlice({
|
|
9
|
+
name: "message",
|
|
10
|
+
initialState,
|
|
11
|
+
reducers: {
|
|
12
|
+
setMessage: (state, action) => {
|
|
13
|
+
state.message = action.payload;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const { setMessage } = messageSlice.actions;
|
|
19
|
+
|
|
20
|
+
export default messageSlice.reducer;
|