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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "l-min-components",
3
- "version": "1.6.1259",
3
+ "version": "1.6.1263",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/assets",
@@ -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(), // Track which rooms are currently loading
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
- // sending messages
630
- const sendMessage = async ({ text, media, courseId, accountId }) => {
631
- let response;
632
- if (selectedAccount.type.toLowerCase() === "enterprise") {
633
- response = await request({
634
- url: "/notify/v1/enterprise/chats/",
635
- params: {
636
- account_id: accountId,
637
- course_id: courseId,
638
- _account: selectedAccount.id,
639
- },
640
- data: {
641
- text,
642
- media,
643
- },
644
- method: "Post",
645
- });
646
- }
647
- if (
648
- String(selectedAccount.type).toLowerCase() === "instructor" &&
649
- affiliateAccount
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
- if (!response?.data) return; // Added optional chaining for safety
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
- /** @type {SentMessageResponse} */
686
- const sentMessageData = response.data;
687
- const roomId = sentMessageData.room;
683
+ const today = new Date();
684
+ const yesterday = new Date(today);
685
+ yesterday.setDate(today.getDate() - 1);
688
686
 
689
- // --- 1. Format the new message into ChatMessage structure ---
690
- // Helper to get YYYY-MM-DD from ISO string
691
- const getLocalDateString = (isoTimestamp) => {
692
- if (!isoTimestamp) return new Date().toISOString().split("T")[0]; // Fallback
693
- try {
694
- return new Date(isoTimestamp).toISOString().split("T")[0];
695
- } catch (e) {
696
- console.error("Error parsing date:", e);
697
- return new Date().toISOString().split("T")[0]; // Fallback
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
- /** @type {ChatMessage} */
702
- const newChatMessage = {
703
- id: sentMessageData.id,
704
- text: sentMessageData.text,
705
- // Map media - assuming SentMessageMedia is compatible enough or ChatMessage.media allows it
706
- media: sentMessageData.media,
707
- sender: {
708
- // Map sender info
709
- id: sentMessageData.sender.id,
710
- first_name:
711
- sentMessageData.sender.full_name || sentMessageData.sender.name, // Use full_name or name
712
- },
713
- is_read: sentMessageData.is_read,
714
- is_delivered: sentMessageData.is_delivered,
715
- created_at: sentMessageData.created_at,
716
- user_message: sentMessageData.user_message, // Should be true
717
- date: getLocalDateString(sentMessageData.created_at), // Extract YYYY-MM-DD
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
- // --- 2. Update State ---
741
+ // --- Store Optimistic Message using pendingKey ---
721
742
  setState((prevState) => {
722
- // --- Check if room exists before updating state ---
723
- let roomExistsInState = false;
724
- for (const courseGroup of prevState.roomsByCourses) {
725
- if (courseGroup.rooms.some((room) => room.id === roomId)) {
726
- roomExistsInState = true;
727
- break;
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
- // If room doesn't exist, fetch rooms and skip this state update
732
- if (!roomExistsInState) {
733
- console.log(
734
- `Room ${roomId} not found in state after sending message, refetching room list.`
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
- getMessageRoomsByCourses(null, true); // Fetch room list
792
+ }
793
+ // --- End API Call ---
737
794
 
738
- // --- Update chats state even if room metadata isn't found yet ---
739
- const chatHistoryForMissingRoom = prevState.chats[roomId] || [];
740
- let updatedChatHistoryForMissingRoom = [...chatHistoryForMissingRoom];
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
- const todayGroupIndexForMissing =
744
- updatedChatHistoryForMissingRoom.findIndex(
745
- (group) => group.date === todayDateStringForMissing
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
- if (todayGroupIndexForMissing !== -1) {
749
- const todayGroup =
750
- updatedChatHistoryForMissingRoom[todayGroupIndexForMissing];
751
- if (
752
- !todayGroup.messages.some((msg) => msg.id === newChatMessage.id)
753
- ) {
754
- const updatedTodayGroup = {
755
- ...todayGroup,
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
- } else {
765
- const newTodayGroup = {
766
- date: todayDateStringForMissing,
767
- messages: [newChatMessage],
768
- };
769
- updatedChatHistoryForMissingRoom = [
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
- const updatedChatsOnly = {
776
- ...prevState.chats,
777
- [roomId]: updatedChatHistoryForMissingRoom,
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
- const todayGroupIndex = updatedChatHistory.findIndex(
791
- (group) => group.date === todayDateString
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
- if (todayGroupIndex !== -1) {
795
- // "today" group exists, add message to it
796
- const todayGroup = updatedChatHistory[todayGroupIndex];
797
- // Avoid adding duplicates if WS sends the same message back
798
- if (!todayGroup.messages.some((msg) => msg.id === newChatMessage.id)) {
799
- const updatedTodayGroup = {
800
- ...todayGroup,
801
- messages: [...todayGroup.messages, newChatMessage], // Add new message
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
- // Replace the old group with the updated one
804
- updatedChatHistory.splice(todayGroupIndex, 1, updatedTodayGroup);
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
- } else {
807
- // "today" group doesn't exist, create it and add to the beginning
808
- const newTodayGroup = {
809
- date: todayDateString,
810
- messages: [newChatMessage],
811
- };
812
- // Prepend the new group instead of pushing to the end
813
- updatedChatHistory = [newTodayGroup, ...updatedChatHistory];
814
- }
815
-
816
- const updatedChats = {
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 === roomId
911
+ (room) => room.id === confirmedRoomId
826
912
  );
827
913
  if (roomIndex !== -1) {
828
- // Found the room, update its last_message
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, // Assuming structure is compatible enough
922
+ media: newChatMessage.media, // Use the confirmed media details
837
923
  created_at: newChatMessage.created_at,
838
924
  },
839
- // Optionally increment unread_count if it's for the *other* user,
840
- // but typically the sender doesn't increment their own unread count.
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
- return courseGroup; // Return unchanged if room not in this group
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
- // --- Return combined state update ---
850
- return {
851
- ...prevState,
852
- chats: updatedChats,
853
- roomsByCourses: updatedRoomsByCourses,
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
- // Optional: Maybe trigger readRoomMessages if the user sends a message?
858
- // await readRoomMessages(newChatMessage.id); // Depends on desired logic
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: "d9b0c6ab4e",
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