@droppii-org/chat-sdk 0.6.0 → 0.6.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"DeskConversationList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskConversationList.tsx"],"names":[],"mappings":"AA4BA,QAAA,MAAM,oBAAoB,+CA2UzB,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"DeskConversationList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskConversationList.tsx"],"names":[],"mappings":"AA4BA,QAAA,MAAM,oBAAoB,+CAuVzB,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -31,13 +31,12 @@ const DeskConversationList = () => {
31
31
  const searchParams = useSearchParams();
32
32
  const setConversationData = useConversationStore((state) => state.setConversationData);
33
33
  const setSelectedConversationId = useConversationStore((state) => state.setSelectedConversationId);
34
- const conversationList = useConversationStore((state) => state.conversationList);
35
34
  const updateConversationList = useConversationStore((state) => state.updateConversationList);
36
35
  const filterSummary = useSessionStore((state) => state.filterSummary);
37
36
  const viewMode = useSessionStore((state) => state.viewMode);
38
37
  const isAllGroupsView = viewMode === "allGroups";
39
38
  const { data: teamSummaries } = useGetTeamSessionSummary();
40
- const { dataFlatten: rawSessions, hasNextPage, fetchNextPage, refetch, } = useGetSession({
39
+ const { dataFlatten: rawSessions, hasNextPage, fetchNextPage, refetch, isSyncing, } = useGetSession({
41
40
  filter: filterSummary,
42
41
  options: { isEnabled: !isAllGroupsView },
43
42
  });
@@ -89,7 +88,13 @@ const DeskConversationList = () => {
89
88
  return;
90
89
  }
91
90
  setSelectedConversationId(threadId);
92
- const selectedConversation = conversationList.find((conv) => conv.conversationID === threadId);
91
+ // Read the snapshot instead of subscribing — this effect must only
92
+ // re-run on threadId change. Depending on conversationList re-fired it
93
+ // on every unrelated mutation (including its own updateConversationList
94
+ // call below), looping the sidebar between loaded and loading.
95
+ const selectedConversation = useConversationStore
96
+ .getState()
97
+ .conversationList.find((conv) => conv.conversationID === threadId);
93
98
  // threadId can come from a notification/stale search click — force a
94
99
  // fresh membership check instead of trusting the groupID cache.
95
100
  if (selectedConversation) {
@@ -110,7 +115,7 @@ const DeskConversationList = () => {
110
115
  }
111
116
  });
112
117
  }
113
- }, [searchParams, conversationList]);
118
+ }, [searchParams]);
114
119
  useEffect(() => {
115
120
  const handler = (sessionUpdated) => {
116
121
  if (sessionUpdated.status !== filterSummary.status ||
@@ -131,7 +136,12 @@ const DeskConversationList = () => {
131
136
  }, className: "rounded-lg text-sm flex-1 h-[36px]", size: "large", allowClear: true, onClick: setShowSearchTrue, value: search, autoFocus: false, onClear: onCloseSearch }), showSearch ? (_jsx(Button, { onClick: onCloseSearch, variant: "outlined", className: "p-0 w-[36px] h-[36px] text-gray-500", children: _jsx(Icon, { icon: "close-b", size: 22 }) })) : (_jsx(Button, { "data-testid": "conversation-list-compose-icon", onClick: () => setIsComposeOpen(true), variant: "outlined", className: "p-0 w-[36px] h-[36px] shrink-0 text-gray-500", children: _jsx(Icon, { icon: "edit-b", size: 20 }) }))] }), _jsxs("div", { className: "flex items-center justify-between mt-2", children: [_jsx("span", { className: "text-sm font-semibold text-blue-500 truncate", children: pathName }), isAllGroupsView ? (_jsxs("span", { className: "flex items-center gap-1.5 shrink-0", children: [_jsx(Icon, { icon: "user-three-o", size: 16, className: "text-gray-500" }), _jsx("span", { className: "text-sm font-semibold text-gray-800", children: t("all_groups") })] })) : (filterConfig && (_jsxs("span", { className: "flex items-center gap-1.5 shrink-0", children: [_jsx(Icon, { icon: filterConfig.iconName, size: 16, className: "text-gray-500" }), _jsx("span", { className: "text-sm font-semibold text-gray-800", children: t(filterConfig.labelKey) })] })))] })] }), _jsxs("div", { className: "relative flex-1 min-h-0", children: [_jsx("div", { id: "scrollableConversationsDiv", style: {
132
137
  height: "100%",
133
138
  overflow: showSearch ? "hidden" : "auto",
134
- }, children: isAllGroupsView ? (isConversationGroupsLoading ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Spin, {}) })) : !(conversationGroups === null || conversationGroups === void 0 ? void 0 : conversationGroups.length) ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Empty, { image: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-gray-300" }), description: t("no_conversation") }) })) : (_jsx(InfiniteScroll, { dataLength: conversationGroups.length, next: fetchNextGroupsPage, hasMore: !!hasNextGroupsPage, loader: _jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) }), scrollableTarget: "scrollableConversationsDiv", children: conversationGroups.map((group) => (_jsx(ConversationByGroupItem, { group: group, latestPreview: groupLatestPreviewMap.get(group.conversationId) }, group.groupId))) }))) : (_jsx(InfiniteScroll, { dataLength: (sessions === null || sessions === void 0 ? void 0 : sessions.length) || 0, next: fetchNextPage, hasMore: hasNextPage, loader: _jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) }), scrollableTarget: "scrollableConversationsDiv", children: (sessions === null || sessions === void 0 ? void 0 : sessions.length) === 0 ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Empty, { image: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-gray-300" }), description: t("no_conversation") }) })) : ((_b = sessions === null || sessions === void 0 ? void 0 : sessions.map) === null || _b === void 0 ? void 0 : _b.call(sessions, (session) => (_jsx(ConversationBySessionItem, { sessionItem: session, latestPreview: latestPreviewMap.get(session.conversationId) }, session.conversationId)))) })) }), _jsx(Drawer, { open: showSearch, mask: false, closeIcon: false, styles: {
139
+ }, children: isAllGroupsView ? (isConversationGroupsLoading ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Spin, {}) })) : !(conversationGroups === null || conversationGroups === void 0 ? void 0 : conversationGroups.length) ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Empty, { image: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-gray-300" }), description: t("no_conversation") }) })) : (_jsx(InfiniteScroll, { dataLength: conversationGroups.length, next: fetchNextGroupsPage, hasMore: !!hasNextGroupsPage, loader: _jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) }), scrollableTarget: "scrollableConversationsDiv", children: conversationGroups.map((group) => (_jsx(ConversationByGroupItem, { group: group, latestPreview: groupLatestPreviewMap.get(group.conversationId) }, group.groupId))) }))) : (_jsx(InfiniteScroll, { dataLength: (sessions === null || sessions === void 0 ? void 0 : sessions.length) || 0, next: fetchNextPage, hasMore: hasNextPage, loader: _jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) }), scrollableTarget: "scrollableConversationsDiv", children: isSyncing ? (
140
+ // conversationList (the client cache dataFlatten joins
141
+ // against) hasn't caught up with this filter's BE response
142
+ // yet — showing the list now would flash a wrong, too-low
143
+ // count that corrects itself a moment later.
144
+ _jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Spin, {}) })) : (sessions === null || sessions === void 0 ? void 0 : sessions.length) === 0 ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Empty, { image: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-gray-300" }), description: t("no_conversation") }) })) : ((_b = sessions === null || sessions === void 0 ? void 0 : sessions.map) === null || _b === void 0 ? void 0 : _b.call(sessions, (session) => (_jsx(ConversationBySessionItem, { sessionItem: session, latestPreview: latestPreviewMap.get(session.conversationId) }, session.conversationId)))) })) }), _jsx(Drawer, { open: showSearch, mask: false, closeIcon: false, styles: {
135
145
  body: {
136
146
  padding: 0,
137
147
  },
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageList.tsx"],"names":[],"mappings":"AAgCA,UAAU,gBAAgB;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAgU3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageList.tsx"],"names":[],"mappings":"AAiCA,UAAU,gBAAgB;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAkV3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -24,6 +24,7 @@ import { useJoinGroupFlow } from "../../hooks/session/useJoinGroupFlow";
24
24
  import { useGroupPermission } from "../../hooks/group/useGroupPermission";
25
25
  import { isGroupConversation } from "../../utils/imCommon";
26
26
  import { ConnectStatus, SessionStatus } from "../../types/chat";
27
+ import { PeerType } from "../../types/conversation";
27
28
  import PinLimitModal from "../../components/conversation-detail/PinLimitModal";
28
29
  import ConfirmModal from "../../components/common/ConfirmModal";
29
30
  dayjs.extend(isToday);
@@ -34,29 +35,36 @@ const MessageList = (props) => {
34
35
  const conversationData = useConversationStore((state) => state.conversationData);
35
36
  const permissions = useGroupPermission(conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID);
36
37
  const isGroupChat = isGroupConversation(conversationData !== null && conversationData !== void 0 ? conversationData : undefined);
38
+ // A CX/support session can ride a Group session with peerType missing on
39
+ // the instant response (see isGroupConversation) — non-member/mute checks
40
+ // must not fire off that loose signal, or a support thread with no group
41
+ // membership record gets wrongly treated as a blocked group member.
42
+ const isConfirmedGroup = (conversationData === null || conversationData === void 0 ? void 0 : conversationData.peerType) === PeerType.GROUP;
37
43
  const groupID = conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID;
38
44
  const currentGroupInfo = useConversationStore((state) => groupID ? state.currentGroupInfo.get(groupID) : undefined);
39
45
  const currentMemberInGroup = useConversationStore((state) => groupID ? state.currentMemberInGroup.get(groupID) : undefined);
40
46
  const isViewOnlyMember = useConversationStore((state) => state.isViewOnlyMember);
47
+ const setIsViewOnlyMember = useConversationStore((state) => state.setIsViewOnlyMember);
41
48
  // "Fresh" now means "we have a cache entry for this groupID" — populated
42
49
  // either by a fetch just now or a cache hit from earlier in the session,
43
50
  // not necessarily a response that JUST arrived (that's the point of the
44
51
  // groupID cache: no spinner on reopening a previously-viewed group).
45
52
  const isGroupInfoFresh = currentGroupInfo !== undefined;
46
- const isGroupStateFresh = !isGroupChat ||
53
+ const isGroupStateFresh = !isConfirmedGroup ||
47
54
  (isGroupInfoFresh &&
48
55
  (!currentMemberInGroup || currentMemberInGroup.roleLevel !== undefined));
49
56
  // Group-wide mute (GroupThreadInfo.tsx) restricts Normal members only.
50
- const isRestrictedByGroupMute = isGroupChat &&
57
+ const isRestrictedByGroupMute = isConfirmedGroup &&
51
58
  (currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.status) === GroupStatus.Muted &&
52
59
  (currentMemberInGroup === null || currentMemberInGroup === void 0 ? void 0 : currentMemberInGroup.roleLevel) === GroupMemberRole.Normal;
53
- // useGroupPermission defaults to Normal-role (sendMessage: true) when
54
- // currentMemberInGroup is undefined — wrong once we know it's absent
55
- // for good, not just not-loaded-yet.
56
- const isConfirmedNonMember = isGroupChat && isGroupInfoFresh && !currentMemberInGroup;
57
- const canSendMessage = !isGroupChat ||
60
+ // useGroupPermission defaults to Normal-role when currentMemberInGroup
61
+ // is undefined — wrong once absent for good, not just not-loaded-yet.
62
+ // Gated on isConfirmedGroup: a support thread has no membership record
63
+ // by design, not because it's a removed member.
64
+ const isConfirmedNonMember = isConfirmedGroup && isGroupInfoFresh && !currentMemberInGroup;
65
+ const canSendMessage = !isConfirmedGroup ||
58
66
  (!isConfirmedNonMember && permissions.sendMessage && !isRestrictedByGroupMute);
59
- const canPinMessage = !isGroupChat || (!isConfirmedNonMember && permissions.pinMessage);
67
+ const canPinMessage = !isConfirmedGroup || (!isConfirmedNonMember && permissions.pinMessage);
60
68
  const setQuotedMessage = useConversationStore((state) => state.setQuotedMessage);
61
69
  const setSearchClientMsgID = useConversationStore((state) => state.setSearchClientMsgID);
62
70
  const [openMenuId, setOpenMenuId] = useState(null);
@@ -71,16 +79,24 @@ const MessageList = (props) => {
71
79
  const pinnedTotalCount = useConversationStore((state) => state.pinnedTotalCount);
72
80
  const pinnedRemainingCount = useConversationStore((state) => state.pinnedRemainingCount);
73
81
  const [pinLimitModalOpen, setPinLimitModalOpen] = useState(false);
74
- const { groupId, latestConversationSession, refetchConversationSessions, } = useConversationSessionState({
82
+ const { rawGroupId, latestConversationSession, refetchConversationSessions, } = useConversationSessionState({
75
83
  conversationId,
76
84
  conversationType: conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationType,
77
85
  peerType: conversationData === null || conversationData === void 0 ? void 0 : conversationData.peerType,
78
86
  groupId: conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID,
79
87
  });
80
88
  const { isJoined, isCheckingMembership, shouldRestrictToLastSession, joinGroup, isJoining, } = useJoinGroupFlow({
81
- groupId,
89
+ groupId: rawGroupId,
82
90
  sessionId: latestConversationSession === null || latestConversationSession === void 0 ? void 0 : latestConversationSession.id,
83
91
  });
92
+ // A stale isViewOnlyMember=true only ever clears via the WASM join-group
93
+ // path or a WS event — neither fires for a support thread joined via
94
+ // useJoinSession's REST call. Sync it off once isJoined confirms true.
95
+ useEffect(() => {
96
+ if (!isConfirmedGroup && isJoined && isViewOnlyMember) {
97
+ setIsViewOnlyMember(false);
98
+ }
99
+ }, [isConfirmedGroup, isJoined, isViewOnlyMember, setIsViewOnlyMember]);
84
100
  const messageController = useMessage(conversationId, searchClientMsgID);
85
101
  const { loadState, moreOldLoading, moreNewLoading } = messageController;
86
102
  const { scrollRef, loadMoreOldMessage, handleInfiniteScroll, scrollToBottom, scrollToMessage, } = useMessageListScroll(Object.assign({ conversationId, userId: user === null || user === void 0 ? void 0 : user.userID, isGroupChat }, messageController));
@@ -164,7 +180,7 @@ const MessageList = (props) => {
164
180
  if (!conversationData) {
165
181
  return (_jsx("div", { className: "flex flex-1 items-center justify-center h-full", children: connectStatus !== ConnectStatus.Connected ? (_jsx(Spin, {})) : (_jsx(Empty, { description: t("no_conversation_data") })) }));
166
182
  }
167
- return (_jsxs("div", { className: "flex flex-col flex-1 relative h-full min-w-0 overflow-hidden bg-white", "data-cp-name": "MessageList", children: [_jsx(MessageHeader, { onClose: onClose, currentSession: latestConversationSession, isJoined: isJoined, onOpenThreadInfo: onOpenThreadInfo }), _jsx(MessageInfiniteScroll, { scrollRef: scrollRef, scrollableTargetId: "scrollableMessagesDiv", loadState: loadState, moreOldLoading: moreOldLoading, onLoadMoreOld: loadMoreOldMessage, onScroll: handleInfiniteScroll, children: loadState.messageList.map((message, _, array) => (_jsx(MessageItem, { message: message, allMessages: array, isGroupChat: isGroupChat, contextMenuOpen: openMenuId === message.clientMsgID, onContextMenuOpenChange: (open) => setOpenMenuId(open ? message.clientMsgID : null), onRevokeMessage: handleOpenRevoke, onQuoteMessage: setQuotedMessage, onPressQuoteMessage: onPressQuoteMessage, onPinMessage: canPinMessage ? handlePinMessage : undefined, onUnpinMessage: canPinMessage ? handleUnpinMessage : undefined, timeRevealed: revealedTimeMsgId === message.clientMsgID, onToggleTimeReveal: handleToggleTimeReveal }, message.clientMsgID))) }), moreNewLoading && (_jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) })), (isGroupChat && isCheckingMembership) || !isGroupStateFresh ? (_jsx("div", { className: "border-t bg-white py-4 flex items-center justify-center", children: _jsx(Spin, {}) })) : (latestConversationSession === null || latestConversationSession === void 0 ? void 0 : latestConversationSession.status) === SessionStatus.UNASSIGNED ? (_jsx(UnassignedSessionFooter, { sessionId: latestConversationSession.id, teamId: latestConversationSession.teamId })) : !isGroupChat && shouldRestrictToLastSession ? (_jsxs("div", { className: "border-t bg-white py-4 flex flex-col items-center gap-2", children: [_jsx("p", { className: "text-sm text-gray-500", children: t("join_group_required") }), _jsx(Button, { type: "primary", onClick: joinGroup, loading: isJoining, children: t("join_group") })] })) : isViewOnlyMember ? (
183
+ return (_jsxs("div", { className: "flex flex-col flex-1 relative h-full min-w-0 overflow-hidden bg-white", "data-cp-name": "MessageList", children: [_jsx(MessageHeader, { onClose: onClose, currentSession: latestConversationSession, isJoined: isJoined, onOpenThreadInfo: onOpenThreadInfo }), _jsx(MessageInfiniteScroll, { scrollRef: scrollRef, scrollableTargetId: "scrollableMessagesDiv", loadState: loadState, moreOldLoading: moreOldLoading, onLoadMoreOld: loadMoreOldMessage, onScroll: handleInfiniteScroll, children: loadState.messageList.map((message, _, array) => (_jsx(MessageItem, { message: message, allMessages: array, isGroupChat: isGroupChat, contextMenuOpen: openMenuId === message.clientMsgID, onContextMenuOpenChange: (open) => setOpenMenuId(open ? message.clientMsgID : null), onRevokeMessage: handleOpenRevoke, onQuoteMessage: setQuotedMessage, onPressQuoteMessage: onPressQuoteMessage, onPinMessage: canPinMessage ? handlePinMessage : undefined, onUnpinMessage: canPinMessage ? handleUnpinMessage : undefined, timeRevealed: revealedTimeMsgId === message.clientMsgID, onToggleTimeReveal: handleToggleTimeReveal }, message.clientMsgID))) }), moreNewLoading && (_jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) })), (Boolean(rawGroupId) && isCheckingMembership) || !isGroupStateFresh ? (_jsx("div", { className: "border-t bg-white py-4 flex items-center justify-center", children: _jsx(Spin, {}) })) : (latestConversationSession === null || latestConversationSession === void 0 ? void 0 : latestConversationSession.status) === SessionStatus.UNASSIGNED ? (_jsx(UnassignedSessionFooter, { sessionId: latestConversationSession.id, teamId: latestConversationSession.teamId })) : !isConfirmedGroup && shouldRestrictToLastSession ? (_jsxs("div", { className: "border-t bg-white py-4 flex flex-col items-center gap-2", "data-cp-name": "JoinGroupRequired", children: [_jsx("p", { className: "text-sm text-gray-500", children: t("join_group_required") }), _jsx(Button, { type: "primary", onClick: joinGroup, loading: isJoining, children: t("join_group") })] })) : isViewOnlyMember ? (
168
184
  // Brief fallback while isJoined catches up after the kick event.
169
185
  _jsx(MessageFooter, { currentSession: latestConversationSession, openCreateCannedModal: openCreateCannedModal })) : !canSendMessage ? (_jsx("div", { className: "border-t bg-[#f5f5f5] py-3 flex items-center justify-center", children: _jsx("p", { className: "text-sm text-gray-500", children: t(isRestrictedByGroupMute ? "send_message_disabled_group_mute" : "send_message_disabled") }) })) : (_jsx(MessageFooter, { currentSession: latestConversationSession, openCreateCannedModal: openCreateCannedModal })), _jsx(ConfirmModal, { open: showConfirmRevoke, onOk: onRevokeMessage, onCancel: handleCloseRevoke, title: t("revoke_message_confirm_title"), okText: t("revoke"), cancelText: t("cancel"), okType: "danger", confirmLoading: isRevoking, children: _jsx("p", { children: t("revoke_message_confirm_message") }) }), _jsx(PinLimitModal, { open: pinLimitModalOpen, max: pinnedTotalCount, onClose: () => setPinLimitModalOpen(false) })] }));
170
186
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AA0CA,eAAO,MAAM,cAAc,YAuiB1B,CAAC"}
1
+ {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AA0CA,eAAO,MAAM,cAAc,YA8iB1B,CAAC"}
@@ -14,7 +14,7 @@ import { useRefetchChatToken } from "../../hooks/init/useChatToken";
14
14
  import { updateSession } from "../../hooks/session/useUpdateSession";
15
15
  import { QUERY_KEYS } from "../../services/query";
16
16
  import { useQueryClient } from "@tanstack/react-query";
17
- import { isGroupConversation } from "../../utils/imCommon";
17
+ import { PeerType } from "../../types/conversation";
18
18
  import { ignoreInternalLatestMessage } from "../../utils/common";
19
19
  import { invalidateSessionQueries } from "../../utils/queryHelpers";
20
20
  import emitter from "../../utils/events";
@@ -39,7 +39,12 @@ export const useGlobalEvent = () => {
39
39
  });
40
40
  const refreshCurrentGroupState = async () => {
41
41
  const currentConversation = useConversationStore.getState().currentConversation;
42
- if (!(currentConversation === null || currentConversation === void 0 ? void 0 : currentConversation.groupID) || !isGroupConversation(currentConversation)) {
42
+ // Strict peerType check, matching updateCurrentConversation — also
43
+ // the retry path for a real group whose peerType was unconfirmed at
44
+ // open time, since this reruns on every OnConversationChanged/
45
+ // OnNewConversation.
46
+ if (!(currentConversation === null || currentConversation === void 0 ? void 0 : currentConversation.groupID) ||
47
+ currentConversation.peerType !== PeerType.GROUP) {
43
48
  return;
44
49
  }
45
50
  // allSettled, not all — getSelfGroupPermissionByReq's underlying wasm
@@ -5,8 +5,9 @@ interface UseConversationSessionStateParams {
5
5
  peerType?: string;
6
6
  groupId?: string;
7
7
  }
8
- export declare const useConversationSessionState: ({ conversationId, conversationType, peerType, groupId, }: UseConversationSessionStateParams) => {
8
+ export declare const useConversationSessionState: ({ conversationId, peerType, groupId, }: UseConversationSessionStateParams) => {
9
9
  groupId: string | undefined;
10
+ rawGroupId: string | undefined;
10
11
  latestConversationSession: import("../../types/dto").ISessionResponse | undefined;
11
12
  customerUserID: string;
12
13
  refetchConversationSessions: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<import("../../types/dto").ISessionResponse[], Error>>;
@@ -1 +1 @@
1
- {"version":3,"file":"useConversationSessionState.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useConversationSessionState.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI3D,UAAU,iCAAiC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,2BAA2B,GAAI,0DAKzC,iCAAiC;;;;;CAoBnC,CAAC"}
1
+ {"version":3,"file":"useConversationSessionState.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useConversationSessionState.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI3D,UAAU,iCAAiC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,2BAA2B,GAAI,wCAIzC,iCAAiC;;;;;;CA6BnC,CAAC"}
@@ -1,17 +1,26 @@
1
1
  "use client";
2
2
  import { useGetListSessionByConversation } from "./useGetListSessionByConversation";
3
- import { isGroupConversation } from "../../utils/imCommon";
4
- export const useConversationSessionState = ({ conversationId, conversationType, peerType, groupId, }) => {
3
+ import { PeerType } from "../../types/conversation";
4
+ export const useConversationSessionState = ({ conversationId, peerType, groupId, }) => {
5
5
  var _a;
6
- const isGroupChat = isGroupConversation({ peerType, conversationType });
7
- const currentGroupId = isGroupChat ? groupId : undefined;
6
+ // Strict check, not isGroupConversation's loose conversationType fallback
7
+ // — that wrongly disabled the session-list fetch and ran group-permission
8
+ // checks against a support/bot thread's non-member-managed groupID.
9
+ const isConfirmedGroup = peerType === PeerType.GROUP;
10
+ const currentGroupId = isConfirmedGroup ? groupId : undefined;
8
11
  const { data: conversationSessions, refetch: refetchConversationSessions, } = useGetListSessionByConversation({
9
12
  conversationId,
10
- enabled: !isGroupChat,
13
+ enabled: !isConfirmedGroup,
11
14
  });
12
15
  const latestConversationSession = conversationSessions === null || conversationSessions === void 0 ? void 0 : conversationSessions[0];
13
16
  return {
17
+ // Gated: undefined unless it's a confirmed real group — for
18
+ // permission-side APIs (getSelfGroupPermissionByReq etc.) that must
19
+ // never run against a support/bot thread's underlying groupID.
14
20
  groupId: currentGroupId,
21
+ // Raw, ungated: useJoinGroupFlow's isJoinGroup check needs the real
22
+ // groupID even for a support/bot thread, or it never resolves.
23
+ rawGroupId: groupId,
15
24
  latestConversationSession,
16
25
  customerUserID: (_a = latestConversationSession === null || latestConversationSession === void 0 ? void 0 : latestConversationSession.ownerId) !== null && _a !== void 0 ? _a : "",
17
26
  refetchConversationSessions,
@@ -42,6 +42,7 @@ export declare const useGetSession: ({ filter, options, }: UseGetSessionProps) =
42
42
  hasNextPage: boolean;
43
43
  isFetchingNextPage: boolean;
44
44
  isLoading: boolean;
45
+ isSyncing: boolean;
45
46
  dataFlatten: ISessionByStatus[];
46
47
  } | {
47
48
  error: null;
@@ -78,6 +79,7 @@ export declare const useGetSession: ({ filter, options, }: UseGetSessionProps) =
78
79
  hasNextPage: boolean;
79
80
  isFetchingNextPage: boolean;
80
81
  isLoading: boolean;
82
+ isSyncing: boolean;
81
83
  dataFlatten: ISessionByStatus[];
82
84
  } | {
83
85
  error: Error;
@@ -114,6 +116,7 @@ export declare const useGetSession: ({ filter, options, }: UseGetSessionProps) =
114
116
  hasNextPage: boolean;
115
117
  isFetchingNextPage: boolean;
116
118
  isLoading: boolean;
119
+ isSyncing: boolean;
117
120
  dataFlatten: ISessionByStatus[];
118
121
  } | {
119
122
  error: null;
@@ -150,6 +153,7 @@ export declare const useGetSession: ({ filter, options, }: UseGetSessionProps) =
150
153
  hasNextPage: boolean;
151
154
  isFetchingNextPage: boolean;
152
155
  isLoading: boolean;
156
+ isSyncing: boolean;
153
157
  dataFlatten: ISessionByStatus[];
154
158
  } | {
155
159
  error: null;
@@ -186,6 +190,7 @@ export declare const useGetSession: ({ filter, options, }: UseGetSessionProps) =
186
190
  hasNextPage: boolean;
187
191
  isFetchingNextPage: boolean;
188
192
  isLoading: boolean;
193
+ isSyncing: boolean;
189
194
  dataFlatten: ISessionByStatus[];
190
195
  } | {
191
196
  isError: false;
@@ -222,6 +227,7 @@ export declare const useGetSession: ({ filter, options, }: UseGetSessionProps) =
222
227
  hasNextPage: boolean;
223
228
  isFetchingNextPage: boolean;
224
229
  isLoading: boolean;
230
+ isSyncing: boolean;
225
231
  dataFlatten: ISessionByStatus[];
226
232
  };
227
233
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"useGetSession.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useGetSession.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EAIb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMhE,UAAU,kBAAkB;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,CAAC;CACH;AA6JD,eAAO,MAAM,aAAa,GAAI,sBAG3B,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyEpB,CAAC"}
1
+ {"version":3,"file":"useGetSession.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useGetSession.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,YAAY,EAIb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMhE,UAAU,kBAAkB;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,CAAC;CACH;AA6JD,eAAO,MAAM,aAAa,GAAI,sBAG3B,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiFpB,CAAC"}
@@ -160,23 +160,34 @@ export const useGetSession = ({ filter, options = { pageSize: undefined, isEnabl
160
160
  },
161
161
  enabled: isEnabled && hasValidFilter(filter),
162
162
  }), { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = _c, rest = __rest(_c, ["data", "fetchNextPage", "hasNextPage", "isFetchingNextPage", "isLoading"]);
163
+ // conversationList is filled in asynchronously by syncSessionConversationsToStore
164
+ // below — joining against it mid-sync would under-count the list. isSyncing
165
+ // tells the consumer to keep showing its loading state until it catches up.
163
166
  const dataFlatten = useMemo(() => {
164
- var _a;
165
167
  if (!data)
166
168
  return [];
167
169
  const allItems = data.pages.flatMap((page) => page.data);
168
- const sessionMap = new Map(allItems.map((session) => [session.conversationId, session]));
169
- return (((_a = conversationList === null || conversationList === void 0 ? void 0 : conversationList.map((conversation) => {
170
- const session = sessionMap.get(conversation.conversationID);
171
- if (!session)
170
+ const conversationMap = new Map(conversationList === null || conversationList === void 0 ? void 0 : conversationList.map((c) => [c.conversationID, c]));
171
+ return allItems
172
+ .map((session) => {
173
+ const conversation = conversationMap.get(session.conversationId);
174
+ if (!conversation)
172
175
  return null;
173
176
  return Object.assign(Object.assign({}, session), { conversation });
174
- })) === null || _a === void 0 ? void 0 : _a.filter((item) => Boolean(item))) || []);
177
+ })
178
+ .filter((item) => Boolean(item));
175
179
  }, [data, conversationList]);
180
+ const isSyncing = useMemo(() => {
181
+ if (!data)
182
+ return false;
183
+ const allItems = data.pages.flatMap((page) => page.data);
184
+ return allItems.length !== dataFlatten.length;
185
+ }, [data, dataFlatten]);
176
186
  return Object.assign({ data,
177
187
  fetchNextPage,
178
188
  hasNextPage,
179
189
  isFetchingNextPage,
180
190
  isLoading,
191
+ isSyncing,
181
192
  dataFlatten }, rest);
182
193
  };
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/store/conversation.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAY3C,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,SAElD,CAAC;AAEF,QAAA,MAAM,oBAAoB,gFAkYvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/store/conversation.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAY3C,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,SAElD,CAAC;AAEF,QAAA,MAAM,oBAAoB,gFAiYvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { GroupVisibility, SessionType, } from "@droppii-org/wasm-client-sdk";
2
2
  import { create } from "zustand";
3
3
  import { DChatSDK } from "../constants/sdk";
4
- import { conversationSort, isGroupConversation } from "../utils/imCommon";
4
+ import { conversationSort } from "../utils/imCommon";
5
5
  import { PeerType } from "../types/conversation";
6
6
  import useUserStore from "./user";
7
7
  import useAuthStore from "./auth";
@@ -106,14 +106,15 @@ const useConversationStore = create((set, get) => ({
106
106
  const prevConversation = get().currentConversation;
107
107
  const toggleNewConversation = conversation.conversationID !== (prevConversation === null || prevConversation === void 0 ? void 0 : prevConversation.conversationID);
108
108
  if (toggleNewConversation) {
109
- if (isGroupConversation(conversation)) {
110
- const groupID = conversation.groupID;
109
+ const isConfirmedGroup = conversation.peerType === PeerType.GROUP;
110
+ const groupID = conversation.groupID;
111
+ set({ isViewOnlyMember: false });
112
+ if (isConfirmedGroup && groupID) {
111
113
  // nonMemberGroupID only guards the PREVIOUS group's stale fetches
112
114
  // — a re-opened group always gets a real fetch, even if same ID.
113
115
  if (get().nonMemberGroupID !== groupID) {
114
116
  set({ nonMemberGroupID: null });
115
117
  }
116
- set({ isViewOnlyMember: false });
117
118
  // Cache hit: a previously-viewed group's info/membership is still
118
119
  // in the Map — render from it immediately, no fetch, no spinner.
119
120
  // forceRevalidate bypasses the cache hit for external entry points
@@ -140,19 +141,15 @@ const useConversationStore = create((set, get) => ({
140
141
  get().getCurrentMemberInGroupByReq(groupID),
141
142
  ]);
142
143
  }
143
- // Fresh page load / external entry (search, notification) skips
144
- // leftGroupHandler's WS-driven detection — derive the same
145
- // public->view-only / private->block state from the fetch/cache
146
- // above. Compare against `conversation.groupID` (the argument, not
147
- // global state) — conversationData can be clobbered by a concurrent
148
- // setConversationData call before this async function resumes past
149
- // its first await, which previously made this check unreliable.
150
- // Require confirmed peerType — a CX/bot session can ride a Group
151
- // session with peerType missing from a raw fetch; don't block on that.
152
- const isConfirmedGroup = conversation.peerType === PeerType.GROUP;
144
+ }
145
+ // Derives view-only/block state from fetch/cache above for fresh
146
+ // loads and external entries. Unconfirmed (support/bot) threads get
147
+ // view-only-rejoin when not-a-member but never the block+modal path
148
+ // — that's reserved for isConfirmedGroup.
149
+ if (groupID) {
153
150
  const notAMember = !get().currentMemberInGroup.has(groupID);
154
151
  const isPublic = ((_a = get().currentGroupInfo.get(groupID)) === null || _a === void 0 ? void 0 : _a.visibility) === GroupVisibility.Public;
155
- if (isConfirmedGroup && notAMember && isPublic) {
152
+ if (notAMember && (!isConfirmedGroup || isPublic)) {
156
153
  set({ isViewOnlyMember: true });
157
154
  }
158
155
  else if (isConfirmedGroup && notAMember) {
@@ -163,8 +160,9 @@ const useConversationStore = create((set, get) => ({
163
160
  return;
164
161
  }
165
162
  }
166
- // Non-group conversations never carry group Map state — nothing to
167
- // clear here; the Map only ever holds entries keyed by real groupIDs.
163
+ // Conversations without a groupID never carry group Map state —
164
+ // nothing to clear here; the Map only ever holds entries keyed by
165
+ // real groupIDs.
168
166
  }
169
167
  set(() => ({ currentConversation: Object.assign({}, conversation) }));
170
168
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droppii-org/chat-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Droppii React Chat SDK",
5
5
  "repository": {
6
6
  "type": "git",