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 +1 -1
- package/src/hooks/messaging-kit/index.jsx +429 -345
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (!
|
|
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
|
-
|
|
701
|
-
//
|
|
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
|
-
|
|
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(
|
|
714
|
-
|
|
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
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
817
|
+
// Add item to the sending queue
|
|
818
|
+
const updatedQueue = [...prevState.messageQueue, queueItem];
|
|
798
819
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
839
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
886
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
// ---
|
|
898
|
+
// --- Handle SUCCESS ---
|
|
899
|
+
/** @type {SentMessageResponse} */
|
|
900
|
+
const sentMessageData = response.data;
|
|
901
|
+
const confirmedRoomId = sentMessageData.room;
|
|
902
|
+
const confirmedMessageId = sentMessageData.id;
|
|
946
903
|
|
|
947
|
-
//
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
//
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
|
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
|
|
1032
|
-
is_read: false,
|
|
1033
|
-
is_delivered: 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
|
-
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
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;
|
|
1156
|
+
/** @type {Record<string, { date: string; messages: any[] }>} */
|
|
1067
1157
|
const groupedMessages = {};
|
|
1068
1158
|
|
|
1069
1159
|
combinedFlatMessages.forEach((message) => {
|
|
1070
|
-
// Use the
|
|
1071
|
-
const
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
|
1093
|
-
const finalGroupedArray = Object.values(groupedMessages)
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|