l-min-components 1.6.1259 → 1.6.1263
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 +1 -1
- package/src/hooks/messaging-kit/index.jsx +471 -196
package/package.json
CHANGED
|
@@ -211,6 +211,21 @@ import sound from "./new-notification-7-210334.mp3";
|
|
|
211
211
|
* @property {WebSocketMessageData} data - The payload containing the message details.
|
|
212
212
|
*/
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Represents an optimistic message before server confirmation.
|
|
216
|
+
* Minimal version without sender info stored directly.
|
|
217
|
+
* @typedef {object} OptimisticMessage
|
|
218
|
+
* @property {string} tempId - Client-generated unique temporary ID (e.g., UUID v4).
|
|
219
|
+
* @property {'pending' | 'failed'} status - The current status of the optimistic message.
|
|
220
|
+
* @property {string} courseId - The course ID associated with the message. Used for grouping pending.
|
|
221
|
+
* @property {string} accountId - The target account ID associated with the message. Used for grouping pending.
|
|
222
|
+
* @property {string | null} text - Message text.
|
|
223
|
+
* @property {any | null} media - Media object/data sent by the user (structure might vary).
|
|
224
|
+
* @property {string} created_at - Client-side ISO 8601 timestamp string when the message was initiated.
|
|
225
|
+
* @property {boolean} user_message - Always true for optimistic messages.
|
|
226
|
+
* @property {any | null} error - Stores error info if status is 'failed'.
|
|
227
|
+
*/
|
|
228
|
+
|
|
214
229
|
const useMessageKit = (/*affiliatesActive*/) => {
|
|
215
230
|
const cookieGrabber = (key = "") => {
|
|
216
231
|
const cookies = document.cookie;
|
|
@@ -276,11 +291,21 @@ const useMessageKit = (/*affiliatesActive*/) => {
|
|
|
276
291
|
* @property {Record<string, RoomChatHistory>} stateTuple[0].chats - An object mapping Room IDs (string keys) to their respective chat history (`RoomChatHistory` objects).
|
|
277
292
|
* Example structure: { "roomId123": { results: [...] }, "roomId456": { results: [...] } }
|
|
278
293
|
*/
|
|
294
|
+
|
|
295
|
+
// initialize message store
|
|
279
296
|
const [state, setState] = useState({
|
|
280
297
|
roomsByCourses: [],
|
|
281
|
-
chats: {},
|
|
282
|
-
loadingRoomIds: new Set(),
|
|
298
|
+
chats: {}, // Maps roomId -> RoomChatHistory (MessagesByDate[])
|
|
299
|
+
loadingRoomIds: new Set(),
|
|
300
|
+
/**
|
|
301
|
+
* Stores optimistic messages sent by the user but not yet confirmed.
|
|
302
|
+
* Key: `pending_${courseId}_${accountId}`
|
|
303
|
+
* Value: OptimisticMessage[]
|
|
304
|
+
* @type {Record<string, OptimisticMessage[]>}
|
|
305
|
+
*/
|
|
306
|
+
optimisticMessages: {},
|
|
283
307
|
});
|
|
308
|
+
|
|
284
309
|
// State for rooms that should be automatically marked as read
|
|
285
310
|
const [autoReadRoomIds, setAutoReadRoomIds] = useState(() => new Set());
|
|
286
311
|
|
|
@@ -626,236 +651,482 @@ const useMessageKit = (/*affiliatesActive*/) => {
|
|
|
626
651
|
}, []);
|
|
627
652
|
// --- End functions to manage auto-read rooms ---
|
|
628
653
|
|
|
629
|
-
//
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
String(
|
|
649
|
-
|
|
650
|
-
// &&
|
|
651
|
-
// affiliatesActive
|
|
652
|
-
) {
|
|
653
|
-
response = await request({
|
|
654
|
-
url: `/notify/v1/instructor/${affiliateAccount}/chats/`,
|
|
655
|
-
params: {
|
|
656
|
-
account_id: accountId,
|
|
657
|
-
course_id: courseId,
|
|
658
|
-
_account: selectedAccount.id,
|
|
659
|
-
},
|
|
660
|
-
data: {
|
|
661
|
-
text,
|
|
662
|
-
media,
|
|
663
|
-
},
|
|
664
|
-
method: "Post",
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
if (selectedAccount.type.toLowerCase() === "personal") {
|
|
668
|
-
response = await request({
|
|
669
|
-
url: `/notify/v1/chats/`,
|
|
670
|
-
params: {
|
|
671
|
-
_account: selectedAccount.id,
|
|
672
|
-
course_id: courseId,
|
|
673
|
-
account_id: accountId,
|
|
674
|
-
},
|
|
675
|
-
data: {
|
|
676
|
-
text,
|
|
677
|
-
media,
|
|
678
|
-
},
|
|
679
|
-
method: "Post",
|
|
680
|
-
});
|
|
654
|
+
// Helper to get YYYY-MM-DD (ensure consistency)
|
|
655
|
+
const getLocalDateString = (isoTimestamp) => {
|
|
656
|
+
if (!isoTimestamp) return new Date().toISOString().split("T")[0];
|
|
657
|
+
try {
|
|
658
|
+
const date = new Date(isoTimestamp);
|
|
659
|
+
// Check if date is valid before formatting
|
|
660
|
+
if (isNaN(date.getTime())) {
|
|
661
|
+
throw new Error("Invalid date object");
|
|
662
|
+
}
|
|
663
|
+
const year = date.getFullYear();
|
|
664
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
665
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
666
|
+
return `${year}-${month}-${day}`;
|
|
667
|
+
} catch (e) {
|
|
668
|
+
console.error("Error parsing date:", isoTimestamp, e);
|
|
669
|
+
// Fallback to current date string
|
|
670
|
+
const today = new Date();
|
|
671
|
+
const year = today.getFullYear();
|
|
672
|
+
const month = String(today.getMonth() + 1).padStart(2, "0");
|
|
673
|
+
const day = String(today.getDate()).padStart(2, "0");
|
|
674
|
+
return `${year}-${month}-${day}`;
|
|
681
675
|
}
|
|
676
|
+
};
|
|
682
677
|
|
|
683
|
-
|
|
678
|
+
// Helper to get the display label for a date group (like "Today", "Yesterday", "Apr 06")
|
|
679
|
+
const getDisplayDateLabel = (dateString) => {
|
|
680
|
+
// dateString is YYYY-MM-DD
|
|
681
|
+
if (!dateString) return "Unknown Date";
|
|
684
682
|
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
683
|
+
const today = new Date();
|
|
684
|
+
const yesterday = new Date(today);
|
|
685
|
+
yesterday.setDate(today.getDate() - 1);
|
|
688
686
|
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
687
|
+
// Use UTC dates for comparison to avoid timezone issues affecting "Today"/"Yesterday" logic
|
|
688
|
+
const todayUtcString = getLocalDateString(today.toISOString());
|
|
689
|
+
const yesterdayUtcString = getLocalDateString(yesterday.toISOString());
|
|
690
|
+
|
|
691
|
+
if (dateString === todayUtcString) {
|
|
692
|
+
return "Today";
|
|
693
|
+
}
|
|
694
|
+
if (dateString === yesterdayUtcString) {
|
|
695
|
+
return "Yesterday";
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Format other dates (e.g., "Apr 06")
|
|
699
|
+
try {
|
|
700
|
+
// Use the dateString directly which should be YYYY-MM-DD
|
|
701
|
+
// Create date object assuming UTC to avoid timezone shifts in formatting
|
|
702
|
+
const messageDate = new Date(dateString + "T00:00:00Z");
|
|
703
|
+
// Check for invalid date object again before formatting
|
|
704
|
+
if (isNaN(messageDate.getTime())) {
|
|
705
|
+
return dateString; // Fallback to YYYY-MM-DD if parsing failed
|
|
698
706
|
}
|
|
699
|
-
|
|
707
|
+
return messageDate.toLocaleDateString("en-US", {
|
|
708
|
+
timeZone: "UTC",
|
|
709
|
+
month: "short",
|
|
710
|
+
day: "numeric",
|
|
711
|
+
});
|
|
712
|
+
} catch (e) {
|
|
713
|
+
console.error("Error formatting date label for:", dateString, e);
|
|
714
|
+
return dateString; // Fallback
|
|
715
|
+
}
|
|
716
|
+
};
|
|
700
717
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
+
//sendMessage function inside useMessageKit:
|
|
719
|
+
const sendMessage = async ({ text, media, courseId, accountId }) => {
|
|
720
|
+
const clientTimestamp = new Date().toISOString();
|
|
721
|
+
// Ensure crypto API is available, provide fallback
|
|
722
|
+
const tempId =
|
|
723
|
+
typeof crypto !== "undefined" && crypto.randomUUID
|
|
724
|
+
? crypto.randomUUID()
|
|
725
|
+
: `temp_${Date.now()}_${Math.random()}`;
|
|
726
|
+
const pendingKey = `pending_${courseId}_${accountId}`; // ALWAYS use this key for optimistic storage
|
|
727
|
+
|
|
728
|
+
// --- Prepare Optimistic Message ---
|
|
729
|
+
const optimisticMessage = {
|
|
730
|
+
tempId,
|
|
731
|
+
status: "pending",
|
|
732
|
+
courseId, // Keep for identification within pendingKey if needed
|
|
733
|
+
accountId, // Keep for identification within pendingKey if needed
|
|
734
|
+
text,
|
|
735
|
+
media,
|
|
736
|
+
created_at: clientTimestamp,
|
|
737
|
+
user_message: true, // Always true for optimistic messages
|
|
738
|
+
error: null,
|
|
718
739
|
};
|
|
719
740
|
|
|
720
|
-
// ---
|
|
741
|
+
// --- Store Optimistic Message using pendingKey ---
|
|
721
742
|
setState((prevState) => {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
}
|
|
743
|
+
const currentOptimistic = prevState.optimisticMessages[pendingKey] || [];
|
|
744
|
+
return {
|
|
745
|
+
...prevState,
|
|
746
|
+
optimisticMessages: {
|
|
747
|
+
...prevState.optimisticMessages,
|
|
748
|
+
[pendingKey]: [...currentOptimistic, optimisticMessage],
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
});
|
|
752
|
+
// --- End Optimistic Update ---
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
// --- API Call ---
|
|
756
|
+
let response;
|
|
757
|
+
const requestData = { text, media };
|
|
758
|
+
const requestParams = {
|
|
759
|
+
account_id: accountId,
|
|
760
|
+
course_id: courseId,
|
|
761
|
+
_account: selectedAccount.id,
|
|
762
|
+
};
|
|
730
763
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
764
|
+
if (selectedAccount.type.toLowerCase() === "enterprise") {
|
|
765
|
+
response = await request({
|
|
766
|
+
url: "/notify/v1/enterprise/chats/",
|
|
767
|
+
params: requestParams,
|
|
768
|
+
data: requestData,
|
|
769
|
+
method: "Post",
|
|
770
|
+
});
|
|
771
|
+
} else if (
|
|
772
|
+
String(selectedAccount.type).toLowerCase() === "instructor" &&
|
|
773
|
+
affiliateAccount
|
|
774
|
+
) {
|
|
775
|
+
response = await request({
|
|
776
|
+
url: `/notify/v1/instructor/${affiliateAccount}/chats/`,
|
|
777
|
+
params: requestParams,
|
|
778
|
+
data: requestData,
|
|
779
|
+
method: "Post",
|
|
780
|
+
});
|
|
781
|
+
} else if (selectedAccount.type.toLowerCase() === "personal") {
|
|
782
|
+
response = await request({
|
|
783
|
+
url: `/notify/v1/chats/`,
|
|
784
|
+
params: requestParams,
|
|
785
|
+
data: requestData,
|
|
786
|
+
method: "Post",
|
|
787
|
+
});
|
|
788
|
+
} else {
|
|
789
|
+
throw new Error(
|
|
790
|
+
"Unsupported account type or configuration for sending messages."
|
|
735
791
|
);
|
|
736
|
-
|
|
792
|
+
}
|
|
793
|
+
// --- End API Call ---
|
|
737
794
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const todayDateStringForMissing = "today"; // Assume API uses "today"
|
|
795
|
+
if (!response?.data) {
|
|
796
|
+
throw new Error("API request succeeded but returned no data.");
|
|
797
|
+
}
|
|
742
798
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
799
|
+
/** @type {SentMessageResponse} */
|
|
800
|
+
const sentMessageData = response.data;
|
|
801
|
+
const confirmedRoomId = sentMessageData.room; // The actual Room ID!
|
|
802
|
+
const confirmedMessageId = sentMessageData.id;
|
|
803
|
+
|
|
804
|
+
// --- Format Confirmed Message ---
|
|
805
|
+
/** @type {ChatMessage} */
|
|
806
|
+
const newChatMessage = {
|
|
807
|
+
id: confirmedMessageId,
|
|
808
|
+
text: sentMessageData.text,
|
|
809
|
+
media: sentMessageData.media, // Assuming structure compatibility
|
|
810
|
+
sender: {
|
|
811
|
+
// Sender info from server response
|
|
812
|
+
id: sentMessageData.sender.id,
|
|
813
|
+
first_name:
|
|
814
|
+
sentMessageData.sender.full_name || sentMessageData.sender.name,
|
|
815
|
+
},
|
|
816
|
+
is_read: sentMessageData.is_read,
|
|
817
|
+
is_delivered: sentMessageData.is_delivered,
|
|
818
|
+
created_at: sentMessageData.created_at,
|
|
819
|
+
user_message: sentMessageData.user_message, // Should be true from API
|
|
820
|
+
date: getLocalDateString(sentMessageData.created_at), // Use helper
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// --- Update State on Success ---
|
|
824
|
+
setState((prevState) => {
|
|
825
|
+
let updatedOptimisticMessages = { ...prevState.optimisticMessages };
|
|
826
|
+
const updatedChats = { ...prevState.chats };
|
|
827
|
+
let updatedRoomsByCourses = [...prevState.roomsByCourses];
|
|
828
|
+
|
|
829
|
+
// --- 1. Remove Optimistic Message from Pending List ---
|
|
830
|
+
let optimisticRemoved = false;
|
|
831
|
+
if (updatedOptimisticMessages[pendingKey]) {
|
|
832
|
+
const originalPendingList = updatedOptimisticMessages[pendingKey];
|
|
833
|
+
// Filter out the message by its temporary ID
|
|
834
|
+
const filteredList = originalPendingList.filter(
|
|
835
|
+
(msg) => msg.tempId !== tempId
|
|
746
836
|
);
|
|
747
837
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
messages: [...todayGroup.messages, newChatMessage],
|
|
757
|
-
};
|
|
758
|
-
updatedChatHistoryForMissingRoom.splice(
|
|
759
|
-
todayGroupIndexForMissing,
|
|
760
|
-
1,
|
|
761
|
-
updatedTodayGroup
|
|
762
|
-
);
|
|
838
|
+
if (filteredList.length < originalPendingList.length) {
|
|
839
|
+
// Check if removal occurred
|
|
840
|
+
optimisticRemoved = true;
|
|
841
|
+
if (filteredList.length > 0) {
|
|
842
|
+
updatedOptimisticMessages[pendingKey] = filteredList; // Update the list
|
|
843
|
+
} else {
|
|
844
|
+
delete updatedOptimisticMessages[pendingKey]; // Remove the key if list is empty
|
|
845
|
+
}
|
|
763
846
|
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
newTodayGroup,
|
|
771
|
-
...updatedChatHistoryForMissingRoom,
|
|
772
|
-
];
|
|
847
|
+
}
|
|
848
|
+
if (!optimisticRemoved) {
|
|
849
|
+
// Log if the message wasn't found (might indicate a race condition or logic issue)
|
|
850
|
+
console.warn(
|
|
851
|
+
`Optimistic message tempId ${tempId} not found in ${pendingKey} for removal upon success.`
|
|
852
|
+
);
|
|
773
853
|
}
|
|
774
854
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
// Return state with only chats updated, roomsByCourses will update after fetch
|
|
781
|
-
return { ...prevState, chats: updatedChatsOnly };
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// --- Room exists, proceed with updates for both chats and roomsByCourses ---
|
|
785
|
-
// --- 2a. Update chats state ---
|
|
786
|
-
const currentChatHistory = prevState.chats[roomId] || [];
|
|
787
|
-
let updatedChatHistory = [...currentChatHistory];
|
|
788
|
-
const todayDateString = "today"; // Assume API uses "today" for current day
|
|
855
|
+
// --- 2. Add Confirmed Message to Chats[roomId] ---
|
|
856
|
+
const messageDateLabel = getDisplayDateLabel(newChatMessage.date);
|
|
857
|
+
// Get current history for the confirmed room ID, default to empty array
|
|
858
|
+
const currentChatHistory = updatedChats[confirmedRoomId] || [];
|
|
859
|
+
let updatedChatHistory = [...currentChatHistory];
|
|
789
860
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
861
|
+
// Find if a group for this date already exists
|
|
862
|
+
let dateGroupIndex = updatedChatHistory.findIndex(
|
|
863
|
+
(group) => group.date === messageDateLabel
|
|
864
|
+
);
|
|
793
865
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
866
|
+
if (dateGroupIndex !== -1) {
|
|
867
|
+
// Group exists: Add message if it's not already there (e.g., from WebSocket echo)
|
|
868
|
+
const dateGroup = updatedChatHistory[dateGroupIndex];
|
|
869
|
+
if (!dateGroup.messages.some((msg) => msg.id === newChatMessage.id)) {
|
|
870
|
+
// Add and re-sort messages within the group
|
|
871
|
+
const updatedMessages = [
|
|
872
|
+
...dateGroup.messages,
|
|
873
|
+
newChatMessage,
|
|
874
|
+
].sort(
|
|
875
|
+
(a, b) =>
|
|
876
|
+
new Date(a.created_at).getTime() -
|
|
877
|
+
new Date(b.created_at).getTime()
|
|
878
|
+
);
|
|
879
|
+
updatedChatHistory[dateGroupIndex] = {
|
|
880
|
+
...dateGroup,
|
|
881
|
+
messages: updatedMessages,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
// Group doesn't exist: Create new group and add it
|
|
886
|
+
const newGroup = {
|
|
887
|
+
date: messageDateLabel,
|
|
888
|
+
dateString: newChatMessage.date, // Store YYYY-MM-DD for sorting
|
|
889
|
+
messages: [newChatMessage],
|
|
802
890
|
};
|
|
803
|
-
|
|
804
|
-
|
|
891
|
+
updatedChatHistory.push(newGroup);
|
|
892
|
+
// Sort the date groups themselves (e.g., "Today" first)
|
|
893
|
+
updatedChatHistory.sort(
|
|
894
|
+
(a, b) =>
|
|
895
|
+
getSortableDateValue(b.date, b.dateString) -
|
|
896
|
+
getSortableDateValue(a.date, a.dateString)
|
|
897
|
+
);
|
|
805
898
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
...prevState.chats,
|
|
818
|
-
[roomId]: updatedChatHistory,
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
// --- 2b. Update roomsByCourses state ---
|
|
822
|
-
const updatedRoomsByCourses = prevState.roomsByCourses.map(
|
|
823
|
-
(courseGroup) => {
|
|
899
|
+
// Update the chats object with the modified history for this room
|
|
900
|
+
updatedChats[confirmedRoomId] = updatedChatHistory;
|
|
901
|
+
|
|
902
|
+
// --- 3. Update Room List (roomsByCourses) ---
|
|
903
|
+
let roomExistsInState = false;
|
|
904
|
+
updatedRoomsByCourses = updatedRoomsByCourses.map((courseGroup) => {
|
|
905
|
+
// Check if this is the correct course group first
|
|
906
|
+
if (String(courseGroup.id) !== String(courseId)) {
|
|
907
|
+
return courseGroup;
|
|
908
|
+
}
|
|
909
|
+
// Find the room within this course group
|
|
824
910
|
const roomIndex = courseGroup.rooms.findIndex(
|
|
825
|
-
(room) => room.id ===
|
|
911
|
+
(room) => room.id === confirmedRoomId
|
|
826
912
|
);
|
|
827
913
|
if (roomIndex !== -1) {
|
|
828
|
-
//
|
|
914
|
+
roomExistsInState = true; // Mark room as found
|
|
829
915
|
const updatedRooms = [...courseGroup.rooms];
|
|
916
|
+
// Update the last message for the found room
|
|
830
917
|
updatedRooms[roomIndex] = {
|
|
831
918
|
...updatedRooms[roomIndex],
|
|
832
919
|
last_message: {
|
|
833
|
-
// Format according to LastMessage type
|
|
834
920
|
id: newChatMessage.id,
|
|
835
921
|
text: newChatMessage.text,
|
|
836
|
-
media: newChatMessage.media, //
|
|
922
|
+
media: newChatMessage.media, // Use the confirmed media details
|
|
837
923
|
created_at: newChatMessage.created_at,
|
|
838
924
|
},
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
// unread_count: updatedRooms[roomIndex].unread_count + (newChatMessage.user_message ? 0 : 1)
|
|
925
|
+
// Resetting unread count here might be incorrect if the user isn't viewing the room
|
|
926
|
+
// Unread count updates should primarily happen on receiving messages or explicit read actions
|
|
842
927
|
};
|
|
928
|
+
// Return the updated course group
|
|
843
929
|
return { ...courseGroup, rooms: updatedRooms };
|
|
844
930
|
}
|
|
845
|
-
|
|
931
|
+
// If room not found in this course group, return group unchanged
|
|
932
|
+
return courseGroup;
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
// --- 4. Handle Room Not Found / Refetch ---
|
|
936
|
+
// If the room's metadata wasn't in roomsByCourses, fetch the list again
|
|
937
|
+
if (!roomExistsInState) {
|
|
938
|
+
console.log(
|
|
939
|
+
`Room ${confirmedRoomId} metadata not found in state after sending message, refetching room list.`
|
|
940
|
+
);
|
|
941
|
+
// Use setTimeout to defer the call slightly, avoiding potential issues during render
|
|
942
|
+
setTimeout(() => getMessageRoomsByCourses(null, true), 0);
|
|
846
943
|
}
|
|
944
|
+
|
|
945
|
+
// --- 5. REMOVED Move Logic ---
|
|
946
|
+
|
|
947
|
+
// --- Return Final State ---
|
|
948
|
+
return {
|
|
949
|
+
...prevState,
|
|
950
|
+
chats: updatedChats,
|
|
951
|
+
optimisticMessages: updatedOptimisticMessages,
|
|
952
|
+
roomsByCourses: updatedRoomsByCourses,
|
|
953
|
+
};
|
|
954
|
+
});
|
|
955
|
+
} catch (error) {
|
|
956
|
+
// --- Update State on Failure ---
|
|
957
|
+
console.error("Failed to send message:", error);
|
|
958
|
+
setState((prevState) => {
|
|
959
|
+
const updatedOptimisticMessages = { ...prevState.optimisticMessages };
|
|
960
|
+
|
|
961
|
+
// Find the optimistic message ONLY in the PENDING list
|
|
962
|
+
if (updatedOptimisticMessages[pendingKey]) {
|
|
963
|
+
const messageIndex = updatedOptimisticMessages[pendingKey].findIndex(
|
|
964
|
+
(msg) => msg.tempId === tempId && msg.status === "pending" // Important: Only update if still pending
|
|
965
|
+
);
|
|
966
|
+
if (messageIndex !== -1) {
|
|
967
|
+
// Update status to failed and store error message
|
|
968
|
+
updatedOptimisticMessages[pendingKey][messageIndex] = {
|
|
969
|
+
...updatedOptimisticMessages[pendingKey][messageIndex],
|
|
970
|
+
status: "failed",
|
|
971
|
+
error: error?.message || "Unknown error", // Store simplified error
|
|
972
|
+
};
|
|
973
|
+
} else {
|
|
974
|
+
console.warn(
|
|
975
|
+
`Optimistic message tempId ${tempId} in ${pendingKey} not found or not pending for failure update.`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
} else {
|
|
979
|
+
console.warn(
|
|
980
|
+
`Pending key ${pendingKey} not found for failure update.`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Return state with updated optimistic message status
|
|
985
|
+
return {
|
|
986
|
+
...prevState,
|
|
987
|
+
optimisticMessages: updatedOptimisticMessages,
|
|
988
|
+
};
|
|
989
|
+
});
|
|
990
|
+
// Optionally re-throw or handle the error further (e.g., show notification)
|
|
991
|
+
}
|
|
992
|
+
}; // End sendMessage
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Retrieves combined confirmed and optimistic messages for a specific chat,
|
|
996
|
+
* sorted chronologically and grouped by date.
|
|
997
|
+
* Optimistic messages in the output will NOT have a 'sender' property.
|
|
998
|
+
*
|
|
999
|
+
* @param {string | null} roomId - The ID of the room (if known). Used ONLY for retrieving confirmed messages.
|
|
1000
|
+
* @param {string} courseId - The course ID associated with the chat. REQUIRED for finding optimistic messages.
|
|
1001
|
+
* @param {string} accountId - The account ID of the other participant. REQUIRED for finding optimistic messages.
|
|
1002
|
+
* @returns {MessagesByDate[]} An array of message groups. Confirmed messages will have a 'sender', optimistic messages will not.
|
|
1003
|
+
*/
|
|
1004
|
+
const getMessagesForRoom = useCallback(
|
|
1005
|
+
(roomId, courseId, accountId) => {
|
|
1006
|
+
// --- 1. Determine Keys ---
|
|
1007
|
+
const chatKey = roomId; // Key for confirmed messages (state.chats)
|
|
1008
|
+
const pendingKey = `pending_${courseId}_${accountId}`; // Key for ALL optimistic messages for this pair
|
|
1009
|
+
|
|
1010
|
+
// --- 2. Get Confirmed Messages (using roomId if provided) ---
|
|
1011
|
+
const confirmedDateGroups =
|
|
1012
|
+
chatKey && state.chats[chatKey] ? state.chats[chatKey] : [];
|
|
1013
|
+
// Confirmed messages already have the correct ChatMessage structure (including sender)
|
|
1014
|
+
const confirmedFlatMessages = confirmedDateGroups.flatMap(
|
|
1015
|
+
(group) => group.messages
|
|
847
1016
|
);
|
|
848
1017
|
|
|
849
|
-
// ---
|
|
850
|
-
|
|
851
|
-
...
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1018
|
+
// --- 3. Get Optimistic Messages (ONLY from pendingKey) ---
|
|
1019
|
+
const uniqueOptimisticMessages = state.optimisticMessages[pendingKey]
|
|
1020
|
+
? [...state.optimisticMessages[pendingKey]] // Get a copy to avoid mutation issues
|
|
1021
|
+
: [];
|
|
1022
|
+
|
|
1023
|
+
// --- 4. Adapt Optimistic Messages to a structure suitable for sorting/grouping ---
|
|
1024
|
+
// (Does NOT add 'sender')
|
|
1025
|
+
const adaptedOptimisticMessages = uniqueOptimisticMessages.map(
|
|
1026
|
+
(optMsg) => ({
|
|
1027
|
+
// Use tempId as the unique identifier
|
|
1028
|
+
id: optMsg.tempId,
|
|
1029
|
+
text: optMsg.text,
|
|
1030
|
+
media: optMsg.media,
|
|
1031
|
+
// NO SENDER FIELD on adapted optimistic messages
|
|
1032
|
+
is_read: false, // Implicitly false
|
|
1033
|
+
is_delivered: false, // Implicitly false
|
|
1034
|
+
created_at: optMsg.created_at, // Client timestamp
|
|
1035
|
+
user_message: optMsg.user_message, // Always true
|
|
1036
|
+
date: getLocalDateString(optMsg.created_at), // Use helper
|
|
1037
|
+
// Add optimistic-specific fields for UI:
|
|
1038
|
+
status: optMsg.status, // 'pending' or 'failed'
|
|
1039
|
+
error: optMsg.error, // Error message if failed
|
|
1040
|
+
tempId: optMsg.tempId, // Keep tempId for keys or retry logic
|
|
1041
|
+
})
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
// --- 5. Combine and Sort All Messages ---
|
|
1045
|
+
// The combined list contains confirmed ChatMessages (with sender)
|
|
1046
|
+
// and adapted optimistic messages (without sender)
|
|
1047
|
+
const combinedFlatMessages = [
|
|
1048
|
+
...confirmedFlatMessages,
|
|
1049
|
+
...adaptedOptimisticMessages,
|
|
1050
|
+
];
|
|
1051
|
+
combinedFlatMessages.sort((a, b) => {
|
|
1052
|
+
// Sort primarily by creation timestamp using getTime() for reliable number comparison
|
|
1053
|
+
const dateA = new Date(a.created_at);
|
|
1054
|
+
const dateB = new Date(b.created_at);
|
|
1055
|
+
const timeA = !isNaN(dateA.getTime()) ? dateA.getTime() : 0; // Handle potential invalid dates
|
|
1056
|
+
const timeB = !isNaN(dateB.getTime()) ? dateB.getTime() : 0;
|
|
1057
|
+
|
|
1058
|
+
if (timeA !== timeB) {
|
|
1059
|
+
return timeA - timeB; // Ascending time order
|
|
1060
|
+
}
|
|
1061
|
+
// If timestamps are identical (rare), maintain relative order or rely on stable sort.
|
|
1062
|
+
return 0;
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// --- 6. Regroup Sorted Messages by Date ---
|
|
1066
|
+
/** @type {Record<string, { date: string; dateString: string; messages: any[] }>} */
|
|
1067
|
+
const groupedMessages = {};
|
|
1068
|
+
|
|
1069
|
+
combinedFlatMessages.forEach((message) => {
|
|
1070
|
+
// Use the pre-calculated date string (YYYY-MM-DD)
|
|
1071
|
+
const dateString = message.date;
|
|
1072
|
+
if (!dateString) {
|
|
1073
|
+
// Safety check
|
|
1074
|
+
console.warn("Message missing date string for grouping:", message);
|
|
1075
|
+
return; // Skip messages without a valid date string
|
|
1076
|
+
}
|
|
1077
|
+
// Get the display label ("Today", "Yesterday", "Apr 06")
|
|
1078
|
+
const displayLabel = getDisplayDateLabel(dateString);
|
|
1079
|
+
|
|
1080
|
+
// Initialize group if it doesn't exist
|
|
1081
|
+
if (!groupedMessages[displayLabel]) {
|
|
1082
|
+
groupedMessages[displayLabel] = {
|
|
1083
|
+
date: displayLabel, // The display label
|
|
1084
|
+
dateString: dateString, // The underlying YYYY-MM-DD string
|
|
1085
|
+
messages: [],
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
// Add the message (with or without sender property) to the correct group
|
|
1089
|
+
groupedMessages[displayLabel].messages.push(message);
|
|
1090
|
+
});
|
|
856
1091
|
|
|
857
|
-
|
|
858
|
-
|
|
1092
|
+
// Convert the grouped object back into an array of groups
|
|
1093
|
+
const finalGroupedArray = Object.values(groupedMessages)
|
|
1094
|
+
// Sort the groups themselves (e.g., Today first, then Yesterday, then older dates)
|
|
1095
|
+
.sort(
|
|
1096
|
+
(a, b) =>
|
|
1097
|
+
getSortableDateValue(b.date, b.dateString) -
|
|
1098
|
+
getSortableDateValue(a.date, a.dateString)
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
// Ensure messages *within* each final group are sorted chronologically
|
|
1102
|
+
// This should be guaranteed by step 5, but double-checking is safe.
|
|
1103
|
+
finalGroupedArray.forEach((group) => {
|
|
1104
|
+
group.messages.sort(
|
|
1105
|
+
(a, b) =>
|
|
1106
|
+
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
|
1107
|
+
);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// Return the final array structure expected by the UI
|
|
1111
|
+
return finalGroupedArray; // Array of { date: string, dateString: string, messages: any[] }
|
|
1112
|
+
},
|
|
1113
|
+
[state.chats, state.optimisticMessages]
|
|
1114
|
+
); // Dependencies
|
|
1115
|
+
|
|
1116
|
+
// Helper to get a sortable value from a display date label
|
|
1117
|
+
const getSortableDateValue = (displayLabel, dateString) => {
|
|
1118
|
+
if (!dateString) return 0; // Handle undefined/null dateString
|
|
1119
|
+
if (displayLabel === "Today") return 2; // Highest priority
|
|
1120
|
+
if (displayLabel === "Yesterday") return 1; // Next highest
|
|
1121
|
+
// For specific dates, return the negative timestamp for descending sort
|
|
1122
|
+
try {
|
|
1123
|
+
const date = new Date(dateString + "T00:00:00Z"); // Use UTC base
|
|
1124
|
+
if (isNaN(date.getTime())) return 0; // Low priority if invalid
|
|
1125
|
+
// Older dates have smaller negative numbers (larger positive time), leading to descending sort
|
|
1126
|
+
return -date.getTime();
|
|
1127
|
+
} catch {
|
|
1128
|
+
return 0; // Fallback for errors
|
|
1129
|
+
}
|
|
859
1130
|
};
|
|
860
1131
|
|
|
861
1132
|
const pinRoom = async (roomId) => {
|
|
@@ -1058,14 +1329,17 @@ const useMessageKit = (/*affiliatesActive*/) => {
|
|
|
1058
1329
|
// latestMessageId: "57e31128214a4269be8b9e3bb18495c4",
|
|
1059
1330
|
// roomId: "5be9b48281ac4c2885d3b719654ed59d",
|
|
1060
1331
|
// });
|
|
1061
|
-
// sendMessage({
|
|
1062
|
-
// text: "automated message",
|
|
1332
|
+
// await sendMessage({
|
|
1333
|
+
// text: "automated message v2",
|
|
1063
1334
|
// courseId: "878",
|
|
1064
|
-
// accountId: "
|
|
1335
|
+
// accountId: "24dbaeede1",
|
|
1065
1336
|
// });
|
|
1066
1337
|
})();
|
|
1067
1338
|
}, []); // Rerun when selectedAccount changes
|
|
1068
|
-
|
|
1339
|
+
// console.log(
|
|
1340
|
+
// getMessagesForRoom("5be9b48281ac4c2885d3b719654ed59d", "878", "24dbaeede1"),
|
|
1341
|
+
// "hold versions"
|
|
1342
|
+
// );
|
|
1069
1343
|
// --- Handle Incoming WebSocket Messages ---
|
|
1070
1344
|
useEffect(() => {
|
|
1071
1345
|
const socket = new WebSocket(buildSocketUrl());
|
|
@@ -1275,6 +1549,7 @@ const useMessageKit = (/*affiliatesActive*/) => {
|
|
|
1275
1549
|
reportChat,
|
|
1276
1550
|
// Helper method to check if a specific room is loading
|
|
1277
1551
|
isRoomLoading: (roomId) => state.loadingRoomIds.has(roomId),
|
|
1552
|
+
getMessagesForRoom,
|
|
1278
1553
|
};
|
|
1279
1554
|
};
|
|
1280
1555
|
|