l-min-components 1.6.1265 → 1.6.1266

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.1265",
3
+ "version": "1.6.1266",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/assets",
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, createContext } from "react";
1
+ import React, { useState, useEffect, createContext, useMemo } from "react";
2
2
  import { Outlet, useLocation } from "react-router-dom";
3
3
  import {
4
4
  Layout,
@@ -42,13 +42,11 @@ const AppMainLayout = ({ children }) => {
42
42
  const [sideMenuLayout, setSideMenuLayout] = useState(true);
43
43
  const [defaultAcct, setDefaultAcct] = useState("");
44
44
  const [centerLayoutStyle, setCenterLayoutStyle] = useState({});
45
- const [hideAffilicates, setHideAffilicates] = useState(false);
46
45
  const [affiliatesActive, setAffiliatesActive] = useState(false);
47
46
  const [accessToken, setAccessToken] = useState("");
48
47
  const [envType, setEnvType] = useState("");
49
48
  const [newNotifications, setNewNotifications] = useState([]);
50
49
  const [affiliateAccount, setAffiliateAccount] = useState(null);
51
- const [deactivated, setDeactivated] = useState(true); // testing, until we get account setup
52
50
  const [gracePeriod, setGracePeriod] = useState(true); // test
53
51
  const [deviceWidth, setDeviceWidth] = useState(window.innerWidth);
54
52
  const [newLoadingForPostDefaultAccount, setNewLoadingForPostDefaultAccount] =
@@ -69,6 +67,34 @@ const AppMainLayout = ({ children }) => {
69
67
  );
70
68
  }, []);
71
69
 
70
+ // getting account name
71
+ // active account type
72
+ const activeAccountType = useMemo(
73
+ () => String(generalData?.selectedAccount?.type).toLowerCase(),
74
+ [generalData]
75
+ );
76
+ const accountName = useMemo(() => {
77
+ const { user: User, selectedAccount } = generalData;
78
+ if (!User || !selectedAccount) return "..."; // or an empty string
79
+ const defaultAccountName = String(User.first_name).concat(
80
+ " ",
81
+ User.last_name
82
+ );
83
+
84
+ switch (activeAccountType) {
85
+ case "enterprise":
86
+ return selectedAccount.metadata.organization_name ?? defaultAccountName;
87
+ case "developer":
88
+ return (
89
+ selectedAccount.metadata.organization_name ??
90
+ selectedAccount.metadata.org_name ??
91
+ defaultAccountName
92
+ );
93
+ default:
94
+ return defaultAccountName;
95
+ }
96
+ }, [generalData]);
97
+ console.log(activeAccountType, "activeAccountType");
72
98
  const {
73
99
  setDefaultAccount,
74
100
  handleSetDefaultAccount,
@@ -119,8 +145,8 @@ const AppMainLayout = ({ children }) => {
119
145
  if (setDefaultAccount?.data) {
120
146
  if (
121
147
  setDefaultAccount?.response?.status === 207 &&
122
- (!window.location.host.includes("staging") &&
123
- !window.location.host.includes("local"))
148
+ !window.location.host.includes("staging") &&
149
+ !window.location.host.includes("local")
124
150
  ) {
125
151
  window.location.href = "/auth/account-type";
126
152
  } else {
@@ -253,6 +279,7 @@ const AppMainLayout = ({ children }) => {
253
279
  value={{
254
280
  messageKit,
255
281
  affiliateAccount,
282
+ accountName,
256
283
  setRightComponent,
257
284
  setRightLayout,
258
285
  generalData,
@@ -271,8 +298,6 @@ const AppMainLayout = ({ children }) => {
271
298
  setSelectedCourseId,
272
299
  centerLayoutStyle,
273
300
  setCenterLayoutStyle,
274
- // return true if instructor affiliates is Active
275
- setHideAffilicates,
276
301
  affiliatesActive,
277
302
  accessToken,
278
303
  envType,
@@ -280,8 +305,7 @@ const AppMainLayout = ({ children }) => {
280
305
  setNewNotifications,
281
306
  notificationMarkReadData,
282
307
  handleGetNotificationMarkRead,
283
- }}
284
- >
308
+ }}>
285
309
  {/* display mobile layout on device width*/}
286
310
  {deviceWidth < 1200 ? (
287
311
  <MobileLayout findText={findText} />
@@ -293,8 +317,7 @@ const AppMainLayout = ({ children }) => {
293
317
  ) : (
294
318
  <Layout
295
319
  coming={coming}
296
- hasLayoutBackgroundImage={hasLayoutBackgroundImage}
297
- >
320
+ hasLayoutBackgroundImage={hasLayoutBackgroundImage}>
298
321
  <HeaderComponent
299
322
  setNewNotifications={setNewNotifications}
300
323
  findText={findText}
@@ -132,8 +132,7 @@ const AccountDropdown = (props) => {
132
132
  handleRedirectFunc(developerItem);
133
133
  }, 1000);
134
134
  }}
135
- key={idx}
136
- >
135
+ key={idx}>
137
136
  <div className="avatar-container">
138
137
  <img
139
138
  src={
@@ -179,8 +178,7 @@ const AccountDropdown = (props) => {
179
178
  handleRedirectFunc(instructorItem);
180
179
  }, 1000);
181
180
  }}
182
- key={idx}
183
- >
181
+ key={idx}>
184
182
  <div className="avatar-container">
185
183
  <img
186
184
  src={
@@ -221,8 +219,7 @@ const AccountDropdown = (props) => {
221
219
  handleRedirectFunc(enterpriseItem);
222
220
  }, 1000);
223
221
  }}
224
- key={idx}
225
- >
222
+ key={idx}>
226
223
  <div className="avatar-container">
227
224
  <img
228
225
  src={
@@ -266,8 +263,7 @@ const AccountDropdown = (props) => {
266
263
  handleRedirectFunc(personalItem);
267
264
  }, 1000);
268
265
  }}
269
- key={idx}
270
- >
266
+ key={idx}>
271
267
  <div className="avatar-container">
272
268
  <img
273
269
  src={
@@ -301,8 +297,7 @@ const AccountDropdown = (props) => {
301
297
  onClick={() => {
302
298
  window.location.href = "/auth/account-type";
303
299
  }}
304
- style={{ cursor: "pointer" }}
305
- >
300
+ style={{ cursor: "pointer" }}>
306
301
  <AddIcon /> {props.findText("Add account")}
307
302
  </button>
308
303
  </AccountDropdownFooter>
@@ -1,4 +1,5 @@
1
1
  import useAxios from "axios-hooks";
2
+ import { useEffect } from "react";
2
3
 
3
4
  const useHeader = () => {
4
5
  // get user details
@@ -332,7 +333,6 @@ const useHeader = () => {
332
333
  }
333
334
  };
334
335
 
335
-
336
336
  const [{ ...getTotalMessagesCountData }, getTotalMessagesCount] = useAxios(
337
337
  {
338
338
  method: "GET",
@@ -369,6 +369,11 @@ const useHeader = () => {
369
369
  }
370
370
  };
371
371
 
372
+ //defaults
373
+ // get user info
374
+ useEffect(() => {
375
+ handleGetUserDetails();
376
+ }, []);
372
377
 
373
378
  return {
374
379
  handleGetUserDetails,
@@ -53,6 +53,7 @@ const HeaderComponent = (props) => {
53
53
  const [languageDropdown, setLanguageDropdown] = useState();
54
54
  const [isOpen, setIsOpen] = useState(false);
55
55
  // const [searchResultOpen, setSearchResultOpen] = useState(false);
56
+ // in general data find user info and replace where user details is being used
56
57
  const {
57
58
  handleGetUserAccountsDetail,
58
59
  handleGetUserDetails,
@@ -74,7 +75,7 @@ const HeaderComponent = (props) => {
74
75
  handleSetDefaultAccount,
75
76
  } = useHeader();
76
77
  const { pathname } = useLocation();
77
- const { setGeneralData, generalData, notificationMarkReadData } =
78
+ const { setGeneralData, generalData, notificationMarkReadData, accountName } =
78
79
  useContext(OutletContext);
79
80
  const [accountType, setAccountType] = useState("");
80
81
  const currentAccountType = props?.selectedAccount?.type
@@ -420,8 +421,7 @@ const HeaderComponent = (props) => {
420
421
  onClick={() => {
421
422
  setLanguageDropdown();
422
423
  setIsOpen();
423
- }}
424
- >
424
+ }}>
425
425
  <li>
426
426
  <a className={isHomePage() ? "active" : ""}>
427
427
  {getNavLinkIcon()} {props.findText(getNavLinkLabel())}
@@ -440,8 +440,7 @@ const HeaderComponent = (props) => {
440
440
  className={
441
441
  window.location.pathname.includes("settings") ? "active" : ""
442
442
  }
443
- style={{ cursor: "pointer" }}
444
- >
443
+ style={{ cursor: "pointer" }}>
445
444
  <SettingIcon /> {props.findText("Settings")}
446
445
  </a>
447
446
  </li>
@@ -474,8 +473,7 @@ const HeaderComponent = (props) => {
474
473
  ? "disabled"
475
474
  : ""
476
475
  }
477
- style={{ cursor: isPendingDelete ? "not-allowed" : "pointer" }}
478
- >
476
+ style={{ cursor: isPendingDelete ? "not-allowed" : "pointer" }}>
479
477
  {unreadNotificationData?.data?.count > 0 ? (
480
478
  <NotificationIcon />
481
479
  ) : (
@@ -509,8 +507,7 @@ const HeaderComponent = (props) => {
509
507
  onClick={() => {
510
508
  setLanguageDropdown();
511
509
  setIsOpen();
512
- }}
513
- >
510
+ }}>
514
511
  {/* {(!isDeveloper || !window.location.hostname.includes("coming")) && ( // when developer and on staging, don't show
515
512
  <SearchInputGroup>
516
513
  <SearchIcon />
@@ -539,8 +536,7 @@ const HeaderComponent = (props) => {
539
536
  onClick={() => {
540
537
  setLanguageDropdown(!languageDropdown);
541
538
  setIsOpen();
542
- }}
543
- >
539
+ }}>
544
540
  <img src={selectedLanguageData[value] || usFlag} alt="" />
545
541
  <ArrowDownIcon />
546
542
  </div>
@@ -562,8 +558,7 @@ const HeaderComponent = (props) => {
562
558
  setIsOpen(!isOpen);
563
559
  setLanguageDropdown();
564
560
  }}
565
- ref={secondContainerRef}
566
- >
561
+ ref={secondContainerRef}>
567
562
  <div className="user-img-container">
568
563
  <img
569
564
  src={props?.selectedAccount?.profile_photo?.url || avatar}
@@ -571,10 +566,7 @@ const HeaderComponent = (props) => {
571
566
  />
572
567
  </div>
573
568
  <div className="user-info-container">
574
- <h5 style={{ textTransform: "capitalize" }}>
575
- {props?.selectedAccount?.metadata?.organization_name ||
576
- props?.selectedAccount?.display_name}
577
- </h5>
569
+ <h5 style={{ textTransform: "capitalize" }}>{accountName}</h5>
578
570
  <h6 style={{ textTransform: "capitalize" }}>
579
571
  {props?.selectedAccount?.type?.toLowerCase()}
580
572
  </h6>
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from "react";
1
+ import { useContext, useEffect, useState } from "react";
2
2
  import styled from "styled-components";
3
3
  // import ExpiryModal from "./components/expiry modal";
4
4
  import Search from "../searchBar";
@@ -9,6 +9,7 @@ import useHeader from "../header/getHeaderDetails";
9
9
  import OutsideAlerter from "../outsideEventChecker";
10
10
  import FullPageLoader from "../fullPageLoader";
11
11
  import deleteCookies from "./deleteCookies";
12
+ import { OutletContext } from "..";
12
13
 
13
14
  const InstructorAccountSwitcher = ({ generalData, onChange }) => {
14
15
  // const [expiryFlow, setExpiryFlow] = useState();
@@ -32,12 +33,14 @@ const InstructorAccountSwitcher = ({ generalData, onChange }) => {
32
33
  }, [generalData?.selectedAccount, search]);
33
34
 
34
35
  const dropdownList = getAllAffiliateData?.data?.results?.map((item) => {
35
- const image = item?.profile_photo?.url || item?.profile_photo[1][1];
36
- return { name: item?.display_name, image, id: item?.id };
36
+ const image = item?.profile_photo?.url;
37
+ return {
38
+ name: item?.metadata.organization_name ?? "...",
39
+ image,
40
+ id: item?.id,
41
+ };
37
42
  });
38
43
 
39
- console.log(dropdownList, getAllAffiliateData);
40
-
41
44
  const [dropdown, setDropdown] = useState(false);
42
45
 
43
46
  const getCookie = () => {
@@ -111,7 +114,7 @@ const InstructorAccountSwitcher = ({ generalData, onChange }) => {
111
114
  const value = res?.data;
112
115
  const image = value?.profile_photo?.url;
113
116
  setSelectedValue(() => ({
114
- name: value?.display_name,
117
+ name: value?.metadata.organization_name,
115
118
  image,
116
119
  id: value?.id,
117
120
  }));
@@ -130,6 +133,8 @@ const InstructorAccountSwitcher = ({ generalData, onChange }) => {
130
133
  }
131
134
  };
132
135
 
136
+ const { accountName } = useContext(OutletContext);
137
+ console.log(selectedValue, "selected..");
133
138
  return getAllAffiliateData?.data?.results?.length > 0 && !search ? (
134
139
  <>
135
140
  {(setDefaultAffiliateData?.loading ||
@@ -160,24 +165,20 @@ const InstructorAccountSwitcher = ({ generalData, onChange }) => {
160
165
  )} */}
161
166
  <Container className="instructor_account_switcher">
162
167
  <div className="left">
163
- {switchValue !== "affiliates" && (
164
- <p>{generalData?.selectedAccount?.display_name}</p>
165
- )}
168
+ {switchValue !== "affiliates" && <p>{accountName}</p>}
166
169
  {switchValue === "affiliates" && (
167
170
  <AffiliatesDropDown>
168
171
  {!selectedValue ? (
169
172
  <div
170
173
  className="placeholder"
171
- onClick={() => setDropdown(!dropdown)}
172
- >
174
+ onClick={() => setDropdown(!dropdown)}>
173
175
  <p>Select affiliate</p>
174
176
  <ArrowDown />
175
177
  </div>
176
178
  ) : (
177
179
  <div
178
180
  className="selected"
179
- onClick={() => setDropdown(!dropdown)}
180
- >
181
+ onClick={() => setDropdown(!dropdown)}>
181
182
  <div>
182
183
  <img src={selectedValue?.image} alt="" />
183
184
  <p>{selectedValue?.name}</p>
@@ -201,8 +202,7 @@ const InstructorAccountSwitcher = ({ generalData, onChange }) => {
201
202
  key={item?.id}
202
203
  onClick={() => {
203
204
  handleSelection(item);
204
- }}
205
- >
205
+ }}>
206
206
  <div className="left">
207
207
  <img src={item?.image} alt="" />
208
208
  <p>{item?.name}</p>
@@ -220,15 +220,13 @@ const InstructorAccountSwitcher = ({ generalData, onChange }) => {
220
220
  <div className="btn__wrapper">
221
221
  <button
222
222
  className={switchValue !== "affiliates" ? "active" : ""}
223
- onClick={() => handleSwitch(1)}
224
- >
223
+ onClick={() => handleSwitch(1)}>
225
224
  <span>My Account</span>
226
225
  <span className="circle"></span>
227
226
  </button>
228
227
  <button
229
228
  className={switchValue === "affiliates" ? "active" : ""}
230
- onClick={() => handleSwitch(2)}
231
- >
229
+ onClick={() => handleSwitch(2)}>
232
230
  <span>Affiliates</span>
233
231
  <span className="circle"></span>
234
232
  </button>
@@ -1,4 +1,4 @@
1
- import React, { useContext, useEffect, useState } from "react";
1
+ import React, { useContext, useEffect, useMemo, useState } from "react";
2
2
  import PropTypes from "prop-types";
3
3
  import {
4
4
  UserCardContainer,
@@ -16,7 +16,7 @@ const UserCard = ({ user, isOpen, findText }) => {
16
16
  const { handleGetUserDetails, userDetails, handleSetDefaultAccount } =
17
17
  useHeader();
18
18
  const [organizationName, setOrganizationName] = useState();
19
- const { generalData } = useContext(OutletContext);
19
+ const { generalData, accountName } = useContext(OutletContext);
20
20
 
21
21
  useEffect(() => {
22
22
  handleGetUserDetails();
@@ -59,7 +59,7 @@ const UserCard = ({ user, isOpen, findText }) => {
59
59
  }
60
60
  }
61
61
  }, [generalData]);
62
- // console.log(generalData, "data");
62
+ console.log(generalData, "data...");
63
63
 
64
64
  return (
65
65
  <UserCardContainer isOpen={isOpen}>
@@ -94,20 +94,16 @@ const UserCard = ({ user, isOpen, findText }) => {
94
94
  if (window.location.pathname.includes("instructor")) {
95
95
  window.location.href = `${window.location.protocol}//${window.location.hostname}/instructor/profile`;
96
96
  }
97
- }}
98
- >
97
+ }}>
99
98
  <UserName>{findText("Hello")}</UserName>
100
99
  <Handle
101
100
  style={{
102
101
  textTransform: "capitalize",
103
102
  fontWeight: 700,
104
103
  fontSize: 15,
105
- }}
106
- >{`${
107
- organizationName
108
- ? organizationName
109
- : generalData?.selectedAccount?.display_name ?? ""
110
- }.`}</Handle>
104
+ }}>
105
+ {accountName}
106
+ </Handle>
111
107
  </UserDetail>
112
108
  </>
113
109
  )}
@@ -326,6 +326,9 @@ const useMessageKit = (/*affiliatesActive*/) => {
326
326
  isProcessingQueue: false,
327
327
  });
328
328
 
329
+ // Ref for synchronous queue processing lock
330
+ const isCurrentlyProcessingRef = useRef(false);
331
+
329
332
  // State for rooms that should be automatically marked as read
330
333
  const [autoReadRoomIds, setAutoReadRoomIds] = useState(() => new Set());
331
334
 
@@ -828,28 +831,45 @@ const useMessageKit = (/*affiliatesActive*/) => {
828
831
  [setState]
829
832
  ); // Dependency: setState
830
833
 
831
- // Effect to process the message queue sequentially
834
+ // Effect to process the message queue sequentially using a Ref lock
832
835
  useEffect(() => {
833
- // Exit if already processing or queue is empty
834
- if (state.isProcessingQueue || state.messageQueue.length === 0) {
836
+ // **Synchronous Check + Claim:**
837
+ // Use the ref for an immediate check. If already processing, bail out.
838
+ // Also check if the queue has items.
839
+ if (isCurrentlyProcessingRef.current || state.messageQueue.length === 0) {
835
840
  return;
836
841
  }
837
842
 
838
- // --- Start Processing ---
843
+ // **Claim the processing slot synchronously**
844
+ isCurrentlyProcessingRef.current = true;
845
+
846
+ // Set the state flag as well (useful for potential UI indicators or triggering re-renders upon completion)
847
+ // Note: This state update is still async, but the ref prevents the race condition.
839
848
  setState((prevState) => ({ ...prevState, isProcessingQueue: true }));
840
849
 
841
- // Get the next message from the front of the queue
850
+ // Get the next message details from the front of the queue.
851
+ // Reading state here is fine, as we've already claimed the slot.
842
852
  const messageToSend = state.messageQueue[0];
853
+
854
+ // Safety check: Ensure messageToSend exists (queue might have emptied)
855
+ if (!messageToSend) {
856
+ isCurrentlyProcessingRef.current = false; // Release the claim
857
+ setState((prevState) => ({ ...prevState, isProcessingQueue: false }));
858
+ console.warn(
859
+ "Queue Processor: Attempted to process but queue was empty."
860
+ );
861
+ return; // Nothing to process
862
+ }
863
+
843
864
  const { tempId, courseId, accountId, text, media } = messageToSend;
844
- const pendingKey = `pending_${courseId}_${accountId}`; // Reconstruct key
865
+ const pendingKey = `pending_${courseId}_${accountId}`;
845
866
 
846
- // Define the async task for sending this message
867
+ // Define the async task for sending this specific message.
847
868
  const processMessage = async () => {
848
869
  try {
849
870
  // --- Make API Call ---
850
871
  let response;
851
872
  // 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
873
  const requestData = { text, media };
854
874
  const requestParams = {
855
875
  account_id: accountId,
@@ -857,7 +877,6 @@ const useMessageKit = (/*affiliatesActive*/) => {
857
877
  _account: selectedAccount.id,
858
878
  };
859
879
 
860
- // (Use the same API call logic as before)
861
880
  if (selectedAccount.type.toLowerCase() === "enterprise") {
862
881
  response = await request({
863
882
  url: "/notify/v1/enterprise/chats/",
@@ -916,11 +935,23 @@ const useMessageKit = (/*affiliatesActive*/) => {
916
935
  is_delivered: sentMessageData.is_delivered,
917
936
  created_at: sentMessageData.created_at,
918
937
  user_message: sentMessageData.user_message,
919
- date: getLocalDateString(sentMessageData.created_at), // Still useful for basic date info if needed
938
+ date: getLocalDateString(sentMessageData.created_at), // Use existing helper
920
939
  };
921
940
 
922
941
  // Update state atomically after success
923
942
  setState((prevState) => {
943
+ // Safety check: Ensure the message we processed is still the first one
944
+ if (
945
+ prevState.messageQueue.length === 0 ||
946
+ prevState.messageQueue[0].tempId !== tempId
947
+ ) {
948
+ console.warn(
949
+ `Queue Processor: Mismatch processing successful message ${tempId}. Queue changed unexpectedly.`
950
+ );
951
+ // Don't modify state if the queue front doesn't match
952
+ return prevState;
953
+ }
954
+
924
955
  // 1. Remove optimistic message
925
956
  let updatedOptimisticMessages = { ...prevState.optimisticMessages };
926
957
  if (updatedOptimisticMessages[pendingKey]) {
@@ -965,12 +996,11 @@ const useMessageKit = (/*affiliatesActive*/) => {
965
996
  date: messageDateLabel,
966
997
  messages: [newChatMessage],
967
998
  });
968
- // Sort groups using helper function
969
999
  updatedChatHistory.sort(
970
1000
  (a, b) =>
971
1001
  getSortableTimestampForGroup(b) -
972
1002
  getSortableTimestampForGroup(a)
973
- );
1003
+ ); // Use helper
974
1004
  }
975
1005
  updatedChats[confirmedRoomId] = updatedChatHistory;
976
1006
 
@@ -982,7 +1012,6 @@ const useMessageKit = (/*affiliatesActive*/) => {
982
1012
  const roomIdx = cg.rooms.findIndex((r) => r.id === confirmedRoomId);
983
1013
  if (roomIdx !== -1) {
984
1014
  roomExists = true;
985
- // Create a new room object to ensure immutability
986
1015
  const updatedRoom = {
987
1016
  ...cg.rooms[roomIdx],
988
1017
  last_message: {
@@ -992,22 +1021,22 @@ const useMessageKit = (/*affiliatesActive*/) => {
992
1021
  created_at: newChatMessage.created_at,
993
1022
  },
994
1023
  };
995
- // Create a new rooms array for the course group
996
1024
  const updatedRooms = [...cg.rooms];
997
1025
  updatedRooms[roomIdx] = updatedRoom;
998
- // Return a new course group object
999
1026
  return { ...cg, rooms: updatedRooms };
1000
1027
  }
1001
1028
  return cg;
1002
1029
  });
1003
- // Trigger refetch if room metadata wasn't found (handled outside setState)
1004
- if (!roomExists) {
1005
- setTimeout(() => getMessageRoomsByCourses(null, true), 0);
1006
- }
1007
1030
 
1008
1031
  // 4. Remove processed item from queue (immutable update)
1009
1032
  const updatedQueue = prevState.messageQueue.slice(1);
1010
1033
 
1034
+ // Trigger refetch if room metadata wasn't found (handled outside setState)
1035
+ if (!roomExists && typeof getMessageRoomsByCourses === "function") {
1036
+ // Check if function exists
1037
+ setTimeout(() => getMessageRoomsByCourses(null, true), 0);
1038
+ }
1039
+
1011
1040
  // Return the combined state updates
1012
1041
  return {
1013
1042
  ...prevState,
@@ -1015,7 +1044,7 @@ const useMessageKit = (/*affiliatesActive*/) => {
1015
1044
  chats: updatedChats,
1016
1045
  roomsByCourses: updatedRoomsByCourses,
1017
1046
  messageQueue: updatedQueue,
1018
- // isProcessingQueue reset happens in 'finally' block
1047
+ isProcessingQueue: true, // Keep true until finally block resets it below
1019
1048
  };
1020
1049
  });
1021
1050
  } catch (error) {
@@ -1025,25 +1054,33 @@ const useMessageKit = (/*affiliatesActive*/) => {
1025
1054
  error
1026
1055
  );
1027
1056
  setState((prevState) => {
1057
+ // Safety check for queue consistency
1058
+ if (
1059
+ prevState.messageQueue.length === 0 ||
1060
+ prevState.messageQueue[0].tempId !== tempId
1061
+ ) {
1062
+ console.warn(
1063
+ `Queue Processor: Mismatch processing failed message ${tempId}. Queue changed unexpectedly.`
1064
+ );
1065
+ return prevState;
1066
+ }
1067
+
1028
1068
  // 1. Update optimistic message status to 'failed'
1029
1069
  let updatedOptimisticMessages = { ...prevState.optimisticMessages };
1030
1070
  if (updatedOptimisticMessages[pendingKey]) {
1031
1071
  const messageIndex = updatedOptimisticMessages[
1032
1072
  pendingKey
1033
1073
  ].findIndex((msg) => msg.tempId === tempId);
1034
- // Only update if found and still pending (might have been handled differently elsewhere?)
1035
1074
  if (
1036
1075
  messageIndex !== -1 &&
1037
1076
  updatedOptimisticMessages[pendingKey][messageIndex].status ===
1038
1077
  "pending"
1039
1078
  ) {
1040
- // Create new message object for immutability
1041
1079
  const failedMessage = {
1042
1080
  ...updatedOptimisticMessages[pendingKey][messageIndex],
1043
1081
  status: "failed",
1044
1082
  error: error?.message || "Unknown error",
1045
1083
  };
1046
- // Create new array for the pending key
1047
1084
  const updatedPendingList = [
1048
1085
  ...updatedOptimisticMessages[pendingKey],
1049
1086
  ];
@@ -1062,33 +1099,39 @@ const useMessageKit = (/*affiliatesActive*/) => {
1062
1099
 
1063
1100
  // 2. Remove failed item from queue (immutable update)
1064
1101
  const updatedQueue = prevState.messageQueue.slice(1);
1065
-
1102
+ // Return the combined state updates
1066
1103
  return {
1067
1104
  ...prevState,
1068
1105
  optimisticMessages: updatedOptimisticMessages,
1069
1106
  messageQueue: updatedQueue,
1070
- // isProcessingQueue reset happens in 'finally' block
1107
+ isProcessingQueue: true, // Keep true until finally block resets it below
1071
1108
  };
1072
1109
  });
1073
1110
  } finally {
1074
1111
  // --- Finish Processing ---
1075
- // Always ensure processing flag is reset, even on error
1112
+ // **Crucial:** Release the synchronous lock first.
1113
+ isCurrentlyProcessingRef.current = false;
1114
+ // Then update the state flag. This state update will trigger a re-render.
1115
+ // If the queue still has items, the effect will run again, see the ref is false,
1116
+ // claim the lock, and start processing the next item.
1076
1117
  setState((prevState) => ({ ...prevState, isProcessingQueue: false }));
1077
1118
  }
1078
1119
  };
1079
1120
 
1080
- // Execute the async sending logic
1121
+ // Execute the async sending logic for the current item.
1081
1122
  processMessage();
1082
1123
 
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
1124
+ // Dependencies:
1125
+ // - `state.isProcessingQueue`: Triggers check when processing finishes.
1126
+ // - `state.messageQueue`: Triggers check when queue content changes.
1127
+ // - `request`, `selectedAccount`, `affiliateAccount`, `getMessageRoomsByCourses`:
1128
+ // Required by exhaustive-deps for use within the effect's scope.
1085
1129
  }, [
1086
1130
  state.isProcessingQueue,
1087
1131
  state.messageQueue,
1088
1132
  // request,
1089
1133
  selectedAccount,
1090
1134
  affiliateAccount,
1091
- // getMessageRoomsByCourses,
1092
1135
  ]);
1093
1136
 
1094
1137
  /**