@droppii-org/chat-sdk 0.4.10 → 0.4.12

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":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAkB/D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,QAAA,MAAM,uBAAuB,GAAI,4EAM9B,4BAA4B,4CAynB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAkB/D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,QAAA,MAAM,uBAAuB,GAAI,4EAM9B,4BAA4B,4CA8qB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -4,8 +4,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import { useBoolean } from "ahooks";
6
6
  import clsx from "clsx";
7
+ import { Button, message as toastMessage } from "antd";
7
8
  import { Icon } from "../../components/icon";
8
9
  import { useSendMessage } from "../../hooks/message/useSendMessage";
10
+ import { useJoinPublicGroup } from "../../hooks/group/useJoinPublicGroup";
9
11
  import useAuthStore from "../../store/auth";
10
12
  import useConversationStore from "../../store/conversation";
11
13
  import useMessageDraftStore from "../../store/messageDraft";
@@ -36,6 +38,38 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
36
38
  const conversationId = selectedConversationId || (conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID) || "";
37
39
  const isConversationReady = !!conversationData && (!!conversationData.userID || !!conversationData.groupID);
38
40
  const isViewOnlyMember = useConversationStore((state) => state.isViewOnlyMember);
41
+ const setIsViewOnlyMember = useConversationStore((state) => state.setIsViewOnlyMember);
42
+ const getCurrentMemberInGroupByReq = useConversationStore((state) => state.getCurrentMemberInGroupByReq);
43
+ const getSelfGroupPermissionByReq = useConversationStore((state) => state.getSelfGroupPermissionByReq);
44
+ const { mutate: joinPublicGroup, isPending: isJoiningPublicGroup } = useJoinPublicGroup();
45
+ const handleJoinPublicGroup = useCallback(() => {
46
+ const groupID = conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID;
47
+ if (!groupID)
48
+ return;
49
+ joinPublicGroup({ groupID }, {
50
+ onSuccess: async () => {
51
+ toastMessage.success(tCommon("join_group_success"));
52
+ // updateCurrentConversation's revalidation only runs on a
53
+ // conversationID change — this is the same conversation the user
54
+ // was already viewing, so refresh membership/permissions directly
55
+ // instead, and flip isViewOnlyMember off immediately rather than
56
+ // waiting on a WS event that may not fire right away.
57
+ setIsViewOnlyMember(false);
58
+ await Promise.allSettled([
59
+ getCurrentMemberInGroupByReq(groupID),
60
+ getSelfGroupPermissionByReq(groupID),
61
+ ]);
62
+ },
63
+ onError: () => toastMessage.error(tCommon("join_group_failed")),
64
+ });
65
+ }, [
66
+ conversationData,
67
+ getCurrentMemberInGroupByReq,
68
+ getSelfGroupPermissionByReq,
69
+ joinPublicGroup,
70
+ setIsViewOnlyMember,
71
+ tCommon,
72
+ ]);
39
73
  const sendMessageOptions = useMemo(() => ({
40
74
  scrollEvent: "CONVERSATION_CHAT_SCROLL_TO_BOTTOM_IF_AT_BOTTOM",
41
75
  }), []);
@@ -408,7 +442,7 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
408
442
  void handleSend();
409
443
  };
410
444
  if (isViewOnlyMember) {
411
- return (_jsx("div", { "data-testid": "conversation-detail-input-view-only", className: "flex w-full shrink-0 items-center justify-center bg-chat-bubble-neutral/[0.05] px-4 py-3 text-center text-sm text-gray-500", children: t("thread_detail.view_only_notice") }));
445
+ return (_jsxs("div", { "data-testid": "conversation-detail-input-view-only", className: "flex w-full shrink-0 items-center justify-between gap-3 bg-chat-bubble-neutral/[0.05] px-4 py-3", children: [_jsx("span", { className: "text-sm text-gray-500", children: t("thread_detail.view_only_notice") }), _jsx(Button, { type: "primary", size: "small", loading: isJoiningPublicGroup, onClick: handleJoinPublicGroup, children: tCommon("join_group") })] }));
412
446
  }
413
447
  return (_jsxs("div", { ref: containerRef, "data-testid": "conversation-detail-input", "data-cp-name": "ConversationDetailInput", className: "flex w-full shrink-0 flex-col bg-chat-bubble-neutral/[0.05]", children: [_jsx("input", { ref: mediaInputRef, type: "file", accept: "image/jpeg,image/png,image/jpg,video/*", multiple: true, className: "hidden", onChange: handleMediaInputChange }), _jsx("input", { ref: documentInputRef, type: "file", accept: ".pdf,.doc,.docx", multiple: true, className: "hidden", onChange: handleDocumentInputChange }), _jsx(ReplyPreviewBar, {}), listUploadFiles.length > 0 && (_jsx(FilePreviewList, { files: listUploadFiles, onRemove: (file) => handleRemoveFile(file.uid), onVideoReady: () => setIsVideoLoading(false) })), showLinkPreview && previewUrl && (_jsx(LinkInputPreview, { url: previewUrl, onRemove: () => setPreviewDismissed(true) })), _jsxs("div", { className: "flex items-end gap-3 px-3 py-2", children: [_jsx("div", { className: "flex shrink-0 items-center", children: _jsx("button", { type: "button", onClick: handleToggleMoreOption, "aria-label": t("thread_detail.more_option.toggle"), "aria-expanded": isMoreOptionOpen, "data-testid": "conversation-detail-more-option-toggle", className: "flex py-3 items-center justify-center rounded-full", children: isMoreOptionOpen ? (_jsx(Icon, { icon: "close-circle-b", size: 24, className: "text-primary-400" })) : (_jsx(Icon, { icon: "plus-circle-o", size: 24, className: "text-gray-900" })) }) }), _jsx("div", { className: clsx("flex min-w-0 flex-1 overflow-hidden rounded-xl border bg-white transition-shadow", isFocused
414
448
  ? "border-primary-200 shadow-primary-focus"
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationMessageList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationMessageList.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAmBzE,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,QAAA,MAAM,uBAAuB,GAAI,gBAAgB,4BAA4B,4CA2L5E,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"ConversationMessageList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationMessageList.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAmBzE,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,QAAA,MAAM,uBAAuB,GAAI,gBAAgB,4BAA4B,4CAoM5E,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -37,6 +37,10 @@ const ConversationMessageList = ({ bottomSlot }) => {
37
37
  const isMessageLoading = !conversationId || loadState.initLoading;
38
38
  const [openToolboxMessageId, setOpenToolboxMessageId] = useState(null);
39
39
  const closeToolbox = useCallback(() => setOpenToolboxMessageId(null), []);
40
+ const [revealedTimeMsgId, setRevealedTimeMsgId] = useState(null);
41
+ const handleToggleTimeReveal = useCallback((clientMsgID) => {
42
+ setRevealedTimeMsgId((prev) => (prev === clientMsgID ? null : clientMsgID));
43
+ }, []);
40
44
  const { pinMessage, unpinMessage } = usePinMessage();
41
45
  const pinnedTotalCount = useConversationStore((state) => state.pinnedTotalCount);
42
46
  const pinnedRemainingCount = useConversationStore((state) => state.pinnedRemainingCount);
@@ -95,6 +99,6 @@ const ConversationMessageList = ({ bottomSlot }) => {
95
99
  target: scrollRef,
96
100
  enable: !!openToolboxMessageId,
97
101
  });
98
- return (_jsxs("div", { "data-testid": "conversation-message-list", "data-cp-name": "ConversationMessageList", className: "relative flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-white", children: [isMessageLoading ? (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) })) : (_jsxs(MessageInfiniteScroll, { scrollRef: scrollRef, scrollableTargetId: "scrollableConversationMessagesDiv", loadState: loadState, moreOldLoading: moreOldLoading, onLoadMoreOld: loadMoreOldMessage, onScroll: handleInfiniteScroll, containerClassName: "h-full", empty: _jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Empty, { description: t("empty.no_messages") }) }), children: [bottomSlot, loadState.messageList.map((message, index, array) => (_jsx(MessageItem, { message: message, allMessages: array, isGroupChat: isGroupChat, forceHideTip: index === 0 && !!bottomSlot, contextMenuOpen: openToolboxMessageId === message.clientMsgID, onContextMenuOpenChange: (open) => setOpenToolboxMessageId(open ? message.clientMsgID : null), onQuoteMessage: setQuotedMessage, onRevokeMessage: handleRevokeMessage, onPressQuoteMessage: onPressQuoteMessage, onPinMessage: canPinMessage ? handlePinMessage : undefined, onUnpinMessage: canPinMessage ? handleUnpinMessage : undefined, enableLongPress: true }, message.clientMsgID)))] })), _jsx(PinLimitModal, { open: pinLimitModalOpen, max: pinnedTotalCount, onClose: () => setPinLimitModalOpen(false) })] }));
102
+ return (_jsxs("div", { "data-testid": "conversation-message-list", "data-cp-name": "ConversationMessageList", className: "relative flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-white", children: [isMessageLoading ? (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) })) : (_jsxs(MessageInfiniteScroll, { scrollRef: scrollRef, scrollableTargetId: "scrollableConversationMessagesDiv", loadState: loadState, moreOldLoading: moreOldLoading, onLoadMoreOld: loadMoreOldMessage, onScroll: handleInfiniteScroll, containerClassName: "h-full", empty: _jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Empty, { description: t("empty.no_messages") }) }), children: [bottomSlot, loadState.messageList.map((message, index, array) => (_jsx(MessageItem, { message: message, allMessages: array, isGroupChat: isGroupChat, forceHideTip: index === 0 && !!bottomSlot, contextMenuOpen: openToolboxMessageId === message.clientMsgID, onContextMenuOpenChange: (open) => setOpenToolboxMessageId(open ? message.clientMsgID : null), onQuoteMessage: setQuotedMessage, onRevokeMessage: handleRevokeMessage, timeRevealed: revealedTimeMsgId === message.clientMsgID, onToggleTimeReveal: handleToggleTimeReveal, onPressQuoteMessage: onPressQuoteMessage, onPinMessage: canPinMessage ? handlePinMessage : undefined, onUnpinMessage: canPinMessage ? handleUnpinMessage : undefined, enableLongPress: true }, message.clientMsgID)))] })), _jsx(PinLimitModal, { open: pinLimitModalOpen, max: pinnedTotalCount, onClose: () => setPinLimitModalOpen(false) })] }));
99
103
  };
100
104
  export default ConversationMessageList;
@@ -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,4CAwT3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
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,4CAiU3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -61,6 +61,10 @@ const MessageList = (props) => {
61
61
  const setSearchClientMsgID = useConversationStore((state) => state.setSearchClientMsgID);
62
62
  const [openMenuId, setOpenMenuId] = useState(null);
63
63
  const [selectedItem, setSelectedItem] = useState(null);
64
+ const [revealedTimeMsgId, setRevealedTimeMsgId] = useState(null);
65
+ const handleToggleTimeReveal = useCallback((clientMsgID) => {
66
+ setRevealedTimeMsgId((prev) => (prev === clientMsgID ? null : clientMsgID));
67
+ }, []);
64
68
  const [showConfirmRevoke, { setTrue: openConfirmRevoke, setFalse: closeConfirmRevoke },] = useBoolean(false);
65
69
  const { revokeMessage, loading: isRevoking } = useRevokeMessage();
66
70
  const { pinMessage, unpinMessage } = usePinMessage();
@@ -160,7 +164,7 @@ const MessageList = (props) => {
160
164
  if (!conversationData) {
161
165
  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") })) }));
162
166
  }
163
- return (_jsxs("div", { className: "flex flex-col flex-1 relative h-full min-w-0 bg-white", style: { overflowX: "hidden" }, "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 }, 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 })) : 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 ? (
167
+ return (_jsxs("div", { className: "flex flex-col flex-1 relative h-full min-w-0 bg-white", style: { overflowX: "hidden" }, "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 ? (
164
168
  // Brief fallback while isJoined catches up after the kick event.
165
169
  _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) })] }));
166
170
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/index.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAGL,wBAAwB,EAEzB,MAAM,cAAc,CAAC;AAStB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAoB/C,UAAU,kBAAkB;IAC1B,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;CACpC;AASD,eAAO,MAAM,oBAAoB,mDAc/B,CAAC;AAEH,eAAO,MAAM,uBAAuB,gCAAyC,CAAC;AAE9E,QAAA,MAAM,qBAAqB,GAAI,4CAG5B,kBAAkB,4CA0UpB,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/index.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAGL,wBAAwB,EAEzB,MAAM,cAAc,CAAC;AAUtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAoB/C,UAAU,kBAAkB;IAC1B,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;CACpC;AASD,eAAO,MAAM,oBAAoB,mDAc/B,CAAC;AAEH,eAAO,MAAM,uBAAuB,gCAAyC,CAAC;AAE9E,QAAA,MAAM,qBAAqB,GAAI,4CAG5B,kBAAkB,4CAgYpB,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -10,7 +10,8 @@ import EnterHandler from "./EnterHandler";
10
10
  import ActionBar from "./ActionBar";
11
11
  import { AccountType, } from "../../../types/chat";
12
12
  import { useSendMessage } from "../../../hooks/message/useSendMessage";
13
- import { Modal, Popover } from "antd";
13
+ import { useJoinPublicGroup } from "../../../hooks/group/useJoinPublicGroup";
14
+ import { Button, Modal, Popover, message as toastMessage } from "antd";
14
15
  import FilePreview from "./FilePreview";
15
16
  import LinkInputPreview from "./LinkInputPreview";
16
17
  import ComposerTextSyncPlugin from "./ComposerTextSyncPlugin";
@@ -72,6 +73,38 @@ const MessageFooterProvider = ({ currentSession, openCreateCannedModal, }) => {
72
73
  const quotedMessage = useConversationStore((state) => state.quotedMessage);
73
74
  const conversationData = useConversationStore((state) => state.conversationData);
74
75
  const isViewOnlyMember = useConversationStore((state) => state.isViewOnlyMember);
76
+ const setIsViewOnlyMember = useConversationStore((state) => state.setIsViewOnlyMember);
77
+ const getCurrentMemberInGroupByReq = useConversationStore((state) => state.getCurrentMemberInGroupByReq);
78
+ const getSelfGroupPermissionByReq = useConversationStore((state) => state.getSelfGroupPermissionByReq);
79
+ const { mutate: joinPublicGroup, isPending: isJoiningPublicGroup } = useJoinPublicGroup();
80
+ const handleJoinPublicGroup = useCallback(() => {
81
+ const groupID = conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID;
82
+ if (!groupID)
83
+ return;
84
+ joinPublicGroup({ groupID }, {
85
+ onSuccess: async () => {
86
+ toastMessage.success(t("join_group_success"));
87
+ // updateCurrentConversation's revalidation only runs on a
88
+ // conversationID change — this is the same conversation the user
89
+ // was already viewing, so refresh membership/permissions directly
90
+ // instead, and flip isViewOnlyMember off immediately rather than
91
+ // waiting on a WS event that may not fire right away.
92
+ setIsViewOnlyMember(false);
93
+ await Promise.allSettled([
94
+ getCurrentMemberInGroupByReq(groupID),
95
+ getSelfGroupPermissionByReq(groupID),
96
+ ]);
97
+ },
98
+ onError: () => toastMessage.error(t("join_group_failed")),
99
+ });
100
+ }, [
101
+ conversationData,
102
+ getCurrentMemberInGroupByReq,
103
+ getSelfGroupPermissionByReq,
104
+ joinPublicGroup,
105
+ setIsViewOnlyMember,
106
+ t,
107
+ ]);
75
108
  // Internal notes are a CX-support concept (team-only remarks not sent to
76
109
  // the customer) — meaningless inside a real member-managed group, where
77
110
  // everyone in the thread already is the "team".
@@ -216,7 +249,7 @@ const MessageFooterProvider = ({ currentSession, openCreateCannedModal, }) => {
216
249
  closeCanned();
217
250
  };
218
251
  if (isViewOnlyMember) {
219
- return (_jsx("div", { "data-testid": "message-footer-view-only", className: "flex w-full items-center justify-center border-t border-gray-25 bg-white px-4 py-3 text-center text-sm text-gray-500", children: tConversationInbox("thread_detail.view_only_notice") }));
252
+ return (_jsxs("div", { "data-testid": "message-footer-view-only", className: "flex w-full items-center justify-between gap-3 border-t border-gray-25 bg-white px-4 py-3", children: [_jsx("span", { className: "text-sm text-gray-500", children: tConversationInbox("thread_detail.view_only_notice") }), _jsx(Button, { type: "primary", size: "small", loading: isJoiningPublicGroup, onClick: handleJoinPublicGroup, children: t("join_group") })] }));
220
253
  }
221
254
  return (_jsxs(MessageFooterContext.Provider, { value: {
222
255
  onSendMessage,
@@ -9,7 +9,11 @@ export interface MessageBubbleProps {
9
9
  id?: string;
10
10
  isTransparent?: boolean;
11
11
  timeLabel?: string;
12
+ /** Whether this specific message's time label is currently revealed by tap. */
13
+ timeRevealed?: boolean;
14
+ /** Called on bubble tap to toggle this message as the single revealed one. */
15
+ onToggleTimeReveal?: () => void;
12
16
  }
13
- declare const MessageBubble: ({ isMine, content, senderName, showSenderName, showTip, isInternal, id, isTransparent, timeLabel, }: MessageBubbleProps) => import("react/jsx-runtime").JSX.Element;
17
+ declare const MessageBubble: ({ isMine, content, senderName, showSenderName, showTip, isInternal, id, isTransparent, timeLabel, timeRevealed, onToggleTimeReveal, }: MessageBubbleProps) => import("react/jsx-runtime").JSX.Element;
14
18
  export default MessageBubble;
15
19
  //# sourceMappingURL=MessageBubble.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MessageBubble.d.ts","sourceRoot":"","sources":["../../../../src/components/message/item/MessageBubble.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEjD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,SAAS,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA6CD,QAAA,MAAM,aAAa,GAAI,qGAUpB,kBAAkB,4CAkEpB,CAAC;AAEF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"MessageBubble.d.ts","sourceRoot":"","sources":["../../../../src/components/message/item/MessageBubble.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,SAAS,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,8EAA8E;IAC9E,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;CACjC;AA6CD,QAAA,MAAM,aAAa,GAAI,uIAYpB,kBAAkB,4CAiEpB,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import clsx from "clsx";
4
- import { useState } from "react";
5
4
  const INCOMING_TAIL_PATH = "M1.3798 18H4V0C3.51762 4.5 3.34269 10.5 0.966518 14C-0.568384 16.2608 -0.159364 18 1.3798 18Z";
6
5
  const OUTGOING_TAIL_PATH = "M2.6202 18H0V0C0.482375 4.5 0.657308 10.5 3.03348 14C4.56838 16.2608 4.15936 18 2.6202 18Z";
7
6
  const MessageBubbleTail = ({ isMine, isInternal, }) => (_jsx("div", { className: clsx("flex shrink-0 items-center justify-center", isInternal
@@ -9,10 +8,9 @@ const MessageBubbleTail = ({ isMine, isInternal, }) => (_jsx("div", { className:
9
8
  : isMine
10
9
  ? "text-chat-bubble-mine"
11
10
  : "text-chat-bubble-neutral"), "data-cp-name": "MessageBubbleTail", children: _jsx("div", { className: "flex-none", children: _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "4", height: "18", viewBox: "0 0 4 18", fill: "none", "aria-hidden": true, children: _jsx("path", { d: isMine ? OUTGOING_TAIL_PATH : INCOMING_TAIL_PATH, fill: "currentColor", fillOpacity: "0.1" }) }) }) }));
12
- const MessageBubble = ({ isMine, content, senderName, showSenderName, showTip = true, isInternal = false, id, isTransparent = false, timeLabel, }) => {
11
+ const MessageBubble = ({ isMine, content, senderName, showSenderName, showTip = true, isInternal = false, id, isTransparent = false, timeLabel, timeRevealed = false, onToggleTimeReveal, }) => {
13
12
  const showTail = showTip && !isTransparent;
14
- const [timeRevealed, setTimeRevealed] = useState(false);
15
- const showTimeLabel = Boolean(timeLabel) && (showTip || timeRevealed);
13
+ const showTimeLabel = Boolean(timeLabel) && timeRevealed;
16
14
  const bubbleClass = clsx("relative min-w-0 max-w-full", isTransparent
17
15
  ? "bg-transparent p-0"
18
16
  : clsx("p-2", showTip
@@ -25,8 +23,8 @@ const MessageBubble = ({ isMine, content, senderName, showSenderName, showTip =
25
23
  ? "bg-chat-bubble-mine/10"
26
24
  : "bg-chat-bubble-neutral/10"));
27
25
  return (_jsxs("div", { className: "relative flex min-w-0 max-w-full items-end", "data-testid": "message-bubble", "data-cp-name": "MessageBubble", onClick: () => {
28
- if (!showTip && timeLabel)
29
- setTimeRevealed((prev) => !prev);
26
+ if (timeLabel)
27
+ onToggleTimeReveal === null || onToggleTimeReveal === void 0 ? void 0 : onToggleTimeReveal();
30
28
  }, children: [!isMine && showTail && (_jsx(MessageBubbleTail, { isMine: isMine, isInternal: isInternal })), _jsx("div", { className: bubbleClass, id: id, children: _jsxs("div", { className: clsx("flex flex-col gap-1", !isTransparent && "px-1"), children: [showSenderName && senderName && (_jsx("span", { className: "text-[12px] font-medium leading-[160%] tracking-[0.12px] text-gray-400", children: senderName })), _jsx("div", { className: "flex flex-col gap-2", children: content })] }) }), isMine && showTail && (_jsx(MessageBubbleTail, { isMine: isMine, isInternal: isInternal })), showTimeLabel && (_jsx("span", { "data-testid": "message-time-label", className: clsx("absolute -bottom-2 flex h-[19px] w-[30px] items-center justify-center rounded-full bg-white text-[10px] text-gray-400 shadow-sm", isMine ? "right-1" : "left-1"), children: timeLabel }))] }));
31
29
  };
32
30
  export default MessageBubble;
@@ -12,7 +12,11 @@ interface MessageItemProps {
12
12
  onPinMessage?: (message: MessageItemType) => void;
13
13
  onUnpinMessage?: (message: MessageItemType) => void;
14
14
  enableLongPress?: boolean;
15
+ /** Whether this message is the single one with its time label revealed. */
16
+ timeRevealed?: boolean;
17
+ /** Called on bubble tap to toggle this message as the revealed one. */
18
+ onToggleTimeReveal?: (clientMsgID: string) => void;
15
19
  }
16
- declare const MessageItem: ({ message, allMessages, isGroupChat, forceHideTip, contextMenuOpen, onContextMenuOpenChange, onRevokeMessage, onQuoteMessage, onPressQuoteMessage, onPinMessage, onUnpinMessage, enableLongPress, }: MessageItemProps) => import("react/jsx-runtime").JSX.Element | null;
20
+ declare const MessageItem: ({ message, allMessages, isGroupChat, forceHideTip, contextMenuOpen, onContextMenuOpenChange, onRevokeMessage, onQuoteMessage, onPressQuoteMessage, onPinMessage, onUnpinMessage, enableLongPress, timeRevealed, onToggleTimeReveal, }: MessageItemProps) => import("react/jsx-runtime").JSX.Element | null;
17
21
  export default MessageItem;
18
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/message/item/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,WAAW,IAAI,eAAe,EAG/B,MAAM,8BAA8B,CAAC;AAwCtC,UAAU,gBAAgB;IACxB,OAAO,EAAE,eAAe,CAAC;IACzB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uBAAuB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,eAAe,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IACpD,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IAClD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IACpD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AA0BD,QAAA,MAAM,WAAW,GAAI,qMAalB,gBAAgB,mDA0TlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/message/item/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,WAAW,IAAI,eAAe,EAG/B,MAAM,8BAA8B,CAAC;AAyCtC,UAAU,gBAAgB;IACxB,OAAO,EAAE,eAAe,CAAC;IACzB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uBAAuB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,eAAe,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IACpD,mBAAmB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IAClD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IACpD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AA2BD,QAAA,MAAM,WAAW,GAAI,uOAelB,gBAAgB,mDA4UlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -6,7 +6,7 @@ import { Avatar, Dropdown, message as toastMessage } from "antd";
6
6
  import { MessageStatus, MessageType, } from "@droppii-org/wasm-client-sdk";
7
7
  import MessageBubble from "./MessageBubble";
8
8
  import { renderBubbleContent } from "./renderMessageContent";
9
- import { getVisibleNeighbor, isMessageHiddenForViewer, } from "../../../hooks/message/useMessage";
9
+ import { getImmediateNeighbor, getVisibleNeighbor, isMessageHiddenForViewer, } from "../../../hooks/message/useMessage";
10
10
  import { MSG_ITEM_CONTENT_PREFIX, MSG_ITEM_PREFIX } from "../../../constants";
11
11
  import { formatDividerTimestamp } from "../../../utils/common";
12
12
  import { CustomMessageType } from "../../../types/customMessage";
@@ -26,12 +26,13 @@ import { buildMessageToolboxMenuItems, getCopyableMessageText, isCopyableMessage
26
26
  const MESSAGE_AVATAR_SIZE = 24;
27
27
  const DIVIDER_GAP_MINUTES = 30;
28
28
  const GROUPING_GAP_MINUTES = 5;
29
+ const GROUPING_GAP_MS = GROUPING_GAP_MINUTES * 60 * 1000;
29
30
  const isOrderMessage = (message) => { var _a; return ((_a = message === null || message === void 0 ? void 0 : message.customElem) === null || _a === void 0 ? void 0 : _a.extension) === CustomMessageType.Order; };
30
31
  const MessageAvatar = ({ avatarUrl, senderNickname, }) => {
31
32
  var _a;
32
33
  return (_jsx(Avatar, { size: MESSAGE_AVATAR_SIZE, src: avatarUrl, className: "shrink-0 self-end", "data-cp-name": "MessageAvatar", children: ((_a = senderNickname === null || senderNickname === void 0 ? void 0 : senderNickname.charAt) === null || _a === void 0 ? void 0 : _a.call(senderNickname, 0)) || "A" }));
33
34
  };
34
- const MessageItem = ({ message, allMessages, isGroupChat = false, forceHideTip = false, contextMenuOpen, onContextMenuOpenChange, onRevokeMessage, onQuoteMessage, onPressQuoteMessage, onPinMessage, onUnpinMessage, enableLongPress = false, }) => {
35
+ const MessageItem = ({ message, allMessages, isGroupChat = false, forceHideTip = false, contextMenuOpen, onContextMenuOpenChange, onRevokeMessage, onQuoteMessage, onPressQuoteMessage, onPinMessage, onUnpinMessage, enableLongPress = false, timeRevealed = false, onToggleTimeReveal, }) => {
35
36
  var _a;
36
37
  const { t } = useTranslation();
37
38
  const { resendMessage } = useSendMessage();
@@ -135,18 +136,33 @@ const MessageItem = ({ message, allMessages, isGroupChat = false, forceHideTip =
135
136
  const previousMessage = getVisibleNeighbor(allMessages, message, "prev");
136
137
  const nextMessage = getVisibleNeighbor(allMessages, message, "next");
137
138
  const isOrder = isOrderMessage(message);
139
+ // A system-log or session-event item breaks the group even though it's
140
+ // skipped by getVisibleNeighbor's "next ordinary message" lookup — so
141
+ // check the *immediate* array neighbor here, not the nearest ordinary one.
142
+ const immediatePrev = getImmediateNeighbor(allMessages, message, "prev");
143
+ const immediateNext = getImmediateNeighbor(allMessages, message, "next");
144
+ const prevBreaksGroup = immediatePrev
145
+ ? isSystemLogMessageType(immediatePrev.contentType) ||
146
+ isSessionEventMessage(immediatePrev)
147
+ : false;
148
+ const nextBreaksGroup = immediateNext
149
+ ? isSystemLogMessageType(immediateNext.contentType) ||
150
+ isSessionEventMessage(immediateNext)
151
+ : false;
138
152
  const prevSameUser = !isOrder &&
153
+ !prevBreaksGroup &&
139
154
  !!previousMessage &&
140
155
  !isOrderMessage(previousMessage) &&
141
156
  previousMessage.sendID === (message === null || message === void 0 ? void 0 : message.sendID) &&
142
- Math.abs(dayjs(message.sendTime).diff(previousMessage.sendTime, "minute")) <=
143
- GROUPING_GAP_MINUTES;
157
+ Math.abs(dayjs(message.sendTime).diff(previousMessage.sendTime, "millisecond")) <=
158
+ GROUPING_GAP_MS;
144
159
  const nextSameUser = !isOrder &&
160
+ !nextBreaksGroup &&
145
161
  !!nextMessage &&
146
162
  !isOrderMessage(nextMessage) &&
147
163
  nextMessage.sendID === (message === null || message === void 0 ? void 0 : message.sendID) &&
148
- Math.abs(dayjs(nextMessage.sendTime).diff(message.sendTime, "minute")) <=
149
- GROUPING_GAP_MINUTES;
164
+ Math.abs(dayjs(nextMessage.sendTime).diff(message.sendTime, "millisecond")) <=
165
+ GROUPING_GAP_MS;
150
166
  const showTimeBreak = !previousMessage ||
151
167
  !dayjs(message.sendTime).isSame(previousMessage.sendTime, "days") ||
152
168
  Math.abs(dayjs(message.sendTime).diff(previousMessage.sendTime, "minute")) >
@@ -178,7 +194,7 @@ const MessageItem = ({ message, allMessages, isGroupChat = false, forceHideTip =
178
194
  timeBreakNode,
179
195
  _jsx(SessionEventMessageItem, { message: message })));
180
196
  }
181
- const bubbleNode = (_jsx(MessageBubble, { isMine: isMine, isInternal: isInternalMessage, senderName: message === null || message === void 0 ? void 0 : message.senderNickname, showSenderName: showSenderName, showTip: showTip, id: `${MSG_ITEM_CONTENT_PREFIX}${message === null || message === void 0 ? void 0 : message.clientMsgID}`, content: renderBubbleContent(message, onPressQuoteMessage), isTransparent: isStickerMessage, timeLabel: dayjs.tz(message.sendTime).format("HH:mm") }));
197
+ const bubbleNode = (_jsx(MessageBubble, { isMine: isMine, isInternal: isInternalMessage, senderName: message === null || message === void 0 ? void 0 : message.senderNickname, showSenderName: showSenderName, showTip: showTip, id: `${MSG_ITEM_CONTENT_PREFIX}${message === null || message === void 0 ? void 0 : message.clientMsgID}`, content: renderBubbleContent(message, onPressQuoteMessage), isTransparent: isStickerMessage, timeLabel: dayjs.tz(message.sendTime).format("HH:mm"), timeRevealed: timeRevealed, onToggleTimeReveal: () => onToggleTimeReveal === null || onToggleTimeReveal === void 0 ? void 0 : onToggleTimeReveal(message.clientMsgID) }));
182
198
  const showPinnedBadge = Boolean(message === null || message === void 0 ? void 0 : message.isPinned);
183
199
  const pinnedBadgeNode = showPinnedBadge ? (_jsx("span", { "data-testid": "message-pinned-badge", className: clsx("absolute -top-2.5 z-[1] flex h-[22px] w-[26px] items-center justify-center rounded-full border border-white bg-gray-25", isMine ? "left-0" : "right-0"), children: _jsx(Icon, { icon: "thumbtack-b", size: 12, className: "text-primary-400" }) })) : null;
184
200
  const wrappedBubble = hasContextMenu ? (_jsx(Dropdown, { menu: { items: contextMenuItems }, trigger: ["contextMenu"], open: contextMenuOpen, onOpenChange: onContextMenuOpenChange, overlayClassName: MESSAGE_TOOLBOX_OVERLAY_CLASSNAME, children: _jsxs("div", { ref: bubbleWrapperRef, className: "relative min-w-0 max-w-full", children: [bubbleNode, pinnedBadgeNode] }) })) : (_jsxs("div", { className: "relative min-w-0 max-w-full", children: [bubbleNode, pinnedBadgeNode] }));
@@ -26,4 +26,5 @@ export declare const pushNewMessage: (message: MessageItem) => void;
26
26
  export declare const updateOneMessage: (message: MessageItem) => void;
27
27
  export declare const removeOneMessage: (clientMsgID: string) => void;
28
28
  export declare const getVisibleNeighbor: (allMessages: MessageItem[], current: MessageItem, direction: "prev" | "next") => MessageItem | undefined;
29
+ export declare const getImmediateNeighbor: (allMessages: MessageItem[], current: MessageItem, direction: "prev" | "next") => MessageItem | undefined;
29
30
  //# sourceMappingURL=useMessage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useMessage.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useMessage.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,WAAW,EACX,WAAW,EAEZ,MAAM,8BAA8B,CAAC;AAatC,eAAO,MAAM,kBAAkB,eAkB9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,WAAW,EACpB,OAAO,OAAO,EACd,eAAe;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,KAC9D,OAqBF,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,iBAAiB,MAAM,EACvB,oBAAoB,MAAM,EAC1B,iBAAc,EACd,uBAAqB;;;;;qBAeA,WAAW,EAAE;;;;;;qBAAb,WAAW,EAAE;;;;;;CAsSnC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,SACpB,CAAC;AAChC,eAAO,MAAM,gBAAgB,GAAI,SAAS,WAAW,SACpB,CAAC;AAClC,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,SACf,CAAC;AAEtC,eAAO,MAAM,kBAAkB,GAC7B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,KACzB,WAAW,GAAG,SAkBhB,CAAC"}
1
+ {"version":3,"file":"useMessage.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useMessage.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,WAAW,EACX,WAAW,EAEZ,MAAM,8BAA8B,CAAC;AAatC,eAAO,MAAM,kBAAkB,eAkB9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,WAAW,EACpB,OAAO,OAAO,EACd,eAAe;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,KAC9D,OAqBF,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,iBAAiB,MAAM,EACvB,oBAAoB,MAAM,EAC1B,iBAAc,EACd,uBAAqB;;;;;qBAeA,WAAW,EAAE;;;;;;qBAAb,WAAW,EAAE;;;;;;CAsSnC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,SACpB,CAAC;AAChC,eAAO,MAAM,gBAAgB,GAAI,SAAS,WAAW,SACpB,CAAC;AAClC,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,SACf,CAAC;AAEtC,eAAO,MAAM,kBAAkB,GAC7B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,KACzB,WAAW,GAAG,SAkBhB,CAAC;AAMF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,KACzB,WAAW,GAAG,SAQhB,CAAC"}
@@ -303,6 +303,17 @@ export const getVisibleNeighbor = (allMessages, current, direction) => {
303
303
  }
304
304
  return undefined;
305
305
  };
306
+ // Unlike getVisibleNeighbor, this does not skip past system-log/session-event
307
+ // items — used to detect when one of those sits directly next to a message,
308
+ // since such an item must end the current sender's group even though it is
309
+ // not itself a group-breaking "different sender" message.
310
+ export const getImmediateNeighbor = (allMessages, current, direction) => {
311
+ const currentIndex = allMessages.findIndex((m) => m.clientMsgID === current.clientMsgID);
312
+ if (currentIndex === -1)
313
+ return undefined;
314
+ const step = direction === "prev" ? 1 : -1;
315
+ return allMessages[currentIndex + step];
316
+ };
306
317
  const removeDuplicateMessages = (messages) => {
307
318
  const seen = new Set();
308
319
  return messages.filter((msg) => {
@@ -72,7 +72,7 @@
72
72
  "profile_hint": "Nhấn để xem thông tin",
73
73
  "options_title": "Tuỳ chọn",
74
74
  "input_placeholder": "Nhập tin nhắn...",
75
- "view_only_notice": "Bạn không còn thành viên của nhóm này. Bạn chỉ có thể xem lại nội dung trò chuyện.",
75
+ "view_only_notice": "Bạn cần tham gia nhóm để gửi tin nhắn",
76
76
  "resolve_failed": "Không thể mở cuộc trò chuyện",
77
77
  "more_option": {
78
78
  "toggle": "Tùy chọn khác",
@@ -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;AAM3C,QAAA,MAAM,oBAAoB,gFAkXvB,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;AAO3C,QAAA,MAAM,oBAAoB,gFAqXvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}
@@ -2,6 +2,7 @@ import { GroupVisibility, SessionType, } from "@droppii-org/wasm-client-sdk";
2
2
  import { create } from "zustand";
3
3
  import { DChatSDK } from "../constants/sdk";
4
4
  import { conversationSort, isGroupConversation } from "../utils/imCommon";
5
+ import { PeerType } from "../types/conversation";
5
6
  import useUserStore from "./user";
6
7
  const CONVERSATION_SPLIT_COUNT = 500;
7
8
  const useConversationStore = create((set, get) => ({
@@ -133,13 +134,16 @@ const useConversationStore = create((set, get) => ({
133
134
  // derive the same public->view-only / private->block state from the
134
135
  // fetch/cache above. currentConversation isn't set until after this
135
136
  // block, so guard via conversationData's groupID instead.
137
+ // Require confirmed peerType — a CX/bot session can ride a Group
138
+ // session with peerType missing from a raw fetch; don't block on that.
139
+ const isConfirmedGroup = conversation.peerType === PeerType.GROUP;
136
140
  const stillOpen = ((_a = get().conversationData) === null || _a === void 0 ? void 0 : _a.groupID) === groupID;
137
141
  const notAMember = !get().currentMemberInGroup.has(groupID);
138
142
  const isPublic = ((_b = get().currentGroupInfo.get(groupID)) === null || _b === void 0 ? void 0 : _b.visibility) === GroupVisibility.Public;
139
- if (stillOpen && notAMember && isPublic) {
143
+ if (isConfirmedGroup && stillOpen && notAMember && isPublic) {
140
144
  set({ isViewOnlyMember: true });
141
145
  }
142
- else if (stillOpen && notAMember) {
146
+ else if (isConfirmedGroup && stillOpen && notAMember) {
143
147
  get().clearSelectedConversation();
144
148
  get().showRemovedFromGroupModal();
145
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droppii-org/chat-sdk",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Droppii React Chat SDK",
5
5
  "repository": {
6
6
  "type": "git",