@droppii-org/chat-sdk 0.3.6 → 0.3.8

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 (57) hide show
  1. package/dist/components/common/ConfirmModal.d.ts +18 -0
  2. package/dist/components/common/ConfirmModal.d.ts.map +1 -0
  3. package/dist/components/common/ConfirmModal.js +15 -0
  4. package/dist/components/common/InfoModal.d.ts +11 -0
  5. package/dist/components/common/InfoModal.d.ts.map +1 -0
  6. package/dist/components/common/InfoModal.js +13 -0
  7. package/dist/components/compose/ComposeNewMessageModal.d.ts.map +1 -1
  8. package/dist/components/compose/ComposeNewMessageModal.js +4 -6
  9. package/dist/components/conversation/ConversationByGroupItem.d.ts.map +1 -1
  10. package/dist/components/conversation/ConversationByGroupItem.js +3 -7
  11. package/dist/components/conversation/ConversationBySessionItem.d.ts.map +1 -1
  12. package/dist/components/conversation/ConversationBySessionItem.js +3 -7
  13. package/dist/components/conversation/DeskConversationList.d.ts.map +1 -1
  14. package/dist/components/conversation/DeskConversationList.js +20 -14
  15. package/dist/components/conversation-inbox/ConversationInboxList.d.ts.map +1 -1
  16. package/dist/components/conversation-inbox/ConversationInboxList.js +4 -6
  17. package/dist/components/message/GroupMembersDrawer.d.ts.map +1 -1
  18. package/dist/components/message/GroupMembersDrawer.js +8 -8
  19. package/dist/components/message/MessageList.d.ts.map +1 -1
  20. package/dist/components/message/MessageList.js +17 -3
  21. package/dist/components/message/item/MessageBubble.d.ts +3 -2
  22. package/dist/components/message/item/MessageBubble.d.ts.map +1 -1
  23. package/dist/components/message/item/MessageBubble.js +8 -2
  24. package/dist/components/message/item/index.d.ts.map +1 -1
  25. package/dist/components/message/item/index.js +23 -6
  26. package/dist/components/thread/GroupThreadInfo.d.ts.map +1 -1
  27. package/dist/components/thread/GroupThreadInfo.js +43 -14
  28. package/dist/components/thread/SingleThreadInfo.d.ts.map +1 -1
  29. package/dist/components/thread/SingleThreadInfo.js +3 -2
  30. package/dist/context/ChatContext.d.ts +1 -1
  31. package/dist/context/ChatContext.d.ts.map +1 -1
  32. package/dist/context/ChatContext.js +2 -1
  33. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  34. package/dist/hooks/global/useGlobalEvent.js +29 -4
  35. package/dist/hooks/group/useCreateGroup.js +5 -5
  36. package/dist/hooks/group/useDismissGroup.d.ts.map +1 -1
  37. package/dist/hooks/group/useDismissGroup.js +18 -0
  38. package/dist/hooks/session/useConversationSessionState.d.ts.map +1 -1
  39. package/dist/hooks/session/useConversationSessionState.js +1 -0
  40. package/dist/layout/index.d.ts.map +1 -1
  41. package/dist/layout/index.js +13 -2
  42. package/dist/locales/vi/common.json +14 -8
  43. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  44. package/dist/screens/conversation-detail/index.js +6 -7
  45. package/dist/store/conversation.d.ts.map +1 -1
  46. package/dist/store/conversation.js +26 -5
  47. package/dist/store/type.d.ts +5 -0
  48. package/dist/store/type.d.ts.map +1 -1
  49. package/dist/styles/global.css +1 -1
  50. package/dist/types/chat.d.ts +2 -0
  51. package/dist/types/chat.d.ts.map +1 -1
  52. package/dist/utils/common.d.ts +1 -0
  53. package/dist/utils/common.d.ts.map +1 -1
  54. package/dist/utils/common.js +18 -0
  55. package/dist/utils/fileValidation.d.ts.map +1 -1
  56. package/dist/utils/fileValidation.js +1 -1
  57. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { Input, Modal, Switch, message } from "antd";
4
+ import ConfirmModal from "../../components/common/ConfirmModal";
4
5
  import { useTranslation } from "react-i18next";
5
6
  import { GroupMemberRole, GroupStatus, MessageReceiveOptType } from "@droppii-org/wasm-client-sdk";
6
7
  import { Icon } from "../../components/icon";
@@ -13,12 +14,12 @@ import { useToggleGroupSendMessage } from "../../hooks/group/useToggleGroupSendM
13
14
  import { useDismissGroup } from "../../hooks/group/useDismissGroup";
14
15
  import { useGroupPermission } from "../../hooks/group/useGroupPermission";
15
16
  import { useUploadFile } from "../../hooks/common/useUploadFile";
16
- import { validateFile } from "../../utils/fileValidation";
17
+ import { ACCEPTED_IMAGE_TYPES, MAX_IMAGE_SIZE_MB } from "../../utils/fileValidation";
17
18
  import AdminPermissionModal from "../../components/group/AdminPermissionModal";
18
19
  import MemberPermissionPanel from "../../components/group/MemberPermissionPanel";
19
20
  import GroupCompositeAvatar from "../../components/group/GroupCompositeAvatar";
20
21
  const GroupThreadInfo = ({ onClose, config }) => {
21
- var _a, _b, _c;
22
+ var _a, _b, _c, _d;
22
23
  const { t } = useTranslation();
23
24
  const conversationData = useConversationStore((state) => state.conversationData);
24
25
  const currentGroupInfo = useConversationStore((state) => state.currentGroupInfo);
@@ -31,6 +32,10 @@ const GroupThreadInfo = ({ onClose, config }) => {
31
32
  // per-permission.
32
33
  const isAdminOrOwner = (currentMemberInGroup === null || currentMemberInGroup === void 0 ? void 0 : currentMemberInGroup.roleLevel) === GroupMemberRole.Admin ||
33
34
  (currentMemberInGroup === null || currentMemberInGroup === void 0 ? void 0 : currentMemberInGroup.roleLevel) === GroupMemberRole.Owner;
35
+ // "Cho phép gửi tin nhắn" (group-wide mute) is owner-only per BR-001 —
36
+ // unlike other admin-manageable rows in this panel, admins may view but
37
+ // not change this toggle.
38
+ const isOwner = (currentMemberInGroup === null || currentMemberInGroup === void 0 ? void 0 : currentMemberInGroup.roleLevel) === GroupMemberRole.Owner;
34
39
  const [editingField, setEditingField] = useState(null);
35
40
  const [nameValue, setNameValue] = useState((_a = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName) !== null && _a !== void 0 ? _a : "");
36
41
  const [descriptionValue, setDescriptionValue] = useState((_b = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction) !== null && _b !== void 0 ? _b : "");
@@ -48,7 +53,7 @@ const GroupThreadInfo = ({ onClose, config }) => {
48
53
  useEffect(() => {
49
54
  setAllowSendMessage((currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.status) !== GroupStatus.Muted);
50
55
  }, [currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.status]);
51
- const { mutate: updateGroupInfo } = useUpdateGroupInfo();
56
+ const { mutate: updateGroupInfo, isPending: isSavingGroupInfo } = useUpdateGroupInfo();
52
57
  const { mutate: uploadFile, isPending: isAvatarUploading } = useUploadFile();
53
58
  const { mutate: toggleSendMessage, isPending: isTogglingSendMessage } = useToggleGroupSendMessage();
54
59
  const { mutate: toggleNotification, isPending: isTogglingNotification } = useToggleConversationNotification();
@@ -65,12 +70,23 @@ const GroupThreadInfo = ({ onClose, config }) => {
65
70
  setDescriptionValue((_a = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction) !== null && _a !== void 0 ? _a : "");
66
71
  setEditingField("description");
67
72
  };
73
+ const cancelEditName = () => {
74
+ var _a;
75
+ setNameValue((_a = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName) !== null && _a !== void 0 ? _a : "");
76
+ setEditingField(null);
77
+ };
68
78
  const saveName = () => {
79
+ if (isSavingGroupInfo)
80
+ return;
69
81
  const trimmed = nameValue.trim();
70
- setEditingField(null);
71
- if (!groupID || !trimmed || trimmed === (currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName))
82
+ if (!groupID || !trimmed || trimmed === (currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName)) {
83
+ setEditingField(null);
72
84
  return;
73
- updateGroupInfo({ groupID, groupName: trimmed }, { onError: () => message.error(t("update_group_info_failed")) });
85
+ }
86
+ updateGroupInfo({ groupID, groupName: trimmed }, {
87
+ onSuccess: () => setEditingField(null),
88
+ onError: () => message.error(t("update_group_info_failed")),
89
+ });
74
90
  };
75
91
  const cancelEditDescription = () => {
76
92
  var _a;
@@ -78,11 +94,17 @@ const GroupThreadInfo = ({ onClose, config }) => {
78
94
  setEditingField(null);
79
95
  };
80
96
  const saveDescription = () => {
97
+ if (isSavingGroupInfo)
98
+ return;
81
99
  const trimmed = descriptionValue.trim();
82
- setEditingField(null);
83
- if (!groupID || trimmed === (currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction))
100
+ if (!groupID || trimmed === (currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction)) {
101
+ setEditingField(null);
84
102
  return;
85
- updateGroupInfo({ groupID, introduction: trimmed }, { onError: () => message.error(t("update_group_info_failed")) });
103
+ }
104
+ updateGroupInfo({ groupID, introduction: trimmed }, {
105
+ onSuccess: () => setEditingField(null),
106
+ onError: () => message.error(t("update_group_info_failed")),
107
+ });
86
108
  };
87
109
  const handleAvatarClick = () => {
88
110
  var _a;
@@ -96,9 +118,13 @@ const GroupThreadInfo = ({ onClose, config }) => {
96
118
  event.target.value = "";
97
119
  if (!file || !groupID)
98
120
  return;
99
- const validation = validateFile(file, t);
100
- if (!validation.isValid) {
101
- message.error(validation.error);
121
+ if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
122
+ message.error(t("avatar_invalid_format"));
123
+ return;
124
+ }
125
+ const fileSizeMB = file.size / 1024 / 1024;
126
+ if (fileSizeMB > MAX_IMAGE_SIZE_MB) {
127
+ message.error(t("avatar_size_exceeded", { maxSize: MAX_IMAGE_SIZE_MB }));
102
128
  return;
103
129
  }
104
130
  uploadFile(file, {
@@ -125,7 +151,7 @@ const GroupThreadInfo = ({ onClose, config }) => {
125
151
  toggleNotification({ conversationID, notify: checked });
126
152
  };
127
153
  const handleToggleSendMessage = (checked) => {
128
- if (!groupID)
154
+ if (!groupID || !isOwner)
129
155
  return;
130
156
  const previous = allowSendMessage;
131
157
  setAllowSendMessage(checked);
@@ -187,6 +213,9 @@ const GroupThreadInfo = ({ onClose, config }) => {
187
213
  if (!groupID) {
188
214
  return null;
189
215
  }
190
- return (_jsxs("div", { className: "h-full w-[330px] bg-white shadow-md flex flex-col relative overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-4 border-b border-gray-100", children: [_jsx("span", { className: "text-lg font-medium", children: t("thread_info_title") }), _jsx("button", { type: "button", className: "flex items-center justify-center w-8 h-8 rounded-full text-gray-500 hover:bg-gray-100", onClick: onClose, children: _jsx(Icon, { icon: "close-o", size: 20 }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-4", children: [_jsxs("div", { className: "relative shrink-0", children: [_jsx(GroupCompositeAvatar, { groupID: groupID, faceURL: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.faceURL, ownerUserID: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.ownerUserID, groupName: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName, size: 56 }), isAvatarUploading && (_jsx("span", { className: "absolute inset-0 flex items-center justify-center rounded-full bg-black/30", children: _jsx("span", { className: "h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" }) })), permissions.updateGroupAvatar && (_jsxs(_Fragment, { children: [_jsx("input", { ref: avatarInputRef, type: "file", accept: "image/jpeg, image/png, image/jpg", className: "hidden", onChange: handleAvatarSelect }), _jsx("button", { type: "button", disabled: isAvatarUploading, onClick: handleAvatarClick, className: "absolute -bottom-1 -right-1 flex items-center justify-center w-5 h-5 rounded-full bg-black text-white", children: _jsx(Icon, { icon: "camera-o", size: 12 }) })] }))] }), _jsx("div", { className: "flex-1 min-w-0", children: editingField === "name" ? (_jsx(Input, { autoFocus: true, value: nameValue, onChange: (event) => setNameValue(event.target.value), onPressEnter: saveName, onBlur: saveName, maxLength: 100 })) : (_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("p", { className: "text-base font-medium truncate", children: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName }), permissions.updateGroupName && (_jsx(Icon, { icon: "edit-o", size: 16, className: "shrink-0 cursor-pointer text-gray-500", onClick: startEditName }))] })) })] }), _jsx("div", { className: "px-4 pb-4", children: editingField === "description" ? (_jsxs("div", { className: "flex flex-col gap-3 bg-[#f5f5f5] rounded-lg p-3", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-gray-900", children: [_jsx(Icon, { icon: "info-circle-o", size: 18, className: "text-gray-500" }), t("description")] }), _jsx(Input.TextArea, { autoFocus: true, value: descriptionValue, onChange: (event) => setDescriptionValue(event.target.value), placeholder: t("enter_group_description"), autoSize: { minRows: 1, maxRows: 4 }, maxLength: 500, className: "bg-white" }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { type: "button", className: "px-4 py-1.5 rounded-full border border-primary-500 text-primary-500 text-sm font-medium", onClick: cancelEditDescription, children: t("cancel") }), _jsx("button", { type: "button", className: "px-4 py-1.5 rounded-full text-primary-500 text-sm font-medium disabled:text-gray-300", disabled: descriptionValue.trim() === ((_c = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction) !== null && _c !== void 0 ? _c : ""), onClick: saveDescription, children: t("save") })] })] })) : (_jsxs("div", { className: "flex items-start justify-between gap-2 bg-[#f5f5f5] rounded-lg px-3 py-2.5", children: [_jsxs("div", { className: "flex items-start gap-2 flex-1 min-w-0", children: [_jsx(Icon, { icon: "info-circle-o", size: 18, className: "shrink-0 mt-0.5 text-gray-900" }), _jsxs("div", { className: "flex flex-col gap-0.5 min-w-0", children: [_jsx("span", { className: "text-sm font-semibold text-gray-900", children: t("description") }), _jsx("p", { className: "text-sm text-gray-400 truncate", children: (currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction) || t("no_description") })] })] }), permissions.updateGroupDescription && (_jsx(Icon, { icon: "edit-o", size: 16, className: "shrink-0 cursor-pointer text-gray-500", onClick: startEditDescription }))] })) }), _jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 rounded-lg overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "bell-o", size: 18 }), t("notification")] }), _jsx(Switch, { checked: notifyEnabled, onChange: handleToggleNotification, loading: isTogglingNotification })] }), (config === null || config === void 0 ? void 0 : config.showAllowSendMessage) && (_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "user-check-o", size: 18 }), t("allow_send_message")] }), _jsx(Switch, { checked: allowSendMessage, onChange: handleToggleSendMessage, loading: isTogglingSendMessage })] })), isAdminOrOwner && (config === null || config === void 0 ? void 0 : config.showMutedChatList) && (_jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("muted-chat-list"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "bell-off-o", size: 18 }), t("muted_chat_list")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] })), _jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("block-list"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "user-del-o", size: 18 }), t("block_list")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] })] }), permissions.configGroupPermission && (_jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 mt-3 rounded-lg overflow-hidden", children: [_jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("member-permission"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "shield-o", size: 18 }), t("member_permission_settings")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] }), _jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("admin-permission"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "shield-done-o", size: 18 }), t("admin_permission_settings")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] })] })), _jsx("div", { className: "mx-4 mt-3 rounded-lg overflow-hidden bg-[#f5f5f5]", children: _jsxs("button", { type: "button", className: "w-full flex items-center gap-3 px-3 py-3 text-left text-sm text-gray-400 cursor-not-allowed", disabled: true, title: t("invite_via_link_pending"), children: [_jsx(Icon, { icon: "link-alt-o", size: 18 }), t("invite_via_link")] }) }), _jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 mt-3 mb-4 rounded-lg overflow-hidden", children: [_jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("delete"), children: [_jsx(Icon, { icon: "trash-o", size: 18 }), t("delete_conversation")] }), _jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("leave"), children: [_jsx(Icon, { icon: "logout-o", size: 18 }), t("leave_group")] }), isAdminOrOwner && (_jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("disband"), children: [_jsx(Icon, { icon: "user-del-o", size: 18 }), t("disband_group")] }))] })] }), _jsx(Modal, { centered: true, open: confirmAction === "leave", onOk: handleConfirmLeave, onCancel: () => setConfirmAction(null), title: t("leave_group_confirm_title"), okText: t("leave_group"), cancelText: t("cancel"), okType: "danger", confirmLoading: isLeaving, getContainer: false, forceRender: true, children: _jsx("p", { children: t("leave_group_confirm_message") }) }), _jsx(Modal, { centered: true, open: confirmAction === "disband", onOk: handleConfirmDisband, onCancel: () => setConfirmAction(null), title: t("disband_group_confirm_title"), okText: t("disband_group"), cancelText: t("cancel"), okType: "danger", confirmLoading: isDismissing, getContainer: false, forceRender: true, children: _jsx("p", { children: t("disband_group_confirm_message") }) }), _jsx(Modal, { centered: true, open: confirmAction === "delete", onOk: handleConfirmDelete, onCancel: () => setConfirmAction(null), title: t("delete_conversation_confirm_title"), okText: t("delete_conversation"), cancelText: t("cancel"), okType: "danger", confirmLoading: isDeleting, getContainer: false, forceRender: true, children: _jsx("p", { children: t("delete_conversation_confirm_message") }) }), _jsx(MemberPermissionPanel, { open: activeRow === "member-permission", groupID: groupID, onClose: () => setActiveRow(null) }), _jsx(AdminPermissionModal, { open: activeRow === "admin-permission", groupID: groupID, onClose: () => setActiveRow(null), inline: true }), (activeRow === "muted-chat-list" || activeRow === "block-list") && (_jsx(Modal, { centered: true, open: true, footer: null, onCancel: () => setActiveRow(null), title: t(activeRow === "muted-chat-list" ? "muted_chat_list" : "block_list"), getContainer: false, forceRender: true, children: _jsx("p", { className: "text-sm text-gray-500", children: t("feature_coming_soon") }) }))] }));
216
+ return (_jsxs("div", { className: "h-full w-[330px] bg-white shadow-md flex flex-col relative overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-4 border-b border-gray-100", children: [_jsx("span", { className: "text-lg font-medium", children: t("thread_info_title") }), _jsx("button", { type: "button", className: "flex items-center justify-center w-8 h-8 rounded-full text-gray-500 hover:bg-gray-100", onClick: onClose, children: _jsx(Icon, { icon: "close-o", size: 20 }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-4", children: [_jsxs("div", { className: "relative shrink-0", children: [_jsx(GroupCompositeAvatar, { groupID: groupID, faceURL: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.faceURL, ownerUserID: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.ownerUserID, groupName: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName, size: 56 }), isAvatarUploading && (_jsx("span", { className: "absolute inset-0 flex items-center justify-center rounded-full bg-black/30", children: _jsx("span", { className: "h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" }) })), permissions.updateGroupAvatar && (_jsxs(_Fragment, { children: [_jsx("input", { ref: avatarInputRef, type: "file", accept: ACCEPTED_IMAGE_TYPES.join(", "), className: "hidden", onChange: handleAvatarSelect }), _jsx("button", { type: "button", disabled: isAvatarUploading, onClick: handleAvatarClick, className: "absolute -bottom-1 -right-1 flex items-center justify-center w-5 h-5 rounded-full bg-black text-white", children: _jsx(Icon, { icon: "camera-o", size: 12 }) })] }))] }), _jsx("div", { className: "flex-1 min-w-0", children: editingField !== "name" && (_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("p", { className: "text-base font-medium truncate", children: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName }), permissions.updateGroupName && (_jsx(Icon, { icon: "edit-o", size: 16, className: "shrink-0 cursor-pointer text-gray-500", onClick: startEditName }))] })) })] }), editingField === "name" && (_jsx("div", { className: "px-4 pb-4", children: _jsxs("div", { className: "flex flex-col gap-3 bg-[#f5f5f5] rounded-lg p-3", children: [_jsx(Input, { autoFocus: true, value: nameValue, onChange: (event) => setNameValue(event.target.value), maxLength: 255, showCount: true, disabled: isSavingGroupInfo, className: "bg-white" }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg border border-primary-500 text-primary-500 text-sm font-medium disabled:border-gray-200 disabled:text-gray-300", disabled: isSavingGroupInfo, onClick: cancelEditName, children: t("cancel") }), _jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg bg-primary-500 text-white text-sm font-medium disabled:bg-gray-100 disabled:text-gray-300", disabled: isSavingGroupInfo ||
217
+ !nameValue.trim() ||
218
+ nameValue.trim() === ((_c = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName) !== null && _c !== void 0 ? _c : ""), onClick: saveName, children: t("save") })] })] }) })), _jsx("div", { className: "px-4 pb-4", children: editingField === "description" ? (_jsxs("div", { className: "flex flex-col gap-3 bg-[#f5f5f5] rounded-lg p-3", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-gray-900", children: [_jsx(Icon, { icon: "info-circle-o", size: 18, className: "text-gray-500" }), t("description")] }), _jsx(Input.TextArea, { autoFocus: true, value: descriptionValue, onChange: (event) => setDescriptionValue(event.target.value), placeholder: t("enter_group_description"), autoSize: { minRows: 1, maxRows: 4 }, maxLength: 500, showCount: true, disabled: isSavingGroupInfo, className: "bg-white" }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg border border-primary-500 text-primary-500 text-sm font-medium disabled:border-gray-200 disabled:text-gray-300", disabled: isSavingGroupInfo, onClick: cancelEditDescription, children: t("cancel") }), _jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg bg-primary-500 text-white text-sm font-medium disabled:bg-gray-100 disabled:text-gray-300", disabled: isSavingGroupInfo ||
219
+ descriptionValue.trim() === ((_d = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction) !== null && _d !== void 0 ? _d : ""), onClick: saveDescription, children: t("save") })] })] })) : (_jsxs("div", { className: "flex items-start justify-between gap-2 bg-[#f5f5f5] rounded-lg px-3 py-2.5", children: [_jsxs("div", { className: "flex items-start gap-2 flex-1 min-w-0", children: [_jsx(Icon, { icon: "info-circle-o", size: 18, className: "shrink-0 mt-0.5 text-gray-900" }), _jsxs("div", { className: "flex flex-col gap-0.5 min-w-0", children: [_jsx("span", { className: "text-sm font-semibold text-gray-900", children: t("description") }), _jsx("p", { className: "text-sm text-gray-400 truncate", children: (currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction) || t("no_description") })] })] }), permissions.updateGroupDescription && (_jsx(Icon, { icon: "edit-o", size: 16, className: "shrink-0 cursor-pointer text-gray-500", onClick: startEditDescription }))] })) }), _jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 rounded-lg overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "bell-o", size: 18 }), t("notification")] }), _jsx(Switch, { checked: notifyEnabled, onChange: handleToggleNotification, loading: isTogglingNotification })] }), (config === null || config === void 0 ? void 0 : config.showAllowSendMessage) && isOwner && (_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "user-check-o", size: 18 }), t("allow_send_message")] }), _jsx(Switch, { checked: allowSendMessage, onChange: handleToggleSendMessage, loading: isTogglingSendMessage })] })), isAdminOrOwner && (config === null || config === void 0 ? void 0 : config.showMutedChatList) && (_jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("muted-chat-list"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "bell-off-o", size: 18 }), t("muted_chat_list")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] })), isAdminOrOwner && (_jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("block-list"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "user-del-o", size: 18 }), t("block_list")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] }))] }), permissions.configGroupPermission && (_jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 mt-3 rounded-lg overflow-hidden", children: [_jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("member-permission"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "shield-o", size: 18 }), t("member_permission_settings")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] }), _jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("admin-permission"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "shield-done-o", size: 18 }), t("admin_permission_settings")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] })] })), _jsx("div", { className: "mx-4 mt-3 rounded-lg overflow-hidden bg-[#f5f5f5]", children: _jsxs("button", { type: "button", className: "w-full flex items-center gap-3 px-3 py-3 text-left text-sm text-gray-400 cursor-not-allowed", disabled: true, title: t("invite_via_link_pending"), children: [_jsx(Icon, { icon: "link-alt-o", size: 18 }), t("invite_via_link")] }) }), _jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 mt-3 mb-4 rounded-lg overflow-hidden", children: [isAdminOrOwner && (_jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("delete"), children: [_jsx(Icon, { icon: "trash-o", size: 18 }), t("delete_conversation")] })), _jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("leave"), children: [_jsx(Icon, { icon: "logout-o", size: 18 }), t("leave_group")] }), isOwner && (_jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("disband"), children: [_jsx(Icon, { icon: "user-del-o", size: 18 }), t("disband_group")] }))] })] }), _jsx(ConfirmModal, { open: confirmAction === "leave", onOk: handleConfirmLeave, onCancel: () => setConfirmAction(null), title: t("leave_group_confirm_title"), okText: t("leave_group"), cancelText: t("cancel"), okType: "danger", confirmLoading: isLeaving, children: _jsx("p", { children: t("leave_group_confirm_message") }) }), _jsx(ConfirmModal, { open: confirmAction === "disband", onOk: handleConfirmDisband, onCancel: () => setConfirmAction(null), title: t("disband_group_confirm_title"), okText: t("disband_group"), cancelText: t("cancel"), okType: "danger", confirmLoading: isDismissing, children: _jsx("p", { children: t("disband_group_confirm_message") }) }), _jsx(ConfirmModal, { open: confirmAction === "delete", onOk: handleConfirmDelete, onCancel: () => setConfirmAction(null), title: t("delete_conversation_confirm_title"), okText: t("delete_conversation"), cancelText: t("cancel"), okType: "danger", confirmLoading: isDeleting, children: _jsx("p", { children: t("delete_conversation_confirm_message") }) }), _jsx(MemberPermissionPanel, { open: activeRow === "member-permission", groupID: groupID, onClose: () => setActiveRow(null) }), _jsx(AdminPermissionModal, { open: activeRow === "admin-permission", groupID: groupID, onClose: () => setActiveRow(null), inline: true }), (activeRow === "muted-chat-list" || activeRow === "block-list") && (_jsx(Modal, { centered: true, open: true, footer: null, onCancel: () => setActiveRow(null), title: t(activeRow === "muted-chat-list" ? "muted_chat_list" : "block_list"), getContainer: false, forceRender: true, children: _jsx("p", { className: "text-sm text-gray-500", children: t("feature_coming_soon") }) }))] }));
191
220
  };
192
221
  export default GroupThreadInfo;
@@ -1 +1 @@
1
- {"version":3,"file":"SingleThreadInfo.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SingleThreadInfo.tsx"],"names":[],"mappings":"AAYA,UAAU,qBAAqB;IAC7B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,QAAA,MAAM,gBAAgB,GAAI,aAAa,qBAAqB,mDAoJ3D,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"SingleThreadInfo.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SingleThreadInfo.tsx"],"names":[],"mappings":"AAaA,UAAU,qBAAqB;IAC7B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,QAAA,MAAM,gBAAgB,GAAI,aAAa,qBAAqB,mDAiJ3D,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
- import { Avatar, Modal, Switch, message } from "antd";
3
+ import { Avatar, Switch, message } from "antd";
4
+ import ConfirmModal from "../../components/common/ConfirmModal";
4
5
  import { useTranslation } from "react-i18next";
5
6
  import { MessageReceiveOptType } from "@droppii-org/wasm-client-sdk";
6
7
  import { Icon } from "../../components/icon";
@@ -65,6 +66,6 @@ const SingleThreadInfo = ({ onClose }) => {
65
66
  if (!conversationID) {
66
67
  return null;
67
68
  }
68
- return (_jsxs("div", { className: "h-full w-[330px] bg-white shadow-md flex flex-col relative", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-4 border-b border-gray-100", children: [_jsx("span", { className: "text-lg font-medium", children: t("thread_info_title") }), _jsx("button", { type: "button", className: "flex items-center justify-center w-8 h-8 rounded-full text-gray-500 hover:bg-gray-100", onClick: onClose, children: _jsx(Icon, { icon: "close-o", size: 20 }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-4", children: [_jsx(Avatar, { src: avatar, size: 56, className: "shrink-0", 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-1 min-w-0 flex items-center gap-2", children: [_jsx("p", { className: "text-base font-medium truncate", children: displayName }), isBot && (_jsx("span", { className: "shrink-0 rounded px-1.5 py-0.5 text-xs font-semibold bg-orange-50 text-orange-500", children: t("bot_tag") }))] })] }), _jsx("div", { className: "bg-[#f5f5f5] mx-4 rounded-lg overflow-hidden", children: _jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "bell-o", size: 18 }), t("notification")] }), _jsx(Switch, { checked: notifyEnabled, onChange: handleToggleNotification, loading: isTogglingNotification })] }) }), _jsx("div", { className: "bg-[#f5f5f5] mx-4 mt-3 rounded-lg overflow-hidden", children: _jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm w-full", onClick: handleToggleBlock, disabled: isTogglingBlock, children: [_jsx(Icon, { icon: "user-del-o", size: 18 }), t(isBlocked ? "unblock_user" : "block_user")] }) }), _jsx("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 mt-3 rounded-lg overflow-hidden", children: _jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmDelete(true), children: [_jsx(Icon, { icon: "trash-o", size: 18 }), t("delete_conversation")] }) })] }), _jsx(Modal, { centered: true, open: confirmDelete, onOk: handleConfirmDelete, onCancel: () => setConfirmDelete(false), title: t("delete_conversation_confirm_title"), okText: t("delete_conversation"), cancelText: t("cancel"), okType: "danger", confirmLoading: isDeleting, getContainer: false, forceRender: true, children: _jsx("p", { children: t("delete_conversation_confirm_message") }) })] }));
69
+ return (_jsxs("div", { className: "h-full w-[330px] bg-white shadow-md flex flex-col relative", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-4 border-b border-gray-100", children: [_jsx("span", { className: "text-lg font-medium", children: t("thread_info_title") }), _jsx("button", { type: "button", className: "flex items-center justify-center w-8 h-8 rounded-full text-gray-500 hover:bg-gray-100", onClick: onClose, children: _jsx(Icon, { icon: "close-o", size: 20 }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-4", children: [_jsx(Avatar, { src: avatar, size: 56, className: "shrink-0", 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-1 min-w-0 flex items-center gap-2", children: [_jsx("p", { className: "text-base font-medium truncate", children: displayName }), isBot && (_jsx("span", { className: "shrink-0 rounded px-1.5 py-0.5 text-xs font-semibold bg-orange-50 text-orange-500", children: t("bot_tag") }))] })] }), _jsx("div", { className: "bg-[#f5f5f5] mx-4 rounded-lg overflow-hidden", children: _jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "bell-o", size: 18 }), t("notification")] }), _jsx(Switch, { checked: notifyEnabled, onChange: handleToggleNotification, loading: isTogglingNotification })] }) }), _jsx("div", { className: "bg-[#f5f5f5] mx-4 mt-3 rounded-lg overflow-hidden", children: _jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm w-full", onClick: handleToggleBlock, disabled: isTogglingBlock, children: [_jsx(Icon, { icon: "user-del-o", size: 18 }), t(isBlocked ? "unblock_user" : "block_user")] }) }), _jsx("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 mt-3 rounded-lg overflow-hidden", children: _jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmDelete(true), children: [_jsx(Icon, { icon: "trash-o", size: 18 }), t("delete_conversation")] }) })] }), _jsx(ConfirmModal, { open: confirmDelete, onOk: handleConfirmDelete, onCancel: () => setConfirmDelete(false), title: t("delete_conversation_confirm_title"), okText: t("delete_conversation"), cancelText: t("cancel"), okType: "danger", confirmLoading: isDeleting, children: _jsx("p", { children: t("delete_conversation_confirm_message") }) })] }));
69
70
  };
70
71
  export default SingleThreadInfo;
@@ -2,5 +2,5 @@ import "../styles/global.css";
2
2
  import { ChatContextType, ChatProviderProps } from "../types/chat";
3
3
  export declare const ChatContext: import("react").Context<ChatContextType>;
4
4
  export declare const useChatContext: () => ChatContextType;
5
- export declare const ChatProvider: ({ children, config, renderCustomMessage, }: ChatProviderProps) => import("react/jsx-runtime").JSX.Element;
5
+ export declare const ChatProvider: ({ children, config, renderCustomMessage, onNavigate, }: ChatProviderProps) => import("react/jsx-runtime").JSX.Element;
6
6
  //# sourceMappingURL=ChatContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChatContext.d.ts","sourceRoot":"","sources":["../../src/context/ChatContext.tsx"],"names":[],"mappings":"AAEA,OAAO,sBAAsB,CAAC;AAG9B,OAAO,EACL,eAAe,EACf,iBAAiB,EAGlB,MAAM,cAAc,CAAC;AAStB,eAAO,MAAM,WAAW,0CAOtB,CAAC;AAEH,eAAO,MAAM,cAAc,uBAAgC,CAAC;AAE5D,eAAO,MAAM,YAAY,GAAI,4CAI1B,iBAAiB,4CAsGnB,CAAC"}
1
+ {"version":3,"file":"ChatContext.d.ts","sourceRoot":"","sources":["../../src/context/ChatContext.tsx"],"names":[],"mappings":"AAEA,OAAO,sBAAsB,CAAC;AAG9B,OAAO,EACL,eAAe,EACf,iBAAiB,EAGlB,MAAM,cAAc,CAAC;AAStB,eAAO,MAAM,WAAW,0CAOtB,CAAC;AAEH,eAAO,MAAM,cAAc,uBAAgC,CAAC;AAE5D,eAAO,MAAM,YAAY,GAAI,wDAK1B,iBAAiB,4CAuGnB,CAAC"}
@@ -18,7 +18,7 @@ export const ChatContext = createContext({
18
18
  updateSyncStatus: () => { },
19
19
  });
20
20
  export const useChatContext = () => useContext(ChatContext);
21
- export const ChatProvider = ({ children, config, renderCustomMessage, }) => {
21
+ export const ChatProvider = ({ children, config, renderCustomMessage, onNavigate, }) => {
22
22
  const [connectStatus, setConnectStatus] = useState(ConnectStatus.Disconnected);
23
23
  const [syncStatus, setSyncStatus] = useState(SyncStatus.Success);
24
24
  const [user, setUser] = useState(null);
@@ -92,5 +92,6 @@ export const ChatProvider = ({ children, config, renderCustomMessage, }) => {
92
92
  updateConnectStatus,
93
93
  updateSyncStatus,
94
94
  renderCustomMessage,
95
+ onNavigate,
95
96
  }, children: _jsx(ConfigProvider, { getPopupContainer: (triggerNode) => document.body, children: _jsx(QueryClientProvider, { client: queryClient, children: _jsx(MainLayout, { children: children }) }) }) }));
96
97
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAuCA,eAAO,MAAM,cAAc,YA8Y1B,CAAC"}
1
+ {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAuCA,eAAO,MAAM,cAAc,YA2a1B,CAAC"}
@@ -19,7 +19,7 @@ import { invalidateSessionQueries } from "../../utils/queryHelpers";
19
19
  import emitter from "../../utils/events";
20
20
  const notPushType = [MessageType.TypingMessage, MessageType.RevokeMessage];
21
21
  export const useGlobalEvent = () => {
22
- const { user } = useChatContext();
22
+ const { user, onNavigate } = useChatContext();
23
23
  const userRef = useLatest(user);
24
24
  const queryClient = useQueryClient();
25
25
  const { updateConnectStatus, updateSyncStatus, getSelfUserInfo } = useChatContext();
@@ -27,6 +27,7 @@ export const useGlobalEvent = () => {
27
27
  const getConversationListByReq = useConversationStore((state) => state.getConversationListByReq);
28
28
  const getCurrentGroupInfoByReq = useConversationStore((state) => state.getCurrentGroupInfoByReq);
29
29
  const getSelfGroupPermissionByReq = useConversationStore((state) => state.getSelfGroupPermissionByReq);
30
+ const getCurrentMemberInGroupByReq = useConversationStore((state) => state.getCurrentMemberInGroupByReq);
30
31
  const { mutate: refetchChatToken } = useRefetchChatToken();
31
32
  const accessToken = useAuthStore((state) => state.accessToken);
32
33
  const chatToken = useAuthStore((state) => state.chatToken);
@@ -43,6 +44,9 @@ export const useGlobalEvent = () => {
43
44
  await Promise.all([
44
45
  getCurrentGroupInfoByReq(currentConversation.groupID),
45
46
  getSelfGroupPermissionByReq(currentConversation.groupID),
47
+ // roleLevel comes only from this call — without it, isAdminOrOwner
48
+ // checks (disband, mute toggle, etc.) fail closed.
49
+ getCurrentMemberInGroupByReq(currentConversation.groupID),
46
50
  ]);
47
51
  };
48
52
  const revokedMessageHandler = ({ data }) => {
@@ -210,10 +214,29 @@ export const useGlobalEvent = () => {
210
214
  const groupMemberInfoChangedHandler = () => {
211
215
  void refreshCurrentGroupState();
212
216
  };
213
- const groupDismissedHandler = ({ data }) => {
217
+ // Shared by OnGroupDismissed and OnJoinedGroupDeleted (kicked) both
218
+ // leave the user out of a group they may have open, same cleanup either way.
219
+ const leftGroupHandler = ({ data }) => {
214
220
  const currentConversation = useConversationStore.getState().currentConversation;
215
221
  if ((currentConversation === null || currentConversation === void 0 ? void 0 : currentConversation.groupID) === data.groupID) {
216
222
  useConversationStore.getState().clearSelectedConversation();
223
+ // OnGroupDismissed also fires on the disbanding owner's own client —
224
+ // skip the modal for them, useDismissGroup already shows a toast.
225
+ const selfDisbandedGroupID = useConversationStore.getState().selfDisbandedGroupID;
226
+ if (selfDisbandedGroupID === data.groupID) {
227
+ useConversationStore.getState().setSelfDisbandedGroupID(null);
228
+ }
229
+ else {
230
+ useConversationStore.getState().showRemovedFromGroupModal();
231
+ }
232
+ const newSearchParams = new URLSearchParams(window.location.search);
233
+ newSearchParams.delete("threadId");
234
+ newSearchParams.delete("userId");
235
+ newSearchParams.delete("groupId");
236
+ newSearchParams.delete("botId");
237
+ const query = newSearchParams.toString();
238
+ const pathname = window.location.pathname;
239
+ onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(query ? `${pathname}?${query}` : pathname);
217
240
  }
218
241
  useConversationStore.setState((state) => ({
219
242
  conversationList: state.conversationList.filter((conversation) => conversation.groupID !== data.groupID),
@@ -258,7 +281,8 @@ export const useGlobalEvent = () => {
258
281
  // group info
259
282
  DChatSDK.on(CbEvents.OnGroupInfoChanged, groupInfoChangedHandler);
260
283
  DChatSDK.on(CbEvents.OnGroupMemberInfoChanged, groupMemberInfoChangedHandler);
261
- DChatSDK.on(CbEvents.OnGroupDismissed, groupDismissedHandler);
284
+ DChatSDK.on(CbEvents.OnGroupDismissed, leftGroupHandler);
285
+ DChatSDK.on(CbEvents.OnJoinedGroupDeleted, leftGroupHandler);
262
286
  // black list
263
287
  DChatSDK.on(CbEvents.OnBlackAdded, blackListChangedHandler);
264
288
  DChatSDK.on(CbEvents.OnBlackDeleted, blackListChangedHandler);
@@ -291,7 +315,8 @@ export const useGlobalEvent = () => {
291
315
  // group info
292
316
  DChatSDK.off(CbEvents.OnGroupInfoChanged, groupInfoChangedHandler);
293
317
  DChatSDK.off(CbEvents.OnGroupMemberInfoChanged, groupMemberInfoChangedHandler);
294
- DChatSDK.off(CbEvents.OnGroupDismissed, groupDismissedHandler);
318
+ DChatSDK.off(CbEvents.OnGroupDismissed, leftGroupHandler);
319
+ DChatSDK.off(CbEvents.OnJoinedGroupDeleted, leftGroupHandler);
295
320
  // black list
296
321
  DChatSDK.off(CbEvents.OnBlackAdded, blackListChangedHandler);
297
322
  DChatSDK.off(CbEvents.OnBlackDeleted, blackListChangedHandler);
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
  import { useCallback } from "react";
3
- import { useRouter } from "next/navigation";
4
3
  import { useQueryClient } from "@tanstack/react-query";
5
4
  import { message } from "antd";
6
5
  import { useTranslation } from "react-i18next";
7
6
  import { GroupType, GroupVerificationType, GroupVisibility, SessionType } from "@droppii-org/wasm-client-sdk";
8
7
  import { DChatSDK } from "../../constants/sdk";
9
8
  import useAuthStore from "../../store/auth";
9
+ import { useChatContext } from "../../context/ChatContext";
10
10
  import { generateDefaultGroupName } from "../../utils/groupName";
11
11
  import { QUERY_KEYS } from "../../services/query";
12
12
  const GET_CONVERSATION_RETRY_COUNT = 4;
@@ -43,7 +43,7 @@ const getOneConversationWithRetry = async (groupID) => {
43
43
  export const useCreateGroup = () => {
44
44
  const { t } = useTranslation("conversation-inbox");
45
45
  const { t: tCommon } = useTranslation();
46
- const router = useRouter();
46
+ const { onNavigate } = useChatContext();
47
47
  const queryClient = useQueryClient();
48
48
  const userID = useAuthStore((state) => state.userID);
49
49
  const createGroup = useCallback(async ({ groupName, isPublic, members, faceURL }) => {
@@ -78,9 +78,9 @@ export const useCreateGroup = () => {
78
78
  void queryClient.invalidateQueries({
79
79
  queryKey: [QUERY_KEYS.GET_CONVERSATION_GROUPS],
80
80
  });
81
- const searchParams = new URLSearchParams();
81
+ const searchParams = new URLSearchParams(window.location.search);
82
82
  searchParams.set("threadId", conversation.conversationID);
83
- router.push(`/chat?${searchParams.toString()}`);
83
+ onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(`${window.location.pathname}?${searchParams.toString()}`);
84
84
  return conversation;
85
85
  }
86
86
  catch (error) {
@@ -88,6 +88,6 @@ export const useCreateGroup = () => {
88
88
  message.error(errorMessage);
89
89
  throw error;
90
90
  }
91
- }, [router, t, tCommon, userID, queryClient]);
91
+ }, [onNavigate, t, tCommon, userID, queryClient]);
92
92
  return { createGroup };
93
93
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useDismissGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useDismissGroup.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,eAAe;aAEmB,MAAM;WAYpD,CAAC"}
1
+ {"version":3,"file":"useDismissGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useDismissGroup.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe;aAImB,MAAM;WA6BpD,CAAC"}
@@ -5,9 +5,14 @@ import useConversationStore from "../../store/conversation";
5
5
  import useSessionStore from "../../store/session";
6
6
  import { SessionStatus } from "../../types/chat";
7
7
  import emitter from "../../utils/events";
8
+ import { useChatContext } from "../../context/ChatContext";
8
9
  export const useDismissGroup = () => {
10
+ const { onNavigate } = useChatContext();
9
11
  return useMutation({
10
12
  mutationFn: async ({ groupID }) => {
13
+ // Set before the request — OnGroupDismissed also fires on our own
14
+ // client, with no ordering guarantee vs. this promise resolving.
15
+ useConversationStore.getState().setSelfDisbandedGroupID(groupID);
11
16
  const { data } = await DChatSDK.dismissGroup(groupID);
12
17
  emitter.emit("GROUP_MEMBERS_CHANGED");
13
18
  return data;
@@ -17,6 +22,19 @@ export const useDismissGroup = () => {
17
22
  useSessionStore
18
23
  .getState()
19
24
  .setFilterSummary({ status: SessionStatus.IN_PROCESS });
25
+ // Clearing the store alone won't navigate away — URL threadId must
26
+ // also clear, or DeskConversationList re-selects it (see PR #226).
27
+ const newSearchParams = new URLSearchParams(window.location.search);
28
+ newSearchParams.delete("threadId");
29
+ newSearchParams.delete("userId");
30
+ newSearchParams.delete("groupId");
31
+ newSearchParams.delete("botId");
32
+ const query = newSearchParams.toString();
33
+ const pathname = window.location.pathname;
34
+ onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(query ? `${pathname}?${query}` : pathname);
35
+ },
36
+ onError: () => {
37
+ useConversationStore.getState().setSelfDisbandedGroupID(null);
20
38
  },
21
39
  });
22
40
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useConversationSessionState.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useConversationSessionState.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI3D,UAAU,iCAAiC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,WAAW,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,2BAA2B,GAAI,gDAIzC,iCAAiC;;;;;CAmBnC,CAAC"}
1
+ {"version":3,"file":"useConversationSessionState.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useConversationSessionState.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI3D,UAAU,iCAAiC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,WAAW,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,2BAA2B,GAAI,gDAIzC,iCAAiC;;;;;CAoBnC,CAAC"}
@@ -7,6 +7,7 @@ export const useConversationSessionState = ({ conversationId, conversationType,
7
7
  const currentGroupId = isGroupConversation ? groupId : undefined;
8
8
  const { data: conversationSessions, refetch: refetchConversationSessions, } = useGetListSessionByConversation({
9
9
  conversationId,
10
+ enabled: !isGroupConversation,
10
11
  });
11
12
  const latestConversationSession = conversationSessions === null || conversationSessions === void 0 ? void 0 : conversationSessions[0];
12
13
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layout/index.tsx"],"names":[],"mappings":"AAIA,OAAO,iBAAiB,CAAC;AAKzB,UAAU,eAAe;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AACD,QAAA,MAAM,UAAU,GAAI,cAAc,eAAe,4CAKhD,CAAC;AAEF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layout/index.tsx"],"names":[],"mappings":"AAKA,OAAO,iBAAiB,CAAC;AAYzB,UAAU,eAAe;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AACD,QAAA,MAAM,UAAU,GAAI,cAAc,eAAe,4CAyBhD,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -1,13 +1,24 @@
1
1
  "use client";
2
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from "react-i18next";
3
4
  import { useGlobalEvent } from "../hooks/global/useGlobalEvent";
4
5
  import dayjs from "dayjs";
5
6
  import "dayjs/locale/vi";
7
+ import utc from "dayjs/plugin/utc";
8
+ import timezone from "dayjs/plugin/timezone";
6
9
  import { useSyncUsersInfo } from "../hooks/user/useUsersInfo";
10
+ import useConversationStore from "../store/conversation";
11
+ import InfoModal from "../components/common/InfoModal";
12
+ dayjs.extend(utc);
13
+ dayjs.extend(timezone);
14
+ dayjs.tz.setDefault("Asia/Ho_Chi_Minh");
7
15
  dayjs.locale("vi");
8
16
  const MainLayout = ({ children }) => {
17
+ const { t } = useTranslation();
9
18
  useGlobalEvent();
10
19
  useSyncUsersInfo();
11
- return _jsx(_Fragment, { children: children });
20
+ const removedFromGroupModalOpen = useConversationStore((state) => state.removedFromGroupModalOpen);
21
+ const hideRemovedFromGroupModal = useConversationStore((state) => state.hideRemovedFromGroupModal);
22
+ return (_jsxs(_Fragment, { children: [children, _jsx(InfoModal, { open: removedFromGroupModalOpen, onOk: hideRemovedFromGroupModal, title: t("removed_from_group_modal_title"), okText: t("removed_from_group_modal_ok"), children: _jsx("p", { children: t("removed_from_group_modal_message") }) })] }));
12
23
  };
13
24
  export default MainLayout;
@@ -102,6 +102,8 @@
102
102
  "select_status": "Chọn trạng thái",
103
103
  "invalid_file_format": "{{fileName}} không đúng định dạng JPG, JPEG, PNG hoặc VIDEO",
104
104
  "file_size_exceeded": "{{fileName}} có kích thước tập tin vượt quá {{maxSize}}MB",
105
+ "avatar_invalid_format": "Vui lòng dùng file PNG, JPG, JPEG, WEBP",
106
+ "avatar_size_exceeded": "Vui lòng dùng ảnh dưới {{maxSize}}MB",
105
107
  "file_processing_error": "Đã xảy ra lỗi khi xử lý tệp",
106
108
  "invalid_document_format": "{{fileName}} không đúng định dạng (chỉ hỗ trợ PDF, DOC, DOCX)",
107
109
  "view_message": "Xem tin nhắn",
@@ -154,10 +156,10 @@
154
156
  "mute_member_success": "Đã tắt quyền gửi tin nhắn",
155
157
  "unmute_member_success": "Đã bật lại quyền gửi tin nhắn",
156
158
  "send_message": "Gửi tin nhắn",
157
- "transfer_owner": "Chuyển quyền chủ nhóm",
158
- "transfer_owner_success": "Đã chuyển quyền chủ nhóm",
159
- "transfer_owner_confirm_title": "Chuyển quyền chủ nhóm",
160
- "transfer_owner_confirm_message": "Bạn có chắc muốn chuyển quyền chủ nhóm cho {{name}}? Bạn sẽ trở thành quản trị viên.",
159
+ "transfer_owner": "Chuyển quyền sở hữu nhóm",
160
+ "transfer_owner_success": "Đã chuyển quyền sở hữu nhóm",
161
+ "transfer_owner_confirm_title": "Chuyển quyền sở hữu nhóm",
162
+ "transfer_owner_confirm_message": "Bạn có chắc muốn chuyển quyền sở hữu nhóm cho {{name}}?",
161
163
  "assign_as_admin": "Chỉ định làm quản trị viên",
162
164
  "assign_as_admin_success": "Đã chỉ định làm quản trị viên",
163
165
  "assign_admin_confirm_title": "Cấp quyền quản trị viên",
@@ -167,9 +169,9 @@
167
169
  "remove_from_admin_success": "Đã xoá khỏi quản trị viên",
168
170
  "remove_from_group": "Xoá khỏi nhóm",
169
171
  "remove_member_success": "Đã xoá thành viên khỏi nhóm",
170
- "remove_member_failed": "Không thể xoá thành viên. Vui lòng thử lại",
171
- "remove_member_confirm_title": "Xoá thành viên khỏi nhóm",
172
- "remove_member_confirm_message": "Bạn chắc muốn xoá {{name}} khỏi nhóm?",
172
+ "remove_member_failed": "Xoá thành viên không thành công, vui lòng thử lại!",
173
+ "remove_member_confirm_title": "Xóa khỏi nhóm",
174
+ "remove_member_confirm_message": "Bạn chắc chắn muốn xóa \"{{name}}\" khỏi nhóm ?",
173
175
  "remove_and_block": "Xoá và chặn",
174
176
  "choose_emoji": "Chọn biểu tượng cảm xúc",
175
177
  "emoji_smileys": "Mặt cười",
@@ -194,6 +196,7 @@
194
196
  "team_messages": "Tin nhắn nhóm",
195
197
  "join_group_required": "Bạn phải là thành viên để gửi tin nhắn",
196
198
  "send_message_disabled": "Chỉ quản trị viên được phép gửi tin nhắn",
199
+ "send_message_disabled_group_mute": "Chỉ chủ nhóm và quản trị viên được phép gửi tin nhắn",
197
200
  "join_group": "Tham gia",
198
201
  "join_group_success": "Tham gia nhóm thành công",
199
202
  "join_group_failed": "Tham gia nhóm thất bại",
@@ -237,6 +240,9 @@
237
240
  "disband_group_failed": "Giải tán nhóm thất bại",
238
241
  "disband_group_confirm_title": "Giải tán nhóm?",
239
242
  "disband_group_confirm_message": "Bạn có chắc chắn muốn giải tán nhóm này không? Hành động này không thể hoàn tác.",
243
+ "removed_from_group_modal_title": "Không thể xem cuộc trò chuyện này",
244
+ "removed_from_group_modal_message": "Bạn hiện không có quyền truy cập vào cuộc trò chuyện hoặc tin nhắn này.",
245
+ "removed_from_group_modal_ok": "Đã hiểu",
240
246
  "delete_conversation_success": "Đã xóa trò chuyện",
241
247
  "delete_conversation_failed": "Xóa trò chuyện thất bại",
242
248
  "delete_conversation_confirm_title": "Xóa trò chuyện?",
@@ -248,4 +254,4 @@
248
254
  "block_user_success": "Đã chặn người dùng",
249
255
  "unblock_user_success": "Đã bỏ chặn người dùng",
250
256
  "block_user_failed": "Thao tác chặn/bỏ chặn thất bại"
251
- }
257
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation-detail/index.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AAWrF,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,6CAA6C,CAAC;AAGrD,MAAM,WAAW,4BACf,SAAQ,4BAA4B,EACpC,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,uBAAuB,GAAI,wFAS9B,4BAA4B,mDAyK9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation-detail/index.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AAWrF,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,6CAA6C,CAAC;AAGrD,MAAM,WAAW,4BACf,SAAQ,4BAA4B,EACpC,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,uBAAuB,GAAI,wFAS9B,4BAA4B,mDAwK9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -12,8 +12,8 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  };
13
13
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  import { useState } from "react";
15
- import { usePathname, useRouter, useSearchParams } from "next/navigation";
16
15
  import { useTranslation } from "react-i18next";
16
+ import { useChatContext } from "../../context/ChatContext";
17
17
  import { useBoolean } from "ahooks";
18
18
  import clsx from "clsx";
19
19
  import { Empty, Spin } from "antd";
@@ -31,9 +31,7 @@ const DChatConversationDetail = (_a) => {
31
31
  var _b;
32
32
  var { className, children, onBack, conversationId, userId, groupId, botId } = _a, inputProps = __rest(_a, ["className", "children", "onBack", "conversationId", "userId", "groupId", "botId"]);
33
33
  const { t } = useTranslation("conversation-inbox");
34
- const router = useRouter();
35
- const pathname = usePathname();
36
- const searchParams = useSearchParams();
34
+ const { onNavigate } = useChatContext();
37
35
  const conversation = useConversationStore((state) => state.conversationData);
38
36
  const { isResolving, error } = useResolveConversation({
39
37
  conversationId,
@@ -52,17 +50,18 @@ const DChatConversationDetail = (_a) => {
52
50
  const [resourceTab, setResourceTab] = useState(null);
53
51
  const handleBack = () => {
54
52
  if (typeof window !== "undefined" && window.history.length > 1) {
55
- router.back();
53
+ window.history.back();
56
54
  onBack === null || onBack === void 0 ? void 0 : onBack();
57
55
  return;
58
56
  }
59
- const newSearchParams = new URLSearchParams(searchParams);
57
+ const newSearchParams = new URLSearchParams(window.location.search);
60
58
  newSearchParams.delete("threadId");
61
59
  newSearchParams.delete("userId");
62
60
  newSearchParams.delete("groupId");
63
61
  newSearchParams.delete("botId");
64
62
  const query = newSearchParams.toString();
65
- router.replace(query ? `${pathname}?${query}` : pathname);
63
+ const pathname = window.location.pathname;
64
+ onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(query ? `${pathname}?${query}` : pathname);
66
65
  onBack === null || onBack === void 0 ? void 0 : onBack();
67
66
  };
68
67
  if (isResolving) {
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/store/conversation.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAM3C,QAAA,MAAM,oBAAoB,gFAgSvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/store/conversation.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAM3C,QAAA,MAAM,oBAAoB,gFAqTvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}