@droppii-org/chat-sdk 0.6.43 → 0.6.45

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.
Files changed (38) hide show
  1. package/dist/components/conversation/ConversationBySessionItem.d.ts.map +1 -1
  2. package/dist/components/conversation/ConversationBySessionItem.js +5 -2
  3. package/dist/components/conversation-detail/ConversationMessageList.d.ts.map +1 -1
  4. package/dist/components/conversation-detail/ConversationMessageList.js +16 -2
  5. package/dist/components/group/AdminPermissionModal.d.ts.map +1 -1
  6. package/dist/components/group/AdminPermissionModal.js +52 -74
  7. package/dist/components/message/MessageHeader.d.ts.map +1 -1
  8. package/dist/components/message/MessageHeader.js +22 -2
  9. package/dist/components/message/MessageList.d.ts.map +1 -1
  10. package/dist/components/message/MessageList.js +17 -3
  11. package/dist/components/message/UnreadMessageDivider.d.ts +7 -0
  12. package/dist/components/message/UnreadMessageDivider.d.ts.map +1 -0
  13. package/dist/components/message/UnreadMessageDivider.js +28 -0
  14. package/dist/components/message/UnreadMessageJumpButton.d.ts +7 -0
  15. package/dist/components/message/UnreadMessageJumpButton.d.ts.map +1 -0
  16. package/dist/components/message/UnreadMessageJumpButton.js +10 -0
  17. package/dist/components/message/item/index.d.ts +4 -1
  18. package/dist/components/message/item/index.d.ts.map +1 -1
  19. package/dist/components/message/item/index.js +14 -5
  20. package/dist/hooks/conversation/useConversation.js +1 -1
  21. package/dist/hooks/message/useMessageListScroll.d.ts +3 -1
  22. package/dist/hooks/message/useMessageListScroll.d.ts.map +1 -1
  23. package/dist/hooks/message/useMessageListScroll.js +10 -2
  24. package/dist/hooks/message/useUnreadMessageIndicator.d.ts +22 -0
  25. package/dist/hooks/message/useUnreadMessageIndicator.d.ts.map +1 -0
  26. package/dist/hooks/message/useUnreadMessageIndicator.js +133 -0
  27. package/dist/hooks/session/useSessionLatestPreview.d.ts.map +1 -1
  28. package/dist/hooks/session/useSessionLatestPreview.js +4 -1
  29. package/dist/locales/vi/common.json +2 -0
  30. package/dist/styles/global.css +1 -1
  31. package/dist/utils/common.d.ts.map +1 -1
  32. package/dist/utils/common.js +3 -0
  33. package/dist/utils/conversation.d.ts +15 -0
  34. package/dist/utils/conversation.d.ts.map +1 -1
  35. package/dist/utils/conversation.js +17 -0
  36. package/dist/utils/imCommon.d.ts +1 -0
  37. package/dist/utils/imCommon.d.ts.map +1 -1
  38. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationBySessionItem.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/ConversationBySessionItem.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAYhD,UAAU,8BAA8B;IACtC,WAAW,EAAE,gBAAgB,CAAC;IAC9B,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,QAAA,MAAM,yBAAyB,GAAI,iCAGhC,8BAA8B,mDA+EhC,CAAC;AAEF,eAAe,yBAAyB,CAAC"}
1
+ {"version":3,"file":"ConversationBySessionItem.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/ConversationBySessionItem.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAahD,UAAU,8BAA8B;IACtC,WAAW,EAAE,gBAAgB,CAAC;IAC9B,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,QAAA,MAAM,yBAAyB,GAAI,iCAGhC,8BAA8B,mDAyFhC,CAAC;AAEF,eAAe,yBAAyB,CAAC"}
@@ -8,11 +8,12 @@ import { useChatNavigate } from "../../hooks/conversation/useChatNavigate";
8
8
  import { useConversationDisplayData } from "../../hooks/conversation/useConversation";
9
9
  import { formatTimestamp, parseLatestMessage } from "../../utils/common";
10
10
  import { isGroupSession } from "../../utils/imCommon";
11
+ import { resolveBotSessionOwnerIdentity } from "../../utils/conversation";
11
12
  import GroupCompositeAvatar from "../../components/group/GroupCompositeAvatar";
12
13
  import ThreadCard from "./ThreadCard";
13
14
  import MentionIndicatorIcon from "./MentionIndicatorIcon";
14
15
  const ConversationBySessionItem = ({ sessionItem, latestPreview, }) => {
15
- var _a;
16
+ var _a, _b;
16
17
  const { t } = useTranslation();
17
18
  const { user } = useChatContext();
18
19
  const onNavigate = useChatNavigate();
@@ -35,6 +36,8 @@ const ConversationBySessionItem = ({ sessionItem, latestPreview, }) => {
35
36
  setSelectedConversationId(conversation.conversationID);
36
37
  };
37
38
  const { avatar, displayName = "" } = useConversationDisplayData(conversation || null);
39
+ const ownerIdentity = resolveBotSessionOwnerIdentity(conversation, sessionItem === null || sessionItem === void 0 ? void 0 : sessionItem.owner, user === null || user === void 0 ? void 0 : user.userID);
40
+ const title = (_a = ownerIdentity === null || ownerIdentity === void 0 ? void 0 : ownerIdentity.displayName) !== null && _a !== void 0 ? _a : displayName;
38
41
  const latestMsg = (latestPreview === null || latestPreview === void 0 ? void 0 : latestPreview.latestMsg) || "";
39
42
  const latestMsgSendTime = (latestPreview === null || latestPreview === void 0 ? void 0 : latestPreview.latestMsgSendTime) || 0;
40
43
  if (!conversation)
@@ -45,6 +48,6 @@ const ConversationBySessionItem = ({ sessionItem, latestPreview, }) => {
45
48
  showName: conversation.showName,
46
49
  }), mentionIndicator: _jsx(MentionIndicatorIcon, { groupAtType: conversation.groupAtType }), time: latestMsgSendTime > 0
47
50
  ? formatTimestamp(latestMsgSendTime, { hasTime: true })
48
- : undefined, avatar: isGroupSession(conversation.conversationType) ? (_jsx(GroupCompositeAvatar, { groupID: conversation.groupID, faceURL: avatar, groupName: displayName, size: 48 })) : (_jsx(Avatar, { size: 48, src: avatar, children: ((_a = displayName === null || displayName === void 0 ? void 0 : displayName.charAt) === null || _a === void 0 ? void 0 : _a.call(displayName, 0)) || "A" })), title: _jsx("h3", { className: "truncate text-sm font-semibold text-gray-900", children: displayName }) }));
51
+ : undefined, avatar: ownerIdentity ? (_jsx(Avatar, { size: 48, src: ownerIdentity.avatarUrl, children: title.charAt(0) })) : isGroupSession(conversation.conversationType) ? (_jsx(GroupCompositeAvatar, { groupID: conversation.groupID, faceURL: avatar, groupName: displayName, size: 48 })) : (_jsx(Avatar, { size: 48, src: avatar, children: ((_b = displayName === null || displayName === void 0 ? void 0 : displayName.charAt) === null || _b === void 0 ? void 0 : _b.call(displayName, 0)) || "A" })), title: _jsx("h3", { className: "truncate text-sm font-semibold text-gray-900", children: title }) }));
49
52
  };
50
53
  export default ConversationBySessionItem;
@@ -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;AAsBzE,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAED,QAAA,MAAM,uBAAuB,GAAI,4DAI9B,4BAA4B,4CAsO9B,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;AAwBzE,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAED,QAAA,MAAM,uBAAuB,GAAI,4DAI9B,4BAA4B,4CAmQ9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -8,6 +8,7 @@ import { useChatContext } from "../../context/ChatContext";
8
8
  import MessageInfiniteScroll from "../../components/message/MessageInfiniteScroll";
9
9
  import { useMessage } from "../../hooks/message/useMessage";
10
10
  import { useMessageListScroll } from "../../hooks/message/useMessageListScroll";
11
+ import { useUnreadMessageIndicator } from "../../hooks/message/useUnreadMessageIndicator";
11
12
  import { usePinMessage } from "../../hooks/message/usePinMessage";
12
13
  import { useRevokeMessage } from "../../hooks/message/useRevokeMessage";
13
14
  import { useGroupPermission } from "../../hooks/group/useGroupPermission";
@@ -16,6 +17,7 @@ import useAuthStore from "../../store/auth";
16
17
  import { isGroupChat as resolveIsGroupChat, isSupportSessionAgentView, } from "../../utils/conversation";
17
18
  import emitter from "../../utils/events";
18
19
  import MessageItem from "../../components/message/item";
20
+ import UnreadMessageJumpButton from "../../components/message/UnreadMessageJumpButton";
19
21
  import PinLimitModal from "./PinLimitModal";
20
22
  const ConversationMessageList = ({ bottomSlot, onCopyMessageLink, onSearchMessageRevoked, }) => {
21
23
  const { t } = useTranslation("conversation-inbox");
@@ -38,7 +40,12 @@ const ConversationMessageList = ({ bottomSlot, onCopyMessageLink, onSearchMessag
38
40
  // agent handling someone else's session needs — load the full history.
39
41
  const useAppHistory = !isSupportSessionAgentView(Object.assign(Object.assign({}, conversation), { conversationID: conversationId }), viewerUserID);
40
42
  const messageController = useMessage(conversationId, searchClientMsgID, true, useAppHistory, onSearchMessageRevoked);
41
- const { scrollRef, loadMoreOldMessage, handleInfiniteScroll, scrollToBottomIfAtBottom, scrollToMessage, } = useMessageListScroll(Object.assign({ conversationId, userId: viewerUserID, isGroupChat }, messageController));
43
+ const { isResolved: isUnreadLookupResolved, firstUnreadMessageId, unreadDividerMessageId, showJumpButton, markDividerSeen, } = useUnreadMessageIndicator({
44
+ conversationId,
45
+ searchClientMsgID,
46
+ messageList: messageController.loadState.messageList,
47
+ });
48
+ const { scrollRef, loadMoreOldMessage, handleInfiniteScroll, scrollToBottomIfAtBottom, scrollToMessage, } = useMessageListScroll(Object.assign({ conversationId, userId: viewerUserID, isGroupChat, markAsReadBlocked: !isUnreadLookupResolved }, messageController));
42
49
  const { loadState, moreOldLoading } = messageController;
43
50
  const isMessageLoading = !conversationId || loadState.initLoading;
44
51
  const [openToolboxMessageId, setOpenToolboxMessageId] = useState(null);
@@ -99,6 +106,12 @@ const ConversationMessageList = ({ bottomSlot, onCopyMessageLink, onSearchMessag
99
106
  setSearchClientMsgID(clientMsgID);
100
107
  }
101
108
  }, [scrollToMessage, loadState.messageList, setSearchClientMsgID]);
109
+ const handleJumpToFirstUnread = useCallback(() => {
110
+ if (!firstUnreadMessageId)
111
+ return;
112
+ markDividerSeen();
113
+ onPressQuoteMessage(firstUnreadMessageId);
114
+ }, [firstUnreadMessageId, markDividerSeen, onPressQuoteMessage]);
102
115
  // MessageItem only knows its own clientMsgID — bind the conversation here,
103
116
  // and keep the callback undefined when the host gave us none so MessageItem
104
117
  // can drop the menu entry instead of rendering a no-op.
@@ -119,6 +132,7 @@ const ConversationMessageList = ({ bottomSlot, onCopyMessageLink, onSearchMessag
119
132
  target: scrollRef,
120
133
  enable: !!openToolboxMessageId,
121
134
  });
122
- 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, onCopyMessageLink: handleCopyMessageLink, enableLongPress: true }, message.clientMsgID)))] })), _jsx(PinLimitModal, { open: pinLimitModalOpen, max: pinnedTotalCount, onClose: () => setPinLimitModalOpen(false) })] }));
135
+ 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, onCopyMessageLink: handleCopyMessageLink, showUnreadDivider: !!unreadDividerMessageId &&
136
+ message.clientMsgID === unreadDividerMessageId, onUnreadDividerSeen: markDividerSeen, enableLongPress: true }, message.clientMsgID)))] })), !isMessageLoading && showJumpButton && (_jsx(UnreadMessageJumpButton, { className: "absolute right-0 top-2 z-10", onClick: handleJumpToFirstUnread })), _jsx(PinLimitModal, { open: pinLimitModalOpen, max: pinnedTotalCount, onClose: () => setPinLimitModalOpen(false) })] }));
123
137
  };
124
138
  export default ConversationMessageList;
@@ -1 +1 @@
1
- {"version":3,"file":"AdminPermissionModal.d.ts","sourceRoot":"","sources":["../../../src/components/group/AdminPermissionModal.tsx"],"names":[],"mappings":"AAcA,UAAU,0BAA0B;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,UAAU,yBAAyB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IAIpB,MAAM,CAAC,EAAE,OAAO,CAAC;IAKjB,MAAM,CAAC,EAAE,0BAA0B,CAAC;IAKpC,wBAAwB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAC5E;AA6DD,QAAA,MAAM,oBAAoB,GAAI,uEAO3B,yBAAyB,mDA2M3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"AdminPermissionModal.d.ts","sourceRoot":"","sources":["../../../src/components/group/AdminPermissionModal.tsx"],"names":[],"mappings":"AAaA,UAAU,0BAA0B;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,UAAU,yBAAyB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IAIpB,MAAM,CAAC,EAAE,OAAO,CAAC;IAGjB,MAAM,CAAC,EAAE,0BAA0B,CAAC;IAKpC,wBAAwB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAC5E;AA6DD,QAAA,MAAM,oBAAoB,GAAI,uEAO3B,yBAAyB,mDA2K3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -4,97 +4,72 @@ import { useEffect, useState } from "react";
4
4
  import { ConfigProvider, Modal, Switch, message } from "antd";
5
5
  import { useTranslation } from "react-i18next";
6
6
  import { GroupMemberRole } from "@droppii-org/wasm-client-sdk";
7
- import { useGroupPermission } from "../../hooks/group/useGroupPermission";
8
7
  import { useGroupMembers } from "../../hooks/group/useGroupMembers";
9
8
  import { useSetGroupMemberPermissions } from "../../hooks/group/useGroupMemberActions";
10
9
  import { Icon } from "../../components/icon";
11
10
  import { ANTD_PRIMARY_COLOR_VAR } from "../../constants";
12
11
  import { GROUP_PERMISSION_KEY } from "../../constants/im";
13
12
  // "Cập nhật thông tin nhóm" is one switch in the UI but bundles 3 BE
14
- // permission keys (name/avatar/description) — DROPPII-30533 fix: reading
15
- // only checked UPDATE_GROUP_NAME, so turning the switch off only ever
16
- // stripped that one key, leaving avatar/description at the role default.
13
+ // permission keys (name/avatar/description), so it maps to all three.
17
14
  const UPDATE_GROUP_INFO_KEYS = [
18
15
  GROUP_PERMISSION_KEY.updateGroupName,
19
16
  GROUP_PERMISSION_KEY.updateGroupAvatar,
20
17
  GROUP_PERMISSION_KEY.updateGroupDescription,
21
18
  ];
22
- const permissionListToOverrides = (permissionList) => ({
23
- sendMessage: permissionList.includes(GROUP_PERMISSION_KEY.sendMessage),
24
- pinMessage: permissionList.includes(GROUP_PERMISSION_KEY.pinMessage),
25
- viewMemberInformation: permissionList.includes(GROUP_PERMISSION_KEY.viewMemberInformation),
26
- addMember: permissionList.includes(GROUP_PERMISSION_KEY.addMember),
27
- removeMember: permissionList.includes(GROUP_PERMISSION_KEY.removeMember),
28
- updateGroupInfo: UPDATE_GROUP_INFO_KEYS.every((key) => permissionList.includes(key)),
29
- });
30
- const overridesToPermissionList = (overrides, currentPermissionList) => {
31
- const desired = {
32
- [GROUP_PERMISSION_KEY.sendMessage]: overrides.sendMessage,
33
- [GROUP_PERMISSION_KEY.pinMessage]: overrides.pinMessage,
34
- [GROUP_PERMISSION_KEY.viewMemberInformation]: overrides.viewMemberInformation,
35
- [GROUP_PERMISSION_KEY.addMember]: overrides.addMember,
36
- [GROUP_PERMISSION_KEY.removeMember]: overrides.removeMember,
37
- [GROUP_PERMISSION_KEY.updateGroupName]: overrides.updateGroupInfo,
38
- [GROUP_PERMISSION_KEY.updateGroupAvatar]: overrides.updateGroupInfo,
39
- [GROUP_PERMISSION_KEY.updateGroupDescription]: overrides.updateGroupInfo,
19
+ const SWITCH_PERMISSION_KEYS = {
20
+ sendMessage: [GROUP_PERMISSION_KEY.sendMessage],
21
+ pinMessage: [GROUP_PERMISSION_KEY.pinMessage],
22
+ viewMemberInformation: [GROUP_PERMISSION_KEY.viewMemberInformation],
23
+ addMember: [GROUP_PERMISSION_KEY.addMember],
24
+ removeMember: [GROUP_PERMISSION_KEY.removeMember],
25
+ updateGroupInfo: UPDATE_GROUP_INFO_KEYS,
26
+ };
27
+ const SWITCHES = Object.keys(SWITCH_PERMISSION_KEYS);
28
+ // A switch reads on only when every given permission list grants all of its keys.
29
+ const commonOverrides = (permissionLists) => {
30
+ const grants = (name) => permissionLists.length > 0 &&
31
+ permissionLists.every((list) => SWITCH_PERMISSION_KEYS[name].every((key) => list.includes(key)));
32
+ return {
33
+ sendMessage: grants("sendMessage"),
34
+ pinMessage: grants("pinMessage"),
35
+ viewMemberInformation: grants("viewMemberInformation"),
36
+ addMember: grants("addMember"),
37
+ removeMember: grants("removeMember"),
38
+ updateGroupInfo: grants("updateGroupInfo"),
40
39
  };
41
- const managedKeys = new Set(Object.keys(desired));
42
- const untouched = currentPermissionList.filter((permission) => !managedKeys.has(permission));
43
- const enabled = Object.entries(desired)
44
- .filter(([, isEnabled]) => isEnabled)
45
- .map(([permission]) => permission);
46
- return [...untouched, ...enabled];
47
40
  };
48
- // "Configure Admin Permissions" (DROPPII-30248 follow-up). When `member` is
49
- // passed (from the member list's per-row menu), it patches that one
50
- // member's permission list via setGroupMemberInfo. When used group-wide (no
51
- // `member`, e.g. from GroupThreadInfo's group settings), "Lưu" applies the
52
- // same permission set to every current Admin in the group (DROPPII-30529/
53
- // 30531 fix) — there is no separate BE endpoint for a role-wide default, so
54
- // this fans out the existing per-member call to each current Admin instead
55
- // of only writing to a local override store.
41
+ // Rewrites only the switches that changed; every other key in the list stays as is.
42
+ const applySwitchChanges = (permissionList, changes) => Object.entries(changes).reduce((list, [name, enabled]) => {
43
+ const keys = SWITCH_PERMISSION_KEYS[name];
44
+ const without = list.filter((permission) => !keys.includes(permission));
45
+ return enabled ? [...without, ...keys] : without;
46
+ }, permissionList);
47
+ const isSamePermissionSet = (a, b) => a.length === b.length && a.every((permission) => b.includes(permission));
48
+ // "Configure Admin Permissions". With `member` (a member row's … menu) it edits
49
+ // that one Admin. Without it (the group info panel) it edits every current
50
+ // Admin at once: switches start from what the Admins already share, and "Lưu"
51
+ // writes only the switches the user flipped, so each Admin keeps the rest.
56
52
  const AdminPermissionModal = ({ open, groupID, onClose, inline = false, member, onMemberPermissionsSaved, }) => {
57
53
  const { t } = useTranslation();
58
- const groupPermissions = useGroupPermission(groupID);
59
54
  const { members: groupMembers } = useGroupMembers(member ? undefined : groupID);
60
55
  const { mutateAsync: setMemberPermissions } = useSetGroupMemberPermissions();
61
56
  const [isSaving, setIsSaving] = useState(false);
62
- const isGroupInfoEditable = groupPermissions.updateGroupName &&
63
- groupPermissions.updateGroupAvatar &&
64
- groupPermissions.updateGroupDescription;
65
- const initialOverrides = () => member
66
- ? permissionListToOverrides(member.permissions)
67
- : {
68
- sendMessage: groupPermissions.sendMessage,
69
- pinMessage: groupPermissions.pinMessage,
70
- viewMemberInformation: groupPermissions.viewMemberInformation,
71
- addMember: groupPermissions.addMember,
72
- removeMember: groupPermissions.removeMember,
73
- updateGroupInfo: isGroupInfoEditable,
74
- };
75
- const [pending, setPending] = useState(initialOverrides);
57
+ const [edits, setEdits] = useState({});
76
58
  useEffect(() => {
77
- if (!open)
78
- return;
79
- setPending(initialOverrides());
80
- // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ if (open)
60
+ setEdits({});
81
61
  }, [open, member === null || member === void 0 ? void 0 : member.userID]);
62
+ const admins = groupMembers.filter((groupMember) => groupMember.roleLevel === GroupMemberRole.Admin);
82
63
  const baseline = member
83
- ? permissionListToOverrides(member.permissions)
84
- : {
85
- sendMessage: groupPermissions.sendMessage,
86
- pinMessage: groupPermissions.pinMessage,
87
- viewMemberInformation: groupPermissions.viewMemberInformation,
88
- addMember: groupPermissions.addMember,
89
- removeMember: groupPermissions.removeMember,
90
- updateGroupInfo: isGroupInfoEditable,
91
- };
92
- const hasChanges = pending.sendMessage !== baseline.sendMessage ||
93
- pending.pinMessage !== baseline.pinMessage ||
94
- pending.viewMemberInformation !== baseline.viewMemberInformation ||
95
- pending.addMember !== baseline.addMember ||
96
- pending.removeMember !== baseline.removeMember ||
97
- pending.updateGroupInfo !== baseline.updateGroupInfo;
64
+ ? commonOverrides([member.permissions])
65
+ : commonOverrides(admins.map((admin) => { var _a; return (_a = admin.permissions) !== null && _a !== void 0 ? _a : []; }));
66
+ const pending = Object.assign(Object.assign({}, baseline), edits);
67
+ const changes = Object.fromEntries(SWITCHES.filter((name) => pending[name] !== baseline[name]).map((name) => [
68
+ name,
69
+ pending[name],
70
+ ]));
71
+ const hasChanges = Object.keys(changes).length > 0;
72
+ const setSwitch = (name, checked) => setEdits((prev) => (Object.assign(Object.assign({}, prev), { [name]: checked })));
98
73
  const handleSave = async () => {
99
74
  var _a, _b;
100
75
  if (!groupID)
@@ -102,7 +77,7 @@ const AdminPermissionModal = ({ open, groupID, onClose, inline = false, member,
102
77
  setIsSaving(true);
103
78
  try {
104
79
  if (member) {
105
- const nextPermissions = overridesToPermissionList(pending, member.permissions);
80
+ const nextPermissions = applySwitchChanges(member.permissions, changes);
106
81
  await setMemberPermissions({
107
82
  groupID,
108
83
  userID: member.userID,
@@ -111,13 +86,16 @@ const AdminPermissionModal = ({ open, groupID, onClose, inline = false, member,
111
86
  onMemberPermissionsSaved === null || onMemberPermissionsSaved === void 0 ? void 0 : onMemberPermissionsSaved(member.userID, nextPermissions);
112
87
  }
113
88
  else {
114
- const admins = groupMembers.filter((groupMember) => groupMember.roleLevel === GroupMemberRole.Admin);
115
89
  await Promise.all(admins.map((admin) => {
116
90
  var _a;
91
+ const current = (_a = admin.permissions) !== null && _a !== void 0 ? _a : [];
92
+ const nextPermissions = applySwitchChanges(current, changes);
93
+ if (isSamePermissionSet(current, nextPermissions))
94
+ return undefined;
117
95
  return setMemberPermissions({
118
96
  groupID,
119
97
  userID: admin.userID,
120
- permissions: overridesToPermissionList(pending, (_a = admin.permissions) !== null && _a !== void 0 ? _a : []),
98
+ permissions: nextPermissions,
121
99
  });
122
100
  }));
123
101
  }
@@ -131,7 +109,7 @@ const AdminPermissionModal = ({ open, groupID, onClose, inline = false, member,
131
109
  setIsSaving(false);
132
110
  }
133
111
  };
134
- const rows = (_jsx(ConfigProvider, { theme: { token: { colorPrimary: ANTD_PRIMARY_COLOR_VAR } }, children: _jsxs("div", { className: "flex flex-col py-2", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_send_message") }), _jsx(Switch, { checked: pending.sendMessage, onChange: (checked) => setPending((prev) => (Object.assign(Object.assign({}, prev), { sendMessage: checked }))) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_pin_unpin_message") }), _jsx(Switch, { checked: pending.pinMessage, onChange: (checked) => setPending((prev) => (Object.assign(Object.assign({}, prev), { pinMessage: checked }))) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_view_member_info") }), _jsx(Switch, { checked: pending.viewMemberInformation, onChange: (checked) => setPending((prev) => (Object.assign(Object.assign({}, prev), { viewMemberInformation: checked }))) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_add_member") }), _jsx(Switch, { checked: pending.addMember, onChange: (checked) => setPending((prev) => (Object.assign(Object.assign({}, prev), { addMember: checked }))) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_remove_member") }), _jsx(Switch, { checked: pending.removeMember, onChange: (checked) => setPending((prev) => (Object.assign(Object.assign({}, prev), { removeMember: checked }))) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_update_group_info") }), _jsx(Switch, { checked: pending.updateGroupInfo, onChange: (checked) => setPending((prev) => (Object.assign(Object.assign({}, prev), { updateGroupInfo: checked }))) })] })] }) }));
112
+ const rows = (_jsx(ConfigProvider, { theme: { token: { colorPrimary: ANTD_PRIMARY_COLOR_VAR } }, children: _jsxs("div", { className: "flex flex-col py-2", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_send_message") }), _jsx(Switch, { checked: pending.sendMessage, onChange: (checked) => setSwitch("sendMessage", checked) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_pin_unpin_message") }), _jsx(Switch, { checked: pending.pinMessage, onChange: (checked) => setSwitch("pinMessage", checked) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_view_member_info") }), _jsx(Switch, { checked: pending.viewMemberInformation, onChange: (checked) => setSwitch("viewMemberInformation", checked) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_add_member") }), _jsx(Switch, { checked: pending.addMember, onChange: (checked) => setSwitch("addMember", checked) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_remove_member") }), _jsx(Switch, { checked: pending.removeMember, onChange: (checked) => setSwitch("removeMember", checked) })] }), _jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [_jsx("span", { className: "text-sm", children: t("permission_update_group_info") }), _jsx(Switch, { checked: pending.updateGroupInfo, onChange: (checked) => setSwitch("updateGroupInfo", checked) })] })] }) }));
135
113
  if (inline) {
136
114
  if (!open)
137
115
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageHeader.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageHeader.tsx"],"names":[],"mappings":"AASA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAQ/C,UAAU,kBAAkB;IAC1B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED,KAAK,sBAAsB,GAAG,aAAa,GAAG,UAAU,CAAC;AAEzD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,sBAAsB,CAAC;IAC9B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,QAAA,MAAM,aAAa,GAAI,0DAKpB,kBAAkB,4CA6OpB,CAAC;AAEF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"MessageHeader.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageHeader.tsx"],"names":[],"mappings":"AASA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAgB/C,UAAU,kBAAkB;IAC1B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED,KAAK,sBAAsB,GAAG,aAAa,GAAG,UAAU,CAAC;AAEzD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,sBAAsB,CAAC;IAC9B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,QAAA,MAAM,aAAa,GAAI,0DAKpB,kBAAkB,4CA+QpB,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -16,17 +16,30 @@ import { isGroupSession, isGroupConversation } from "../../utils/imCommon";
16
16
  import { useGroupPermission } from "../../hooks/group/useGroupPermission";
17
17
  import GroupCompositeAvatar from "../../components/group/GroupCompositeAvatar";
18
18
  import GroupMembersDrawer from "./GroupMembersDrawer";
19
+ import { useGroupMembers } from "../../hooks/group/useGroupMembers";
20
+ import { useGetUserSideInfo } from "../../hooks/session/useGetUserSideInfo";
21
+ import { PeerType } from "../../types/conversation";
22
+ import { isSupportSessionAgentView, resolveBotSessionOwnerIdentity, resolveSupportSessionOwnerId, } from "../../utils/conversation";
19
23
  const MessageHeader = ({ onClose, currentSession, isJoined, onOpenThreadInfo, }) => {
20
- var _a, _b;
24
+ var _a, _b, _c, _d, _e;
21
25
  const { t } = useTranslation();
22
26
  const isCx = useAuthStore((state) => state.isCx);
23
27
  const isCrm = useAuthStore((state) => state.isCrm);
28
+ const selfUserID = useAuthStore((state) => state.userID);
24
29
  const conversationData = useConversationStore((state) => state.conversationData);
25
30
  const groupID = conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID;
26
31
  const currentGroupInfo = useConversationStore((state) => groupID ? state.currentGroupInfo.get(groupID) : undefined);
27
32
  const { mutate: updateSession } = useUpdateSession();
28
33
  const permissions = useGroupPermission(conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID);
29
34
  const { avatar, displayName } = useConversationDisplayData(conversationData);
35
+ // Same chat-service user record as the desk list's session owner.
36
+ const botSessionOwnerId = (conversationData === null || conversationData === void 0 ? void 0 : conversationData.peerType) === PeerType.BOT &&
37
+ isSupportSessionAgentView(conversationData, selfUserID)
38
+ ? resolveSupportSessionOwnerId(conversationData)
39
+ : "";
40
+ const { data: botSessionOwner } = useGetUserSideInfo((_a = conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID) !== null && _a !== void 0 ? _a : "", botSessionOwnerId);
41
+ const ownerIdentity = resolveBotSessionOwnerIdentity(conversationData, botSessionOwner, selfUserID);
42
+ const headerName = (_b = ownerIdentity === null || ownerIdentity === void 0 ? void 0 : ownerIdentity.displayName) !== null && _b !== void 0 ? _b : displayName;
30
43
  const shouldShowMemberCount = isGroupSession(conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationType);
31
44
  // A "real" group (peerType GROUP) vs a CX support thread that happens to
32
45
  // use a Group/WorkingGroup session under the hood (e.g. a multi-agent
@@ -38,6 +51,13 @@ const MessageHeader = ({ onClose, currentSession, isJoined, onOpenThreadInfo, })
38
51
  // may view the member list, so it's bypassed there. The member list
39
52
  // button itself still shows for both (shouldShowMemberCount).
40
53
  const isRealGroup = isGroupConversation(conversationData !== null && conversationData !== void 0 ? conversationData : undefined);
54
+ // Group info is only fetched for peerType GROUP; other threads count the
55
+ // same member list the drawer shows, so the two never disagree.
56
+ const usesGroupInfoCount = (conversationData === null || conversationData === void 0 ? void 0 : conversationData.peerType) === PeerType.GROUP;
57
+ const { members: threadMembers } = useGroupMembers(shouldShowMemberCount && !usesGroupInfoCount ? groupID : undefined);
58
+ const memberCount = usesGroupInfoCount
59
+ ? ((_c = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.memberCount) !== null && _c !== void 0 ? _c : 0)
60
+ : threadMembers.length;
41
61
  const canViewMemberList = shouldShowMemberCount &&
42
62
  isJoined === true &&
43
63
  isCrm &&
@@ -145,6 +165,6 @@ const MessageHeader = ({ onClose, currentSession, isJoined, onOpenThreadInfo, })
145
165
  setCurrentSessionStatus(currentSession.status);
146
166
  }
147
167
  }, [currentSession]);
148
- return (_jsxs("div", { className: "px-4 py-3 flex items-center border-b gap-3 bg-white flex-wrap", "data-cp-name": "MessageHeader", children: [shouldShowMemberCount ? (_jsx(GroupCompositeAvatar, { groupID: conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID, faceURL: avatar, ownerUserID: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.ownerUserID, groupName: displayName, size: 40, className: "min-w-10 min-h-10" })) : (_jsx(Avatar, { src: avatar, size: "large", className: "min-w-10 min-h-10", children: ((_a = displayName === null || displayName === void 0 ? void 0 : displayName.charAt) === null || _a === void 0 ? void 0 : _a.call(displayName, 0)) || "A" })), _jsxs("div", { className: "flex flex-col overflow-hidden flex-1", children: [_jsx("p", { className: "text-base truncate", children: displayName || "" }), shouldShowMemberCount && (_jsx("p", { className: "text-xs text-gray-500 truncate", children: t("member_count", { count: (_b = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.memberCount) !== null && _b !== void 0 ? _b : 0 }) }))] }), _jsxs("div", { className: "flex items-center gap-2 justify-end overflow-hidden", children: [isCx && !isRealGroup && (_jsx(SelectSession, { placeholder: t("select_tag"), options: tagOptions, value: currentSessionTag, onChange: (value) => handleUpdateSession(value, "tag", value === currentSessionTag), excludeOptions: [SessionTag.SLOW_PROCESSING, SessionTag.NONE] })), isCx && !isRealGroup && (_jsx(SelectSession, { placeholder: t("select_status"), options: statusOptions, value: currentSessionStatus, onChange: (value) => handleUpdateSession(value, "status", value === currentSessionStatus) })), _jsx(SearchDrawer, {}), canViewMemberList && (_jsx(GroupMembersDrawer, { groupID: conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID, conversationId: conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID, currentSession: currentSession })), _jsx(MediaCollection, {}), _jsx(Button, { type: "text", shape: "default", className: "text-[var(--dc-color-header-icon)] w-8 h-8 p-0", onClick: onOpenThreadInfo, children: _jsx(Icon, { icon: "align-justify-o", size: 22 }) }), !!onClose && (_jsx(Button, { type: "text", shape: "default", className: "text-[var(--dc-color-header-icon)] w-8 h-8 p-0", onClick: onClose, children: _jsx(Icon, { icon: "close-b", size: 22 }) }))] })] }));
168
+ return (_jsxs("div", { className: "px-4 py-3 flex items-center border-b gap-3 bg-white flex-wrap", "data-cp-name": "MessageHeader", children: [ownerIdentity ? (_jsx(Avatar, { src: ownerIdentity.avatarUrl, size: "large", className: "min-w-10 min-h-10", children: ((_d = headerName === null || headerName === void 0 ? void 0 : headerName.charAt) === null || _d === void 0 ? void 0 : _d.call(headerName, 0)) || "A" })) : shouldShowMemberCount ? (_jsx(GroupCompositeAvatar, { groupID: conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID, faceURL: avatar, ownerUserID: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.ownerUserID, groupName: displayName, size: 40, className: "min-w-10 min-h-10" })) : (_jsx(Avatar, { src: avatar, size: "large", className: "min-w-10 min-h-10", children: ((_e = displayName === null || displayName === void 0 ? void 0 : displayName.charAt) === null || _e === void 0 ? void 0 : _e.call(displayName, 0)) || "A" })), _jsxs("div", { className: "flex flex-col overflow-hidden flex-1", children: [_jsx("p", { className: "text-base truncate", children: headerName || "" }), shouldShowMemberCount && (_jsx("p", { className: "text-xs text-gray-500 truncate", children: t("member_count", { count: memberCount }) }))] }), _jsxs("div", { className: "flex items-center gap-2 justify-end overflow-hidden", children: [isCx && !isRealGroup && (_jsx(SelectSession, { placeholder: t("select_tag"), options: tagOptions, value: currentSessionTag, onChange: (value) => handleUpdateSession(value, "tag", value === currentSessionTag), excludeOptions: [SessionTag.SLOW_PROCESSING, SessionTag.NONE] })), isCx && !isRealGroup && (_jsx(SelectSession, { placeholder: t("select_status"), options: statusOptions, value: currentSessionStatus, onChange: (value) => handleUpdateSession(value, "status", value === currentSessionStatus) })), _jsx(SearchDrawer, {}), canViewMemberList && (_jsx(GroupMembersDrawer, { groupID: conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID, conversationId: conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID, currentSession: currentSession })), _jsx(MediaCollection, {}), _jsx(Button, { type: "text", shape: "default", className: "text-[var(--dc-color-header-icon)] w-8 h-8 p-0", onClick: onOpenThreadInfo, children: _jsx(Icon, { icon: "align-justify-o", size: 22 }) }), !!onClose && (_jsx(Button, { type: "text", shape: "default", className: "text-[var(--dc-color-header-icon)] w-8 h-8 p-0", onClick: onClose, children: _jsx(Icon, { icon: "close-b", size: 22 }) }))] })] }));
149
169
  };
150
170
  export default MessageHeader;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageList.tsx"],"names":[],"mappings":"AAkCA,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;IAC9B,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAuX3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageList.tsx"],"names":[],"mappings":"AAoCA,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;IAC9B,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAsZ3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -9,6 +9,7 @@ import { GroupMemberRole, GroupStatus } from "@droppii-org/wasm-client-sdk";
9
9
  import emitter from "../../utils/events";
10
10
  import MessageItem from "./item";
11
11
  import MessageInfiniteScroll from "./MessageInfiniteScroll";
12
+ import UnreadMessageJumpButton from "./UnreadMessageJumpButton";
12
13
  import MessageHeader from "./MessageHeader";
13
14
  import MessageFooter from "./footer";
14
15
  import UnassignedSessionFooter from "./UnassignedSessionFooter";
@@ -16,6 +17,7 @@ import { useTranslation } from "react-i18next";
16
17
  import useConversationStore from "../../store/conversation";
17
18
  import { useBoolean } from "ahooks";
18
19
  import { useMessageListScroll } from "../../hooks/message/useMessageListScroll";
20
+ import { useUnreadMessageIndicator } from "../../hooks/message/useUnreadMessageIndicator";
19
21
  import { useChatContext } from "../../context/ChatContext";
20
22
  import { useRevokeMessage } from "../../hooks/message/useRevokeMessage";
21
23
  import { usePinMessage } from "../../hooks/message/usePinMessage";
@@ -107,7 +109,12 @@ const MessageList = (props) => {
107
109
  }, [isConfirmedGroup, isJoined, isViewOnlyMember, setIsViewOnlyMember]);
108
110
  const messageController = useMessage(conversationId, searchClientMsgID, true, false, onSearchMessageRevoked);
109
111
  const { loadState, moreOldLoading, moreNewLoading } = messageController;
110
- const { scrollRef, loadMoreOldMessage, handleInfiniteScroll, scrollToBottom, scrollToMessage, } = useMessageListScroll(Object.assign({ conversationId, userId: user === null || user === void 0 ? void 0 : user.userID, isGroupChat }, messageController));
112
+ const { isResolved: isUnreadLookupResolved, firstUnreadMessageId, unreadDividerMessageId, showJumpButton, markDividerSeen, } = useUnreadMessageIndicator({
113
+ conversationId,
114
+ searchClientMsgID,
115
+ messageList: loadState.messageList,
116
+ });
117
+ const { scrollRef, loadMoreOldMessage, handleInfiniteScroll, scrollToBottom, scrollToMessage, } = useMessageListScroll(Object.assign({ conversationId, userId: user === null || user === void 0 ? void 0 : user.userID, isGroupChat, markAsReadBlocked: !isUnreadLookupResolved }, messageController));
111
118
  const handleOpenRevoke = useCallback((clientMsgID) => {
112
119
  setSelectedItem(clientMsgID);
113
120
  openConfirmRevoke();
@@ -134,6 +141,12 @@ const MessageList = (props) => {
134
141
  setSearchClientMsgID(clientMsgID);
135
142
  }
136
143
  }, [scrollToMessage, loadState.messageList, setSearchClientMsgID]);
144
+ const handleJumpToFirstUnread = useCallback(() => {
145
+ if (!firstUnreadMessageId)
146
+ return;
147
+ markDividerSeen();
148
+ onPressQuoteMessage(firstUnreadMessageId);
149
+ }, [firstUnreadMessageId, markDividerSeen, onPressQuoteMessage]);
137
150
  const handlePinMessage = useCallback(async (message) => {
138
151
  if (pinnedRemainingCount <= 0 && pinnedTotalCount > 0) {
139
152
  setPinLimitModalOpen(true);
@@ -168,7 +181,7 @@ const MessageList = (props) => {
168
181
  emitter.off("CHAT_LIST_SCROLL_TO_BOTTOM", scrollToBottom);
169
182
  emitter.off("CHAT_LIST_SCROLL_TO_MESSAGE", scrollToMessage);
170
183
  };
171
- }, []);
184
+ }, [scrollToBottom, scrollToMessage]);
172
185
  useEffect(() => {
173
186
  const handler = (sessionUpdated) => {
174
187
  if (sessionUpdated.conversationId === conversationId) {
@@ -193,7 +206,8 @@ const MessageList = (props) => {
193
206
  if (!conversationData) {
194
207
  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") })) }));
195
208
  }
196
- 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 }), showPinnedMessageBar && (_jsx(PinnedMessageBar, { genericSubtitle: true, canUnpin: canPinMessage })), _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, onCopyMessageLink: handleCopyMessageLink }, 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 ? (
209
+ 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 }), showPinnedMessageBar && (_jsx(PinnedMessageBar, { genericSubtitle: true, canUnpin: canPinMessage })), _jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [!loadState.initLoading && showJumpButton && (_jsx(UnreadMessageJumpButton, { className: "absolute right-0 top-2 z-10", onClick: handleJumpToFirstUnread })), _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, onCopyMessageLink: handleCopyMessageLink, showUnreadDivider: !!unreadDividerMessageId &&
210
+ message.clientMsgID === unreadDividerMessageId, onUnreadDividerSeen: markDividerSeen }, 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 ? (
197
211
  // Brief fallback while isJoined catches up after the kick event.
198
212
  _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
199
213
  ? "send_message_disabled_group_mute"
@@ -0,0 +1,7 @@
1
+ interface UnreadMessageDividerProps {
2
+ /** Fires once, the first time the divider scrolls into view. */
3
+ onSeen?: () => void;
4
+ }
5
+ declare const UnreadMessageDivider: ({ onSeen }: UnreadMessageDividerProps) => import("react/jsx-runtime").JSX.Element;
6
+ export default UnreadMessageDivider;
7
+ //# sourceMappingURL=UnreadMessageDivider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnreadMessageDivider.d.ts","sourceRoot":"","sources":["../../../src/components/message/UnreadMessageDivider.tsx"],"names":[],"mappings":"AAMA,UAAU,yBAAyB;IACjC,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,oBAAoB,GAAI,YAAY,yBAAyB,4CAkClE,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ import { useLatest } from "ahooks";
5
+ import { useTranslation } from "react-i18next";
6
+ const UnreadMessageDivider = ({ onSeen }) => {
7
+ const { t } = useTranslation();
8
+ const dividerRef = useRef(null);
9
+ const onSeenRef = useLatest(onSeen);
10
+ useEffect(() => {
11
+ const element = dividerRef.current;
12
+ if (!element || typeof IntersectionObserver === "undefined")
13
+ return;
14
+ // No explicit root: intersection is still clipped by the scrolling
15
+ // timeline, so scrolled-out-of-view does not count as seen.
16
+ const observer = new IntersectionObserver((entries) => {
17
+ var _a;
18
+ if (entries.some((entry) => entry.isIntersecting)) {
19
+ (_a = onSeenRef.current) === null || _a === void 0 ? void 0 : _a.call(onSeenRef);
20
+ observer.disconnect();
21
+ }
22
+ });
23
+ observer.observe(element);
24
+ return () => observer.disconnect();
25
+ }, [onSeenRef]);
26
+ return (_jsxs("div", { ref: dividerRef, "data-testid": "unread-message-divider", className: "flex items-center gap-4 py-1", children: [_jsx("span", { className: "h-px flex-1 bg-[#393E40]/10" }), _jsx("span", { className: "shrink-0 text-xs font-bold uppercase text-[#A8ADAF]", children: t("unread_message_divider") }), _jsx("span", { className: "h-px flex-1 bg-[#393E40]/10" })] }));
27
+ };
28
+ export default UnreadMessageDivider;
@@ -0,0 +1,7 @@
1
+ interface UnreadMessageJumpButtonProps {
2
+ onClick: () => void;
3
+ className?: string;
4
+ }
5
+ declare const UnreadMessageJumpButton: ({ onClick, className, }: UnreadMessageJumpButtonProps) => import("react/jsx-runtime").JSX.Element;
6
+ export default UnreadMessageJumpButton;
7
+ //# sourceMappingURL=UnreadMessageJumpButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UnreadMessageJumpButton.d.ts","sourceRoot":"","sources":["../../../src/components/message/UnreadMessageJumpButton.tsx"],"names":[],"mappings":"AAKA,UAAU,4BAA4B;IACpC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqBD,QAAA,MAAM,uBAAuB,GAAI,yBAG9B,4BAA4B,4CAiB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import clsx from "clsx";
4
+ import { useTranslation } from "react-i18next";
5
+ const DoubleAngleUpIcon = () => (_jsx("svg", { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": "true", className: "shrink-0", children: _jsx("path", { d: "M3 7l4-4 4 4M3 11l4-4 4 4", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }) }));
6
+ const UnreadMessageJumpButton = ({ onClick, className, }) => {
7
+ const { t } = useTranslation();
8
+ return (_jsxs("button", { type: "button", "data-testid": "unread-message-jump-button", onClick: onClick, className: clsx("flex items-center gap-0.5 rounded-l-[10px] border border-r-0 border-white bg-[#F1F3F8] px-1.5 py-0.5 text-xs font-medium text-[#393E40]", className), children: [_jsx(DoubleAngleUpIcon, {}), _jsx("span", { children: t("unread_message_jump") })] }));
9
+ };
10
+ export default UnreadMessageJumpButton;
@@ -17,7 +17,10 @@ interface MessageItemProps {
17
17
  /** Called on bubble tap to toggle this message as the revealed one. */
18
18
  onToggleTimeReveal?: (clientMsgID: string) => void;
19
19
  onCopyMessageLink?: (clientMsgID: string) => void;
20
+ /** Renders the "unread messages" divider above this message. */
21
+ showUnreadDivider?: boolean;
22
+ onUnreadDividerSeen?: () => void;
20
23
  }
21
- declare const MessageItem: ({ message, allMessages, isGroupChat, forceHideTip, contextMenuOpen, onContextMenuOpenChange, onRevokeMessage, onQuoteMessage, onPressQuoteMessage, onPinMessage, onUnpinMessage, enableLongPress, timeRevealed, onToggleTimeReveal, onCopyMessageLink, }: MessageItemProps) => import("react/jsx-runtime").JSX.Element | null;
24
+ declare const MessageItem: ({ message, allMessages, isGroupChat, forceHideTip, contextMenuOpen, onContextMenuOpenChange, onRevokeMessage, onQuoteMessage, onPressQuoteMessage, onPinMessage, onUnpinMessage, enableLongPress, timeRevealed, onToggleTimeReveal, onCopyMessageLink, showUnreadDivider, onUnreadDividerSeen, }: MessageItemProps) => import("react/jsx-runtime").JSX.Element | null;
22
25
  export default MessageItem;
23
26
  //# 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;AA0CtC,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;IACnD,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACnD;AAkCD,QAAA,MAAM,WAAW,GAAI,0PAgBlB,gBAAgB,mDAoXlB,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;AA2CtC,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;IACnD,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;CAClC;AAkCD,QAAA,MAAM,WAAW,GAAI,kSAkBlB,gBAAgB,mDAmYlB,CAAC;AAEF,eAAe,WAAW,CAAC"}