l-min-components 1.6.1263 → 1.6.1265

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.6.1263",
3
+ "version": "1.6.1265",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/assets",
@@ -226,6 +226,16 @@ import sound from "./new-notification-7-210334.mp3";
226
226
  * @property {any | null} error - Stores error info if status is 'failed'.
227
227
  */
228
228
 
229
+ /**
230
+ * Represents an item waiting in the message sending queue.
231
+ * @typedef {object} QueueItem
232
+ * @property {string} tempId - The temporary ID linking to the optimistic message.
233
+ * @property {string} courseId - The course ID for the API call.
234
+ * @property {string} accountId - The account ID for the API call.
235
+ * @property {string | null} text - The message text content.
236
+ * @property {any | null} media - The media data (e.g., File object, FormData details). Needs proper handling before API call.
237
+ */
238
+
229
239
  const useMessageKit = (/*affiliatesActive*/) => {
230
240
  const cookieGrabber = (key = "") => {
231
241
  const cookies = document.cookie;
@@ -304,6 +314,16 @@ const useMessageKit = (/*affiliatesActive*/) => {
304
314
  * @type {Record<string, OptimisticMessage[]>}
305
315
  */
306
316
  optimisticMessages: {},
317
+ /**
318
+ * Queue of messages waiting to be sent via API.
319
+ * @type {QueueItem[]}
320
+ */
321
+ messageQueue: [],
322
+ /**
323
+ * Flag indicating if the queue processor is currently sending a message.
324
+ * @type {boolean}
325
+ */
326
+ isProcessingQueue: false,
307
327
  });
308
328
 
309
329
  // State for rooms that should be automatically marked as read
@@ -651,22 +671,18 @@ const useMessageKit = (/*affiliatesActive*/) => {
651
671
  }, []);
652
672
  // --- End functions to manage auto-read rooms ---
653
673
 
654
- // Helper to get YYYY-MM-DD (ensure consistency)
674
+ // Helper to get YYYY-MM-DD - Useful for potential fallback or other date needs
655
675
  const getLocalDateString = (isoTimestamp) => {
656
676
  if (!isoTimestamp) return new Date().toISOString().split("T")[0];
657
677
  try {
658
678
  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
- }
679
+ if (isNaN(date.getTime())) throw new Error("Invalid date object");
663
680
  const year = date.getFullYear();
664
681
  const month = String(date.getMonth() + 1).padStart(2, "0");
665
682
  const day = String(date.getDate()).padStart(2, "0");
666
683
  return `${year}-${month}-${day}`;
667
684
  } catch (e) {
668
685
  console.error("Error parsing date:", isoTimestamp, e);
669
- // Fallback to current date string
670
686
  const today = new Date();
671
687
  const year = today.getFullYear();
672
688
  const month = String(today.getMonth() + 1).padStart(2, "0");
@@ -675,321 +691,405 @@ const useMessageKit = (/*affiliatesActive*/) => {
675
691
  }
676
692
  };
677
693
 
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";
682
-
683
- const today = new Date();
684
- const yesterday = new Date(today);
685
- yesterday.setDate(today.getDate() - 1);
694
+ // *** UPDATED getDisplayDateLabel ***
695
+ // Now accepts the full ISO timestamp string
696
+ const getDisplayDateLabel = (createdAtISOString) => {
697
+ if (!createdAtISOString) return "Unknown Date";
686
698
 
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
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
700
+ const messageDate = new Date(createdAtISOString);
701
+ // Ensure the date parsed correctly
704
702
  if (isNaN(messageDate.getTime())) {
705
- return dateString; // Fallback to YYYY-MM-DD if parsing failed
703
+ console.warn(
704
+ "getDisplayDateLabel: Invalid date parsed from",
705
+ createdAtISOString
706
+ );
707
+ return createdAtISOString.split("T")[0] || "Invalid Date"; // Basic fallback
708
+ }
709
+
710
+ const now = new Date();
711
+
712
+ // Get midnight today in the browser's local time
713
+ const todayStart = new Date(
714
+ now.getFullYear(),
715
+ now.getMonth(),
716
+ now.getDate()
717
+ );
718
+ // Get midnight yesterday in the browser's local time
719
+ const yesterdayStart = new Date(todayStart);
720
+ yesterdayStart.setDate(todayStart.getDate() - 1);
721
+ // Get midnight tomorrow (needed for the upper bound of "today")
722
+ const tomorrowStart = new Date(todayStart);
723
+ tomorrowStart.setDate(todayStart.getDate() + 1);
724
+
725
+ // Compare the message's timestamp against local date boundaries
726
+ if (messageDate >= todayStart && messageDate < tomorrowStart) {
727
+ return "Today";
728
+ } else if (messageDate >= yesterdayStart && messageDate < todayStart) {
729
+ return "Yesterday";
730
+ } else {
731
+ // Format other dates using locale-specific representation
732
+ let options = { month: "short", day: "numeric" };
733
+ // Optionally add year if it's not the current year
734
+ if (messageDate.getFullYear() !== now.getFullYear()) {
735
+ options.year = "numeric";
736
+ }
737
+ // 'undefined' uses the browser's default locale
738
+ return messageDate.toLocaleDateString(undefined, options);
706
739
  }
707
- return messageDate.toLocaleDateString("en-US", {
708
- timeZone: "UTC",
709
- month: "short",
710
- day: "numeric",
711
- });
712
740
  } catch (e) {
713
- console.error("Error formatting date label for:", dateString, e);
714
- return dateString; // Fallback
741
+ console.error(
742
+ "Error formatting display date label for:",
743
+ createdAtISOString,
744
+ e
745
+ );
746
+ return createdAtISOString.split("T")[0] || "Error Date"; // Basic fallback
715
747
  }
716
748
  };
717
749
 
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,
739
- };
750
+ // *** NEW getSortableTimestampForGroup ***
751
+ // Helper to get a reliable timestamp for sorting date groups
752
+ const getSortableTimestampForGroup = (group) => {
753
+ // Use the timestamp of the first message in the group for sorting
754
+ // Assumes messages within the group are sorted ascending by created_at
755
+ if (group?.messages?.length > 0 && group.messages[0]?.created_at) {
756
+ try {
757
+ const date = new Date(group.messages[0].created_at);
758
+ if (!isNaN(date.getTime())) {
759
+ // Return timestamp representing midnight of that day in local time for group sorting
760
+ const groupDateStart = new Date(
761
+ date.getFullYear(),
762
+ date.getMonth(),
763
+ date.getDate()
764
+ );
765
+ return groupDateStart.getTime(); // Use positive timestamp
766
+ }
767
+ } catch {
768
+ /* ignore errors, fallback below */
769
+ }
770
+ }
771
+ // Fallback if no valid message/timestamp found
772
+ return 0;
773
+ };
740
774
 
741
- // --- Store Optimistic Message using pendingKey ---
742
- setState((prevState) => {
743
- const currentOptimistic = prevState.optimisticMessages[pendingKey] || [];
744
- return {
745
- ...prevState,
746
- optimisticMessages: {
747
- ...prevState.optimisticMessages,
748
- [pendingKey]: [...currentOptimistic, optimisticMessage],
749
- },
775
+ // This function now adds the message optimistically and places it in the queue.
776
+ const sendMessage = useCallback(
777
+ ({ text, media, courseId, accountId }) => {
778
+ const clientTimestamp = new Date().toISOString();
779
+ const tempId =
780
+ typeof crypto !== "undefined" && crypto.randomUUID
781
+ ? crypto.randomUUID()
782
+ : `temp_${Date.now()}_${Math.random()}`;
783
+ const pendingKey = `pending_${courseId}_${accountId}`;
784
+
785
+ // --- Prepare Optimistic Message ---
786
+ const optimisticMessage = {
787
+ tempId,
788
+ status: "pending", // Initially pending
789
+ courseId,
790
+ accountId,
791
+ text,
792
+ media, // Store original media reference for potential UI display needs
793
+ created_at: clientTimestamp,
794
+ user_message: true, // Always true
795
+ error: null,
750
796
  };
751
- });
752
- // --- End Optimistic Update ---
753
797
 
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,
798
+ // --- Prepare Queue Item ---
799
+ const queueItem = {
800
+ tempId,
801
+ courseId,
802
+ accountId,
803
+ text,
804
+ media, // Pass the media data to the queue item for sending
762
805
  };
763
806
 
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."
791
- );
792
- }
793
- // --- End API Call ---
807
+ // --- Add Optimistic Message AND Queue Item to State ---
808
+ setState((prevState) => {
809
+ // Add optimistic message for UI update
810
+ const currentOptimistic =
811
+ prevState.optimisticMessages[pendingKey] || [];
812
+ const updatedOptimistic = {
813
+ ...prevState.optimisticMessages,
814
+ [pendingKey]: [...currentOptimistic, optimisticMessage],
815
+ };
794
816
 
795
- if (!response?.data) {
796
- throw new Error("API request succeeded but returned no data.");
797
- }
817
+ // Add item to the sending queue
818
+ const updatedQueue = [...prevState.messageQueue, queueItem];
798
819
 
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
- };
820
+ return {
821
+ ...prevState,
822
+ optimisticMessages: updatedOptimistic,
823
+ messageQueue: updatedQueue,
824
+ };
825
+ });
826
+ // NOTE: The actual API call is deferred to the queue processor effect.
827
+ },
828
+ [setState]
829
+ ); // Dependency: setState
822
830
 
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
836
- );
831
+ // Effect to process the message queue sequentially
832
+ useEffect(() => {
833
+ // Exit if already processing or queue is empty
834
+ if (state.isProcessingQueue || state.messageQueue.length === 0) {
835
+ return;
836
+ }
837
837
 
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
- }
846
- }
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
- );
853
- }
838
+ // --- Start Processing ---
839
+ setState((prevState) => ({ ...prevState, isProcessingQueue: true }));
854
840
 
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];
841
+ // Get the next message from the front of the queue
842
+ const messageToSend = state.messageQueue[0];
843
+ const { tempId, courseId, accountId, text, media } = messageToSend;
844
+ const pendingKey = `pending_${courseId}_${accountId}`; // Reconstruct key
860
845
 
861
- // Find if a group for this date already exists
862
- let dateGroupIndex = updatedChatHistory.findIndex(
863
- (group) => group.date === messageDateLabel
864
- );
846
+ // Define the async task for sending this message
847
+ const processMessage = async () => {
848
+ try {
849
+ // --- Make API Call ---
850
+ let response;
851
+ // TODO: Handle media preparation (e.g., check if 'media' is File, create FormData)
852
+ // This example assumes 'media' is directly usable or requires minimal processing
853
+ const requestData = { text, media };
854
+ const requestParams = {
855
+ account_id: accountId,
856
+ course_id: courseId,
857
+ _account: selectedAccount.id,
858
+ };
865
859
 
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
- }
860
+ // (Use the same API call logic as before)
861
+ if (selectedAccount.type.toLowerCase() === "enterprise") {
862
+ response = await request({
863
+ url: "/notify/v1/enterprise/chats/",
864
+ params: requestParams,
865
+ data: requestData,
866
+ method: "Post",
867
+ });
868
+ } else if (
869
+ String(selectedAccount.type).toLowerCase() === "instructor" &&
870
+ affiliateAccount
871
+ ) {
872
+ response = await request({
873
+ url: `/notify/v1/instructor/${affiliateAccount}/chats/`,
874
+ params: requestParams,
875
+ data: requestData,
876
+ method: "Post",
877
+ });
878
+ } else if (selectedAccount.type.toLowerCase() === "personal") {
879
+ response = await request({
880
+ url: `/notify/v1/chats/`,
881
+ params: requestParams,
882
+ data: requestData,
883
+ method: "Post",
884
+ });
884
885
  } 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],
890
- };
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)
886
+ throw new Error(
887
+ "Queue Processor: Unsupported account type or configuration."
897
888
  );
898
889
  }
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
910
- const roomIndex = courseGroup.rooms.findIndex(
911
- (room) => room.id === confirmedRoomId
912
- );
913
- if (roomIndex !== -1) {
914
- roomExistsInState = true; // Mark room as found
915
- const updatedRooms = [...courseGroup.rooms];
916
- // Update the last message for the found room
917
- updatedRooms[roomIndex] = {
918
- ...updatedRooms[roomIndex],
919
- last_message: {
920
- id: newChatMessage.id,
921
- text: newChatMessage.text,
922
- media: newChatMessage.media, // Use the confirmed media details
923
- created_at: newChatMessage.created_at,
924
- },
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
927
- };
928
- // Return the updated course group
929
- return { ...courseGroup, rooms: updatedRooms };
930
- }
931
- // If room not found in this course group, return group unchanged
932
- return courseGroup;
933
- });
890
+ // --- End API Call ---
934
891
 
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.`
892
+ if (!response?.data) {
893
+ throw new Error(
894
+ "Queue Processor: API request succeeded but returned no data."
940
895
  );
941
- // Use setTimeout to defer the call slightly, avoiding potential issues during render
942
- setTimeout(() => getMessageRoomsByCourses(null, true), 0);
943
896
  }
944
897
 
945
- // --- 5. REMOVED Move Logic ---
898
+ // --- Handle SUCCESS ---
899
+ /** @type {SentMessageResponse} */
900
+ const sentMessageData = response.data;
901
+ const confirmedRoomId = sentMessageData.room;
902
+ const confirmedMessageId = sentMessageData.id;
946
903
 
947
- // --- Return Final State ---
948
- return {
949
- ...prevState,
950
- chats: updatedChats,
951
- optimisticMessages: updatedOptimisticMessages,
952
- roomsByCourses: updatedRoomsByCourses,
904
+ // Format confirmed message
905
+ /** @type {ChatMessage} */
906
+ const newChatMessage = {
907
+ id: confirmedMessageId,
908
+ text: sentMessageData.text,
909
+ media: sentMessageData.media,
910
+ sender: {
911
+ id: sentMessageData.sender.id,
912
+ first_name:
913
+ sentMessageData.sender.full_name || sentMessageData.sender.name,
914
+ },
915
+ is_read: sentMessageData.is_read,
916
+ is_delivered: sentMessageData.is_delivered,
917
+ created_at: sentMessageData.created_at,
918
+ user_message: sentMessageData.user_message,
919
+ date: getLocalDateString(sentMessageData.created_at), // Still useful for basic date info if needed
953
920
  };
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
921
 
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
922
+ // Update state atomically after success
923
+ setState((prevState) => {
924
+ // 1. Remove optimistic message
925
+ let updatedOptimisticMessages = { ...prevState.optimisticMessages };
926
+ if (updatedOptimisticMessages[pendingKey]) {
927
+ const filteredList = updatedOptimisticMessages[pendingKey].filter(
928
+ (msg) => msg.tempId !== tempId
929
+ );
930
+ if (filteredList.length === 0)
931
+ delete updatedOptimisticMessages[pendingKey];
932
+ else updatedOptimisticMessages[pendingKey] = filteredList;
933
+ }
934
+
935
+ // 2. Add confirmed message to chats
936
+ const updatedChats = { ...prevState.chats };
937
+ const messageDateLabel = getDisplayDateLabel(
938
+ newChatMessage.created_at
939
+ ); // Use corrected helper
940
+ const currentChatHistory = updatedChats[confirmedRoomId] || [];
941
+ let updatedChatHistory = [...currentChatHistory];
942
+ let dateGroupIndex = updatedChatHistory.findIndex(
943
+ (g) => g.date === messageDateLabel
965
944
  );
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
- };
945
+
946
+ if (dateGroupIndex !== -1) {
947
+ // Add to existing group if not duplicate
948
+ if (
949
+ !updatedChatHistory[dateGroupIndex].messages.some(
950
+ (m) => m.id === newChatMessage.id
951
+ )
952
+ ) {
953
+ updatedChatHistory[dateGroupIndex].messages = [
954
+ ...updatedChatHistory[dateGroupIndex].messages,
955
+ newChatMessage,
956
+ ].sort(
957
+ (a, b) =>
958
+ new Date(a.created_at).getTime() -
959
+ new Date(b.created_at).getTime()
960
+ );
961
+ }
962
+ } else {
963
+ // Create new group
964
+ updatedChatHistory.push({
965
+ date: messageDateLabel,
966
+ messages: [newChatMessage],
967
+ });
968
+ // Sort groups using helper function
969
+ updatedChatHistory.sort(
970
+ (a, b) =>
971
+ getSortableTimestampForGroup(b) -
972
+ getSortableTimestampForGroup(a)
973
+ );
974
+ }
975
+ updatedChats[confirmedRoomId] = updatedChatHistory;
976
+
977
+ // 3. Update roomsByCourses last_message (if room metadata exists)
978
+ let updatedRoomsByCourses = [...prevState.roomsByCourses];
979
+ let roomExists = false;
980
+ updatedRoomsByCourses = updatedRoomsByCourses.map((cg) => {
981
+ if (String(cg.id) !== String(courseId)) return cg;
982
+ const roomIdx = cg.rooms.findIndex((r) => r.id === confirmedRoomId);
983
+ if (roomIdx !== -1) {
984
+ roomExists = true;
985
+ // Create a new room object to ensure immutability
986
+ const updatedRoom = {
987
+ ...cg.rooms[roomIdx],
988
+ last_message: {
989
+ id: newChatMessage.id,
990
+ text: newChatMessage.text,
991
+ media: newChatMessage.media,
992
+ created_at: newChatMessage.created_at,
993
+ },
994
+ };
995
+ // Create a new rooms array for the course group
996
+ const updatedRooms = [...cg.rooms];
997
+ updatedRooms[roomIdx] = updatedRoom;
998
+ // Return a new course group object
999
+ return { ...cg, rooms: updatedRooms };
1000
+ }
1001
+ return cg;
1002
+ });
1003
+ // Trigger refetch if room metadata wasn't found (handled outside setState)
1004
+ if (!roomExists) {
1005
+ setTimeout(() => getMessageRoomsByCourses(null, true), 0);
1006
+ }
1007
+
1008
+ // 4. Remove processed item from queue (immutable update)
1009
+ const updatedQueue = prevState.messageQueue.slice(1);
1010
+
1011
+ // Return the combined state updates
1012
+ return {
1013
+ ...prevState,
1014
+ optimisticMessages: updatedOptimisticMessages,
1015
+ chats: updatedChats,
1016
+ roomsByCourses: updatedRoomsByCourses,
1017
+ messageQueue: updatedQueue,
1018
+ // isProcessingQueue reset happens in 'finally' block
1019
+ };
1020
+ });
1021
+ } catch (error) {
1022
+ // --- Handle FAILURE ---
1023
+ console.error(
1024
+ `Queue Processor: Failed to send message tempId ${tempId}:`,
1025
+ error
1026
+ );
1027
+ setState((prevState) => {
1028
+ // 1. Update optimistic message status to 'failed'
1029
+ let updatedOptimisticMessages = { ...prevState.optimisticMessages };
1030
+ if (updatedOptimisticMessages[pendingKey]) {
1031
+ const messageIndex = updatedOptimisticMessages[
1032
+ pendingKey
1033
+ ].findIndex((msg) => msg.tempId === tempId);
1034
+ // Only update if found and still pending (might have been handled differently elsewhere?)
1035
+ if (
1036
+ messageIndex !== -1 &&
1037
+ updatedOptimisticMessages[pendingKey][messageIndex].status ===
1038
+ "pending"
1039
+ ) {
1040
+ // Create new message object for immutability
1041
+ const failedMessage = {
1042
+ ...updatedOptimisticMessages[pendingKey][messageIndex],
1043
+ status: "failed",
1044
+ error: error?.message || "Unknown error",
1045
+ };
1046
+ // Create new array for the pending key
1047
+ const updatedPendingList = [
1048
+ ...updatedOptimisticMessages[pendingKey],
1049
+ ];
1050
+ updatedPendingList[messageIndex] = failedMessage;
1051
+ updatedOptimisticMessages[pendingKey] = updatedPendingList;
1052
+ } else {
1053
+ console.warn(
1054
+ `Queue Processor: Optimistic message tempId ${tempId} in ${pendingKey} not found or not pending for failure update.`
1055
+ );
1056
+ }
973
1057
  } else {
974
1058
  console.warn(
975
- `Optimistic message tempId ${tempId} in ${pendingKey} not found or not pending for failure update.`
1059
+ `Queue Processor: Pending key ${pendingKey} not found for failure update.`
976
1060
  );
977
1061
  }
978
- } else {
979
- console.warn(
980
- `Pending key ${pendingKey} not found for failure update.`
981
- );
982
- }
983
1062
 
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
1063
+ // 2. Remove failed item from queue (immutable update)
1064
+ const updatedQueue = prevState.messageQueue.slice(1);
1065
+
1066
+ return {
1067
+ ...prevState,
1068
+ optimisticMessages: updatedOptimisticMessages,
1069
+ messageQueue: updatedQueue,
1070
+ // isProcessingQueue reset happens in 'finally' block
1071
+ };
1072
+ });
1073
+ } finally {
1074
+ // --- Finish Processing ---
1075
+ // Always ensure processing flag is reset, even on error
1076
+ setState((prevState) => ({ ...prevState, isProcessingQueue: false }));
1077
+ }
1078
+ };
1079
+
1080
+ // Execute the async sending logic
1081
+ processMessage();
1082
+
1083
+ // Dependencies for the effect: react when queue or processing status changes
1084
+ // Also depend on selectedAccount and affiliateAccount for API calls within the processor
1085
+ }, [
1086
+ state.isProcessingQueue,
1087
+ state.messageQueue,
1088
+ // request,
1089
+ selectedAccount,
1090
+ affiliateAccount,
1091
+ // getMessageRoomsByCourses,
1092
+ ]);
993
1093
 
994
1094
  /**
995
1095
  * Retrieves combined confirmed and optimistic messages for a specific chat,
@@ -1017,23 +1117,22 @@ const useMessageKit = (/*affiliatesActive*/) => {
1017
1117
 
1018
1118
  // --- 3. Get Optimistic Messages (ONLY from pendingKey) ---
1019
1119
  const uniqueOptimisticMessages = state.optimisticMessages[pendingKey]
1020
- ? [...state.optimisticMessages[pendingKey]] // Get a copy to avoid mutation issues
1120
+ ? [...state.optimisticMessages[pendingKey]] // Get a copy
1021
1121
  : [];
1022
1122
 
1023
1123
  // --- 4. Adapt Optimistic Messages to a structure suitable for sorting/grouping ---
1024
1124
  // (Does NOT add 'sender')
1025
1125
  const adaptedOptimisticMessages = uniqueOptimisticMessages.map(
1026
1126
  (optMsg) => ({
1027
- // Use tempId as the unique identifier
1028
1127
  id: optMsg.tempId,
1029
1128
  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
1129
+ media: optMsg.media, // The original media reference stored in optimistic state
1130
+ // NO SENDER FIELD added here
1131
+ is_read: false,
1132
+ is_delivered: false,
1034
1133
  created_at: optMsg.created_at, // Client timestamp
1035
1134
  user_message: optMsg.user_message, // Always true
1036
- date: getLocalDateString(optMsg.created_at), // Use helper
1135
+ date: getLocalDateString(optMsg.created_at), // Use helper for basic date info
1037
1136
  // Add optimistic-specific fields for UI:
1038
1137
  status: optMsg.status, // 'pending' or 'failed'
1039
1138
  error: optMsg.error, // Error message if failed
@@ -1042,64 +1141,50 @@ const useMessageKit = (/*affiliatesActive*/) => {
1042
1141
  );
1043
1142
 
1044
1143
  // --- 5. Combine and Sort All Messages ---
1045
- // The combined list contains confirmed ChatMessages (with sender)
1046
- // and adapted optimistic messages (without sender)
1047
1144
  const combinedFlatMessages = [
1048
1145
  ...confirmedFlatMessages,
1049
1146
  ...adaptedOptimisticMessages,
1050
1147
  ];
1051
1148
  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;
1149
+ const timeA = new Date(a.created_at).getTime();
1150
+ const timeB = new Date(b.created_at).getTime();
1151
+ // Handle potential NaN from invalid dates
1152
+ return (isNaN(timeA) ? 0 : timeA) - (isNaN(timeB) ? 0 : timeB);
1063
1153
  });
1064
1154
 
1065
1155
  // --- 6. Regroup Sorted Messages by Date ---
1066
- /** @type {Record<string, { date: string; dateString: string; messages: any[] }>} */
1156
+ /** @type {Record<string, { date: string; messages: any[] }>} */
1067
1157
  const groupedMessages = {};
1068
1158
 
1069
1159
  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
1160
+ // Use the corrected helper with the full timestamp
1161
+ const displayLabel = getDisplayDateLabel(message.created_at);
1162
+ if (
1163
+ !displayLabel ||
1164
+ displayLabel === "Unknown Date" ||
1165
+ displayLabel === "Invalid Date" ||
1166
+ displayLabel === "Error Date"
1167
+ ) {
1168
+ console.warn("Skipping message due to invalid date label:", message);
1169
+ return;
1076
1170
  }
1077
- // Get the display label ("Today", "Yesterday", "Apr 06")
1078
- const displayLabel = getDisplayDateLabel(dateString);
1079
1171
 
1080
- // Initialize group if it doesn't exist
1081
1172
  if (!groupedMessages[displayLabel]) {
1082
1173
  groupedMessages[displayLabel] = {
1083
- date: displayLabel, // The display label
1084
- dateString: dateString, // The underlying YYYY-MM-DD string
1174
+ date: displayLabel, // The display label (e.g., "Today")
1085
1175
  messages: [],
1086
1176
  };
1087
1177
  }
1088
- // Add the message (with or without sender property) to the correct group
1089
1178
  groupedMessages[displayLabel].messages.push(message);
1090
1179
  });
1091
1180
 
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
- );
1181
+ // Convert grouped object back to array and sort the groups using timestamps
1182
+ const finalGroupedArray = Object.values(groupedMessages).sort(
1183
+ (a, b) =>
1184
+ getSortableTimestampForGroup(b) - getSortableTimestampForGroup(a)
1185
+ ); // Descending date order
1100
1186
 
1101
- // Ensure messages *within* each final group are sorted chronologically
1102
- // This should be guaranteed by step 5, but double-checking is safe.
1187
+ // Ensure messages *within* each final group are sorted (redundant but safe)
1103
1188
  finalGroupedArray.forEach((group) => {
1104
1189
  group.messages.sort(
1105
1190
  (a, b) =>
@@ -1107,27 +1192,26 @@ const useMessageKit = (/*affiliatesActive*/) => {
1107
1192
  );
1108
1193
  });
1109
1194
 
1110
- // Return the final array structure expected by the UI
1111
- return finalGroupedArray; // Array of { date: string, dateString: string, messages: any[] }
1195
+ return finalGroupedArray; // Array of { date: string, messages: any[] }
1112
1196
  },
1113
1197
  [state.chats, state.optimisticMessages]
1114
- ); // Dependencies
1198
+ ); // Dependencies: only needs state parts used
1115
1199
 
1116
1200
  // 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
- }
1130
- };
1201
+ // const getSortableDateValue = (displayLabel, dateString) => {
1202
+ // if (!dateString) return 0; // Handle undefined/null dateString
1203
+ // if (displayLabel === "Today") return 2; // Highest priority
1204
+ // if (displayLabel === "Yesterday") return 1; // Next highest
1205
+ // // For specific dates, return the negative timestamp for descending sort
1206
+ // try {
1207
+ // const date = new Date(dateString + "T00:00:00Z"); // Use UTC base
1208
+ // if (isNaN(date.getTime())) return 0; // Low priority if invalid
1209
+ // // Older dates have smaller negative numbers (larger positive time), leading to descending sort
1210
+ // return -date.getTime();
1211
+ // } catch {
1212
+ // return 0; // Fallback for errors
1213
+ // }
1214
+ // };
1131
1215
 
1132
1216
  const pinRoom = async (roomId) => {
1133
1217
  // Optimistic update