@droppii-org/chat-sdk 0.7.11 → 0.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
  2. package/dist/components/conversation-detail/ConversationDetailInput.js +19 -3
  3. package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts +8 -1
  4. package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts.map +1 -1
  5. package/dist/components/conversation-inbox/ConversationInboxHeader.js +20 -3
  6. package/dist/components/conversation-inbox/PublicGroupList.d.ts +3 -0
  7. package/dist/components/conversation-inbox/PublicGroupList.d.ts.map +1 -0
  8. package/dist/components/conversation-inbox/PublicGroupList.js +43 -0
  9. package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.d.ts +3 -1
  10. package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.d.ts.map +1 -1
  11. package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.js +12 -4
  12. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  13. package/dist/hooks/global/useGlobalEvent.js +5 -0
  14. package/dist/hooks/group/useGroupSendRestriction.d.ts +4 -0
  15. package/dist/hooks/group/useGroupSendRestriction.d.ts.map +1 -0
  16. package/dist/hooks/group/useGroupSendRestriction.js +31 -0
  17. package/dist/hooks/group/useJoinPublicGroup.d.ts +2 -0
  18. package/dist/hooks/group/useJoinPublicGroup.d.ts.map +1 -1
  19. package/dist/hooks/group/useJoinPublicGroup.js +44 -2
  20. package/dist/hooks/search/useSearchPublicGroups.d.ts +5 -1
  21. package/dist/hooks/search/useSearchPublicGroups.d.ts.map +1 -1
  22. package/dist/hooks/search/useSearchPublicGroups.js +5 -3
  23. package/dist/locales/vi/common.json +2 -1
  24. package/dist/locales/vi/conversation-inbox.json +6 -1
  25. package/dist/screens/conversation-inbox/index.d.ts +2 -0
  26. package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
  27. package/dist/screens/conversation-inbox/index.js +9 -2
  28. package/dist/store/conversation.d.ts.map +1 -1
  29. package/dist/store/conversation.js +15 -11
  30. package/dist/utils/messageLog.js +1 -1
  31. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAoB/D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,wEAAwE;IACxE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACpE;AAED,QAAA,MAAM,uBAAuB,GAAI,8GAQ9B,4BAA4B,4CAkxB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAoB/D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,wEAAwE;IACxE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACpE;AAED,QAAA,MAAM,uBAAuB,GAAI,8GAQ9B,4BAA4B,4CA4yB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -9,6 +9,7 @@ import { Icon } from "../../components/icon";
9
9
  import { useSendMessage } from "../../hooks/message/useSendMessage";
10
10
  import { useReportTypingState } from "../../hooks/message/useReportTypingState";
11
11
  import { useJoinPublicGroup } from "../../hooks/group/useJoinPublicGroup";
12
+ import { useGroupSendRestriction } from "../../hooks/group/useGroupSendRestriction";
12
13
  import { useAgentSupportThreadJoin } from "../../hooks/session/useAgentSupportThreadJoin";
13
14
  import useAuthStore from "../../store/auth";
14
15
  import useConversationStore from "../../store/conversation";
@@ -48,13 +49,14 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
48
49
  const getSelfGroupPermissionByReq = useConversationStore((state) => state.getSelfGroupPermissionByReq);
49
50
  const { mutate: joinPublicGroup, isPending: isJoiningPublicGroup } = useJoinPublicGroup();
50
51
  const supportThreadJoin = useAgentSupportThreadJoin(conversationData, onJoinSupportThread);
52
+ const groupSendState = useGroupSendRestriction(conversationData);
51
53
  const handleJoinPublicGroup = useCallback(() => {
52
54
  const groupID = conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID;
53
55
  if (!groupID)
54
56
  return;
55
57
  joinPublicGroup({ groupID }, {
56
58
  onSuccess: async () => {
57
- toastMessage.success(tCommon("join_group_success"));
59
+ toastMessage.success(tCommon("public_group_joined"));
58
60
  // updateCurrentConversation's revalidation only runs on a
59
61
  // conversationID change — this is the same conversation the user
60
62
  // was already viewing, so refresh membership/permissions directly
@@ -232,6 +234,17 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
232
234
  useEffect(() => {
233
235
  adjustTextareaHeight();
234
236
  }, [value, adjustTextareaHeight]);
237
+ // The textarea mounts only once a spinner/notice branch gives way, after the
238
+ // draft was restored; size it again so a multi-line draft isn't one row tall.
239
+ useEffect(() => {
240
+ requestAnimationFrame(adjustTextareaHeight);
241
+ }, [
242
+ groupSendState,
243
+ isViewOnlyMember,
244
+ supportThreadJoin.isCheckingMembership,
245
+ supportThreadJoin.needsJoin,
246
+ adjustTextareaHeight,
247
+ ]);
235
248
  useEffect(() => {
236
249
  if (!isFocused)
237
250
  return;
@@ -489,8 +502,8 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
489
502
  event.preventDefault();
490
503
  void handleSend();
491
504
  };
492
- // Don't flash a composer the agent may not be allowed to use.
493
- if (supportThreadJoin.isCheckingMembership) {
505
+ // Don't flash a composer the agent or member may not be allowed to use.
506
+ if (supportThreadJoin.isCheckingMembership || groupSendState === "checking") {
494
507
  return (_jsx("div", { "data-testid": "conversation-detail-input-checking", className: "flex w-full shrink-0 items-center justify-center bg-chat-bubble-neutral/[0.05] px-4 py-3", children: _jsx(Spin, { size: "small" }) }));
495
508
  }
496
509
  if (isViewOnlyMember || supportThreadJoin.needsJoin) {
@@ -500,6 +513,9 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
500
513
  ? supportThreadJoin.join
501
514
  : handleJoinPublicGroup, children: tCommon("join_group") })] }));
502
515
  }
516
+ if (groupSendState === "restricted") {
517
+ return (_jsx("div", { "data-testid": "conversation-detail-input-send-restricted", className: "flex w-full shrink-0 items-center justify-center bg-chat-bubble-neutral/[0.05] px-4 py-3", children: _jsx("span", { className: "text-sm text-gray-500", children: t("thread_detail.cannot_send_notice") }) }));
518
+ }
503
519
  return (_jsxs("div", { ref: containerRef, "data-testid": "conversation-detail-input", "data-cp-name": "ConversationDetailInput", className: "flex w-full shrink-0 flex-col bg-chat-bubble-neutral/[0.05]", children: [_jsx("input", { ref: mediaInputRef, type: "file", accept: "image/jpeg,image/png,image/jpg,video/*", multiple: true, className: "hidden", onChange: handleMediaInputChange }), _jsx("input", { ref: documentInputRef, type: "file", accept: ".pdf,.doc,.docx", multiple: true, className: "hidden", onChange: handleDocumentInputChange }), _jsx(ReplyPreviewBar, {}), listUploadFiles.length > 0 && (_jsx(FilePreviewList, { files: listUploadFiles, onRemove: (file) => handleRemoveFile(file.uid), onVideoReady: (uid) => setPendingVideoUids((prev) => prev.filter((p) => p !== uid)) })), showLinkPreview && previewUrl && (_jsx(LinkInputPreview, { url: previewUrl, onRemove: () => setPreviewDismissed(true) })), _jsxs("div", { className: "flex items-end gap-3 px-3 py-2", children: [_jsx("div", { className: "flex shrink-0 items-center", children: _jsx("button", { type: "button", onClick: handleToggleMoreOption, "aria-label": t("thread_detail.more_option.toggle"), "aria-expanded": isMoreOptionOpen, "data-testid": "conversation-detail-more-option-toggle", className: "flex py-3 items-center justify-center rounded-full", children: isMoreOptionOpen ? (_jsx(Icon, { icon: "close-circle-b", size: 24, className: "text-[var(--dc-color-primary)]" })) : (_jsx(Icon, { icon: "plus-circle-o", size: 24, className: "text-gray-900" })) }) }), _jsx("div", { className: clsx("flex min-w-0 flex-1 overflow-hidden rounded-xl border bg-white transition-shadow", isFocused
504
520
  ? "border-[var(--dc-color-primary-focus)] shadow-[0px_0px_2px_var(--dc-color-primary-focus)]"
505
521
  : "border-gray-50"), style: { minHeight: CONTAINER_MIN_HEIGHT }, children: _jsxs("div", { className: "flex min-w-0 flex-[1_0_0] items-center gap-2 self-stretch px-3 py-1", children: [onOpenBotMenu && (_jsx("button", { type: "button", onClick: onOpenBotMenu, "aria-label": t("thread_detail.bot.menu.open_menu"), "data-testid": "conversation-detail-open-bot-menu", className: "flex shrink-0 items-center justify-center", children: _jsx(Icon, { icon: "chat-suggest-o", size: 24, className: "text-gray-600" }) })), _jsx("textarea", { ref: textareaRef, value: value, rows: 1, onChange: (event) => {
@@ -1,10 +1,17 @@
1
+ export declare enum InboxTab {
2
+ Messages = "MESSAGES",
3
+ Community = "COMMUNITY"
4
+ }
1
5
  export interface ConversationInboxHeaderProps {
2
6
  onOpenSearch?: () => void;
3
7
  /** Overrides the default "tabs.messages" i18n title (e.g. Mall buyer's fixed "Tin nhắn" label). */
4
8
  title?: string;
5
9
  /** Extra action rendered after the search icon — e.g. Mall seller's contact icon. */
6
10
  rightSlot?: React.ReactNode;
11
+ /** With onChangeTab, shows the Tin nhắn | Cộng đồng tabs instead of the single title. */
12
+ activeTab?: InboxTab;
13
+ onChangeTab?: (tab: InboxTab) => void;
7
14
  }
8
- declare const ConversationInboxHeader: ({ onOpenSearch, title, rightSlot, }: ConversationInboxHeaderProps) => import("react/jsx-runtime").JSX.Element;
15
+ declare const ConversationInboxHeader: ({ onOpenSearch, title, rightSlot, activeTab, onChangeTab, }: ConversationInboxHeaderProps) => import("react/jsx-runtime").JSX.Element;
9
16
  export default ConversationInboxHeader;
10
17
  //# sourceMappingURL=ConversationInboxHeader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationInboxHeader.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-inbox/ConversationInboxHeader.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B;AAED,QAAA,MAAM,uBAAuB,GAAI,qCAI9B,4BAA4B,4CAiC9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"ConversationInboxHeader.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-inbox/ConversationInboxHeader.tsx"],"names":[],"mappings":"AAMA,oBAAY,QAAQ;IAClB,QAAQ,aAAa;IACrB,SAAS,cAAc;CACxB;AAOD,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,yFAAyF;IACzF,SAAS,CAAC,EAAE,QAAQ,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,IAAI,CAAC;CACvC;AA8CD,QAAA,MAAM,uBAAuB,GAAI,6DAM9B,4BAA4B,4CA0C9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -1,9 +1,26 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import clsx from "clsx";
3
4
  import { useTranslation } from "react-i18next";
4
5
  import { Icon } from "../../components/icon";
5
- const ConversationInboxHeader = ({ onOpenSearch, title, rightSlot, }) => {
6
+ export var InboxTab;
7
+ (function (InboxTab) {
8
+ InboxTab["Messages"] = "MESSAGES";
9
+ InboxTab["Community"] = "COMMUNITY";
10
+ })(InboxTab || (InboxTab = {}));
11
+ const INBOX_TABS = [
12
+ { key: InboxTab.Messages, labelKey: "tabs.messages" },
13
+ { key: InboxTab.Community, labelKey: "tabs.community" },
14
+ ];
15
+ const HeaderTab = ({ label, active, testId, onClick }) => {
16
+ const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: clsx("pb-1.5 text-center text-[16px] font-bold leading-[160%] tracking-[0.16px]", active ? "text-[var(--dc-color-primary)]" : "text-gray-400"), children: label }), _jsx("span", { className: clsx("h-[3px] w-full rounded-t", active ? "bg-[var(--dc-color-primary)]" : "bg-transparent") })] }));
17
+ const className = "flex h-full flex-col items-center justify-end px-3";
18
+ if (!onClick)
19
+ return _jsx("div", { className: className, children: content });
20
+ return (_jsx("button", { type: "button", role: "tab", "aria-selected": active, "data-testid": testId, className: clsx(className, "cursor-pointer"), onClick: onClick, children: content }));
21
+ };
22
+ const ConversationInboxHeader = ({ onOpenSearch, title, rightSlot, activeTab = InboxTab.Messages, onChangeTab, }) => {
6
23
  const { t } = useTranslation("conversation-inbox");
7
- return (_jsx("div", { "data-testid": "conversation-inbox-header", "data-cp-name": "ConversationInboxHeader", className: "flex w-full flex-col border-b border-gray-100 bg-white", children: _jsxs("div", { className: "flex h-12 items-end px-3", children: [_jsxs("div", { className: "flex h-full flex-col items-center justify-end px-3", children: [_jsx("span", { className: "pb-1.5 text-center text-[16px] font-bold leading-[160%] tracking-[0.16px] text-[var(--dc-color-primary)]", children: title !== null && title !== void 0 ? title : t("tabs.messages") }), _jsx("span", { className: "h-[3px] w-full rounded-t bg-[var(--dc-color-primary)]" })] }), _jsxs("div", { className: "ml-auto mb-1.5 flex shrink-0 items-center gap-2", children: [onOpenSearch && (_jsx("button", { type: "button", "data-testid": "conversation-inbox-search", "aria-label": t("search_placeholder"), className: "flex shrink-0 cursor-pointer items-center justify-center p-1 text-[var(--dc-color-nav-icon)]", onClick: onOpenSearch, children: _jsx(Icon, { icon: "search-o", size: 24 }) })), rightSlot] })] }) }));
24
+ return (_jsx("div", { "data-testid": "conversation-inbox-header", "data-cp-name": "ConversationInboxHeader", className: "flex w-full flex-col border-b border-gray-100 bg-white", children: _jsxs("div", { className: "flex h-12 items-end px-3", children: [onChangeTab ? (_jsx("div", { role: "tablist", className: "flex h-full items-end", children: INBOX_TABS.map((tab) => (_jsx(HeaderTab, { label: t(tab.labelKey), active: activeTab === tab.key, testId: `conversation-inbox-tab-${tab.key.toLowerCase()}`, onClick: () => onChangeTab(tab.key) }, tab.key))) })) : (_jsx(HeaderTab, { label: title !== null && title !== void 0 ? title : t("tabs.messages"), active: true })), _jsxs("div", { className: "ml-auto mb-1.5 flex shrink-0 items-center gap-2", children: [onOpenSearch && (_jsx("button", { type: "button", "data-testid": "conversation-inbox-search", "aria-label": t("search_placeholder"), className: "flex shrink-0 cursor-pointer items-center justify-center p-1 text-[var(--dc-color-nav-icon)]", onClick: onOpenSearch, children: _jsx(Icon, { icon: "search-o", size: 24 }) })), rightSlot] })] }) }));
8
25
  };
9
26
  export default ConversationInboxHeader;
@@ -0,0 +1,3 @@
1
+ declare const PublicGroupList: () => import("react/jsx-runtime").JSX.Element;
2
+ export default PublicGroupList;
3
+ //# sourceMappingURL=PublicGroupList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PublicGroupList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-inbox/PublicGroupList.tsx"],"names":[],"mappings":"AAoBA,QAAA,MAAM,eAAe,+CAqFpB,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Spin } from "antd";
4
+ import InfiniteScroll from "react-infinite-scroll-component";
5
+ import { useTranslation } from "react-i18next";
6
+ import { useChatContext } from "../../context/ChatContext";
7
+ import { useChatNavigate } from "../../hooks/conversation/useChatNavigate";
8
+ import { useSearchPublicGroups } from "../../hooks/search/useSearchPublicGroups";
9
+ import useConversationStore from "../../store/conversation";
10
+ import { ConnectStatus } from "../../types/chat";
11
+ import { ContactSearchStateMessage, PublicGroupResultItem, buildContactSearchParams, } from "./contact-search";
12
+ const SCROLL_TARGET_ID = "inbox-public-group-scroll";
13
+ // Inbox `Cộng đồng` tab: every public group the seller can see, joined or not,
14
+ // from the same searchPublicGroups call chat-mobile's CommunityTab uses.
15
+ const PublicGroupList = () => {
16
+ const { t } = useTranslation("conversation-inbox");
17
+ const onNavigate = useChatNavigate();
18
+ const setPendingResolveTarget = useConversationStore((state) => state.setPendingResolveTarget);
19
+ const { connectStatus } = useChatContext();
20
+ // The tab can be opened while the engine still boots; a call then fails at
21
+ // once and would leave the error state with nothing to retry it.
22
+ const { groups, fetchNextPage, hasNextPage, isFetchingNextPage, isPending, isError, } = useSearchPublicGroups({
23
+ searchTerm: "",
24
+ allowEmptyTerm: true,
25
+ enabled: connectStatus === ConnectStatus.Connected,
26
+ });
27
+ const handleOpenGroup = (groupID) => {
28
+ const target = { groupId: groupID };
29
+ setPendingResolveTarget(target);
30
+ onNavigate(`${window.location.pathname}?${buildContactSearchParams(target).toString()}`);
31
+ };
32
+ if (isPending) {
33
+ return (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) }));
34
+ }
35
+ if (isError) {
36
+ return (_jsx(ContactSearchStateMessage, { testId: "inbox-public-group-failed", children: t("contact_search.failed") }));
37
+ }
38
+ if (groups.length === 0) {
39
+ return (_jsx(ContactSearchStateMessage, { testId: "inbox-public-group-empty", children: t("community.empty") }));
40
+ }
41
+ return (_jsx("div", { id: SCROLL_TARGET_ID, "data-testid": "inbox-public-group-list", "data-cp-name": "PublicGroupList", className: "flex-1 overflow-y-auto", children: _jsx(InfiniteScroll, { dataLength: groups.length, next: fetchNextPage, hasMore: !!hasNextPage, scrollableTarget: SCROLL_TARGET_ID, loader: isFetchingNextPage ? (_jsx("div", { className: "flex items-center justify-center py-3", children: _jsx(Spin, { size: "small" }) })) : null, children: groups.map((group) => (_jsx(PublicGroupResultItem, { group: group, onOpen: handleOpenGroup, stayOnJoin: true }, group.groupID))) }) }));
42
+ };
43
+ export default PublicGroupList;
@@ -2,7 +2,9 @@ import type { SearchPublicGroupInfo } from "@droppii-org/wasm-client-sdk";
2
2
  interface PublicGroupResultItemProps {
3
3
  group: SearchPublicGroupInfo;
4
4
  onOpen: (groupID: string) => void;
5
+ /** Inbox `Cộng đồng` tab: a join keeps the seller on the list and toasts instead of opening the thread. */
6
+ stayOnJoin?: boolean;
5
7
  }
6
- declare const PublicGroupResultItem: ({ group, onOpen, }: PublicGroupResultItemProps) => import("react/jsx-runtime").JSX.Element;
8
+ declare const PublicGroupResultItem: ({ group, onOpen, stayOnJoin, }: PublicGroupResultItemProps) => import("react/jsx-runtime").JSX.Element;
7
9
  export default PublicGroupResultItem;
8
10
  //# sourceMappingURL=PublicGroupResultItem.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PublicGroupResultItem.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/PublicGroupResultItem.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAK1E,UAAU,0BAA0B;IAClC,KAAK,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,QAAA,MAAM,qBAAqB,GAAI,oBAG5B,0BAA0B,4CA4E5B,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"PublicGroupResultItem.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/PublicGroupResultItem.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAK1E,UAAU,0BAA0B;IAClC,KAAK,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,2GAA2G;IAC3G,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,QAAA,MAAM,qBAAqB,GAAI,gCAI5B,0BAA0B,4CAyF5B,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -1,22 +1,30 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import clsx from "clsx";
4
- import { Spin } from "antd";
4
+ import { Spin, message as toastMessage } from "antd";
5
5
  import { useTranslation } from "react-i18next";
6
6
  import { useJoinPublicGroup } from "../../../hooks/group/useJoinPublicGroup";
7
7
  import GroupAvatar from "../../../components/group/GroupAvatar";
8
8
  import { CONTACT_SEARCH_AVATAR_SIZE } from "./constants";
9
- const PublicGroupResultItem = ({ group, onOpen, }) => {
9
+ const PublicGroupResultItem = ({ group, onOpen, stayOnJoin = false, }) => {
10
10
  var _a, _b;
11
11
  const { t } = useTranslation("conversation-inbox");
12
+ const { t: tCommon } = useTranslation("common");
12
13
  const { mutate: joinGroup, isPending } = useJoinPublicGroup();
13
- // Joined groups open straight away; the rest join first, then open.
14
+ // Joined groups open straight away; search joins first, then opens.
14
15
  const handleAction = () => {
15
16
  if (group.isJoined) {
16
17
  onOpen(group.groupID);
17
18
  return;
18
19
  }
19
- joinGroup({ groupID: group.groupID }, { onSuccess: () => onOpen(group.groupID) });
20
+ if (!stayOnJoin) {
21
+ joinGroup({ groupID: group.groupID }, { onSuccess: () => onOpen(group.groupID) });
22
+ return;
23
+ }
24
+ joinGroup({ groupID: group.groupID }, {
25
+ onSuccess: () => toastMessage.success(tCommon("public_group_joined")),
26
+ onError: () => toastMessage.error(tCommon("join_group_failed")),
27
+ });
20
28
  };
21
29
  return (_jsxs("div", { "data-cp-name": "PublicGroupResultItem", "data-testid": `contact-search-public-group-${group.groupID}`, className: "flex items-center gap-2 border-b border-gray-25 bg-white p-3", children: [_jsxs("button", { type: "button", className: "flex min-w-0 flex-1 items-center gap-2 text-left", onClick: () => onOpen(group.groupID), children: [_jsx(GroupAvatar, { faceURL: group.faceURL, groupName: group.groupName, size: CONTACT_SEARCH_AVATAR_SIZE, className: "shrink-0" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-1 pb-0.5", children: [_jsx("span", { className: "truncate text-[16px] font-medium leading-[1.6] text-black", children: group.groupName }), _jsxs("span", { className: "flex min-w-0 flex-col text-[14px] leading-[1.6] tracking-[0.14px] text-gray-600", children: [_jsx("span", { className: "truncate", children: t("contact_search.member_count", {
22
30
  count: (_a = group.memberCount) !== null && _a !== void 0 ? _a : 0,
@@ -1 +1 @@
1
- {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAiDA,eAAO,MAAM,cAAc,YAwnB1B,CAAC"}
1
+ {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAkDA,eAAO,MAAM,cAAc,YA4nB1B,CAAC"}
@@ -20,6 +20,7 @@ import { useQueryClient } from "@tanstack/react-query";
20
20
  import { PeerType } from "../../types/conversation";
21
21
  import { ignoreInternalLatestMessage } from "../../utils/common";
22
22
  import { invalidateSessionQueries } from "../../utils/queryHelpers";
23
+ import { refreshPublicGroupLists } from "../../hooks/group/useJoinPublicGroup";
23
24
  import emitter from "../../utils/events";
24
25
  const notPushType = [MessageType.TypingMessage, MessageType.RevokeMessage];
25
26
  export const useGlobalEvent = () => {
@@ -381,6 +382,8 @@ export const useGlobalEvent = () => {
381
382
  conversationList: state.conversationList.filter((conversation) => conversation.groupID !== data.groupID),
382
383
  }));
383
384
  }
385
+ // Public-group cards turn back to Tham gia (or drop out when disbanded).
386
+ void refreshPublicGroupLists(queryClient, data.groupID);
384
387
  };
385
388
  // Thin wrappers matching the SDK's fixed callback signature, forwarding
386
389
  // to leftGroupHandler with the right reason.
@@ -434,6 +437,8 @@ export const useGlobalEvent = () => {
434
437
  void queryClient.invalidateQueries({
435
438
  queryKey: [QUERY_KEYS.IS_JOINED_GROUP, data.groupID],
436
439
  });
440
+ // A first-time join from another device or an invite flips the card to Truy cập.
441
+ void refreshPublicGroupLists(queryClient, data.groupID);
437
442
  };
438
443
  const setIMListener = () => {
439
444
  //account
@@ -0,0 +1,4 @@
1
+ import { type ConversationItem } from "@droppii-org/wasm-client-sdk";
2
+ export type GroupSendState = "checking" | "restricted" | "allowed";
3
+ export declare const useGroupSendRestriction: (conversation?: ConversationItem | null) => GroupSendState;
4
+ //# sourceMappingURL=useGroupSendRestriction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGroupSendRestriction.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useGroupSendRestriction.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,8BAA8B,CAAC;AAKtC,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAC;AAMnE,eAAO,MAAM,uBAAuB,GAClC,eAAe,gBAAgB,GAAG,IAAI,KACrC,cA4BF,CAAC"}
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { GroupMemberRole, GroupStatus, } from "@droppii-org/wasm-client-sdk";
3
+ import useConversationStore from "../../store/conversation";
4
+ import { PeerType } from "../../types/conversation";
5
+ import { useGroupPermission } from "./useGroupPermission";
6
+ // A confirmed member is restricted with no SEND_MESSAGE permission, a muted
7
+ // group (owner/admin exempt) or an own mute. "checking" lasts until the store
8
+ // finishes this thread's membership check (it sets currentConversation last),
9
+ // so a restricted member never sees the composer flash first.
10
+ export const useGroupSendRestriction = (conversation) => {
11
+ var _a;
12
+ const isConfirmedGroup = (conversation === null || conversation === void 0 ? void 0 : conversation.peerType) === PeerType.GROUP;
13
+ const groupID = conversation === null || conversation === void 0 ? void 0 : conversation.groupID;
14
+ const isChecked = useConversationStore((state) => { var _a; return ((_a = state.currentConversation) === null || _a === void 0 ? void 0 : _a.conversationID) === (conversation === null || conversation === void 0 ? void 0 : conversation.conversationID); });
15
+ const groupInfo = useConversationStore((state) => groupID ? state.currentGroupInfo.get(groupID) : undefined);
16
+ const member = useConversationStore((state) => groupID ? state.currentMemberInGroup.get(groupID) : undefined);
17
+ const permissions = useGroupPermission(groupID);
18
+ if (!isConfirmedGroup)
19
+ return "allowed";
20
+ if (!isChecked)
21
+ return "checking";
22
+ // Non-members are handled by the view-only footer, not this notice.
23
+ if (!member)
24
+ return "allowed";
25
+ const isGroupMuted = (groupInfo === null || groupInfo === void 0 ? void 0 : groupInfo.status) === GroupStatus.Muted &&
26
+ member.roleLevel === GroupMemberRole.Normal;
27
+ const isMemberMuted = ((_a = member.muteEndTime) !== null && _a !== void 0 ? _a : 0) > Date.now();
28
+ return !permissions.sendMessage || isGroupMuted || isMemberMuted
29
+ ? "restricted"
30
+ : "allowed";
31
+ };
@@ -1,6 +1,8 @@
1
+ import { type QueryClient } from "@tanstack/react-query";
1
2
  interface JoinPublicGroupInput {
2
3
  groupID: string;
3
4
  }
5
+ export declare const refreshPublicGroupLists: (queryClient: QueryClient, groupID: string) => Promise<void>;
4
6
  export declare const useJoinPublicGroup: () => import("@tanstack/react-query").UseMutationResult<unknown, Error, JoinPublicGroupInput, unknown>;
5
7
  export {};
6
8
  //# sourceMappingURL=useJoinPublicGroup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useJoinPublicGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useJoinPublicGroup.ts"],"names":[],"mappings":"AAQA,UAAU,oBAAoB;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,eAAO,MAAM,kBAAkB,wGAoB9B,CAAC"}
1
+ {"version":3,"file":"useJoinPublicGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useJoinPublicGroup.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAU/B,UAAU,oBAAoB;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB;AAYD,eAAO,MAAM,uBAAuB,GAClC,aAAa,WAAW,EACxB,SAAS,MAAM,kBAQd,CAAC;AAoBJ,eAAO,MAAM,kBAAkB,wGA+C9B,CAAC"}
@@ -1,9 +1,34 @@
1
1
  "use client";
2
- import { useMutation, useQueryClient } from "@tanstack/react-query";
3
- import { GroupJoinSource } from "@droppii-org/wasm-client-sdk";
2
+ import { useMutation, useQueryClient, } from "@tanstack/react-query";
3
+ import { GroupJoinSource, } from "@droppii-org/wasm-client-sdk";
4
4
  import { DChatSDK } from "../../constants/sdk";
5
5
  import { QUERY_KEYS } from "../../services/query";
6
+ import useConversationStore from "../../store/conversation";
6
7
  import emitter from "../../utils/events";
8
+ const listShowsGroup = (query, groupID) => {
9
+ var _a, _b;
10
+ return !!((_b = (_a = query.state.data) === null || _a === void 0 ? void 0 : _a.pages) === null || _b === void 0 ? void 0 : _b.some((page) => { var _a; return (_a = page === null || page === void 0 ? void 0 : page.groups) === null || _a === void 0 ? void 0 : _a.some((group) => group.groupID === groupID); }));
11
+ };
12
+ // Refetches only the public-group lists that show this group, so membership
13
+ // events for unrelated threads don't reload every loaded page. A load already
14
+ // running (e.g. the next page) is left alone: cancelling it stalls paging.
15
+ export const refreshPublicGroupLists = (queryClient, groupID) => queryClient.invalidateQueries({
16
+ queryKey: [QUERY_KEYS.SEARCH_PUBLIC_GROUPS],
17
+ predicate: (query) => listShowsGroup(query, groupID),
18
+ }, { cancelRefetch: false });
19
+ const IN_FLIGHT_LIST_WAIT_MS = 5000;
20
+ // A list still loading was built from pre-join data and would undo the flip
21
+ // when it lands, so let it land first (bounded, in case the engine stalls).
22
+ const waitForPublicGroupLoads = (queryClient) => {
23
+ const loads = queryClient
24
+ .getQueryCache()
25
+ .findAll({ queryKey: [QUERY_KEYS.SEARCH_PUBLIC_GROUPS], fetchStatus: "fetching" })
26
+ .map((query) => { var _a; return (_a = query.promise) === null || _a === void 0 ? void 0 : _a.catch(() => undefined); });
27
+ return Promise.race([
28
+ Promise.all(loads),
29
+ new Promise((resolve) => setTimeout(resolve, IN_FLIGHT_LIST_WAIT_MS)),
30
+ ]);
31
+ };
7
32
  // Joining straight from search results, matching chat-mobile's GroupItem —
8
33
  // GroupJoinSource.Search is what BE uses to attribute the join.
9
34
  export const useJoinPublicGroup = () => {
@@ -20,9 +45,26 @@ export const useJoinPublicGroup = () => {
20
45
  },
21
46
  // Joining a group already cached locally fires no join event, so member-only UI reads this.
22
47
  onSuccess: async (_data, { groupID }) => {
48
+ // A kick earlier in the session leaves this marker, which would drop the
49
+ // member/permission refetch that follows the join.
50
+ useConversationStore.setState((state) => state.nonMemberGroupID === groupID ? { nonMemberGroupID: null } : state);
23
51
  const queryKey = [QUERY_KEYS.IS_JOINED_GROUP, groupID];
24
52
  await queryClient.cancelQueries({ queryKey });
25
53
  queryClient.setQueryData(queryKey, true);
54
+ // Every public-group list (inbox tab, search) flips this card to Truy cập and counts the new member.
55
+ await waitForPublicGroupLoads(queryClient);
56
+ queryClient.setQueriesData({ queryKey: [QUERY_KEYS.SEARCH_PUBLIC_GROUPS] }, (data) => data && Object.assign(Object.assign({}, data), { pages: data.pages.map((page) => {
57
+ var _a;
58
+ return (Object.assign(Object.assign({}, page), { groups: ((_a = page === null || page === void 0 ? void 0 : page.groups) !== null && _a !== void 0 ? _a : []).map((group) => {
59
+ var _a;
60
+ return group.groupID === groupID && !group.isJoined
61
+ ? Object.assign(Object.assign({}, group), { isJoined: true, memberCount: ((_a = group.memberCount) !== null && _a !== void 0 ? _a : 0) + 1 }) : group;
62
+ }) }));
63
+ }) }));
64
+ },
65
+ // A stale card can offer Tham gia for a group joined elsewhere; refetch so it settles.
66
+ onError: (_error, { groupID }) => {
67
+ void refreshPublicGroupLists(queryClient, groupID);
26
68
  },
27
69
  });
28
70
  };
@@ -3,14 +3,18 @@ interface UseSearchPublicGroupsParams {
3
3
  searchTerm: string;
4
4
  enabled?: boolean;
5
5
  pageSize?: number;
6
+ /** The inbox `Cộng đồng` tab lists every public group, so it queries with an empty keyword. */
7
+ allowEmptyTerm?: boolean;
6
8
  }
7
- export declare const useSearchPublicGroups: ({ searchTerm, enabled, pageSize, }: UseSearchPublicGroupsParams) => {
9
+ export declare const useSearchPublicGroups: ({ searchTerm, enabled, pageSize, allowEmptyTerm, }: UseSearchPublicGroupsParams) => {
8
10
  groups: SearchPublicGroupInfo[];
9
11
  total: number;
10
12
  fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<import("@droppii-org/wasm-client-sdk").SearchPublicGroupsResult, unknown>, Error>>;
11
13
  hasNextPage: boolean;
12
14
  isFetchingNextPage: boolean;
13
15
  isLoading: boolean;
16
+ /** True until the first page arrives, including while the query waits to be enabled. */
17
+ isPending: boolean;
14
18
  isError: boolean;
15
19
  };
16
20
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"useSearchPublicGroups.d.ts","sourceRoot":"","sources":["../../../src/hooks/search/useSearchPublicGroups.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,8BAA8B,CAAC;AAKtC,UAAU,2BAA2B;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAKD,eAAO,MAAM,qBAAqB,GAAI,oCAInC,2BAA2B;;;;;;;;CA0D7B,CAAC"}
1
+ {"version":3,"file":"useSearchPublicGroups.d.ts","sourceRoot":"","sources":["../../../src/hooks/search/useSearchPublicGroups.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,8BAA8B,CAAC;AAKtC,UAAU,2BAA2B;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAKD,eAAO,MAAM,qBAAqB,GAAI,oDAKnC,2BAA2B;;;;;;;IAgE1B,wFAAwF;;;CAI3F,CAAC"}
@@ -8,10 +8,10 @@ import { PAGE_SIZE } from "../../constants";
8
8
  // Community results come from the IM SDK rather than the contacts REST API —
9
9
  // same call chat-mobile uses for its `Cộng đồng` tab, so both platforms return
10
10
  // the identical public-group set (joined and not yet joined).
11
- export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = PAGE_SIZE.medium, }) => {
11
+ export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = PAGE_SIZE.medium, allowEmptyTerm = false, }) => {
12
12
  var _a, _b, _c;
13
13
  const trimmedSearchTerm = searchTerm.trim();
14
- const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } = useInfiniteQuery({
14
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isPending, isError, } = useInfiniteQuery({
15
15
  initialPageParam: 0,
16
16
  queryKey: [QUERY_KEYS.SEARCH_PUBLIC_GROUPS, trimmedSearchTerm, pageSize],
17
17
  queryFn: async ({ pageParam = 0 }) => {
@@ -29,7 +29,7 @@ export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = P
29
29
  const fetched = allPages.reduce((total, page) => { var _a, _b; return total + ((_b = (_a = page === null || page === void 0 ? void 0 : page.groups) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); }, 0);
30
30
  return fetched < ((_a = lastPage === null || lastPage === void 0 ? void 0 : lastPage.total) !== null && _a !== void 0 ? _a : 0) ? fetched : undefined;
31
31
  },
32
- enabled: enabled && trimmedSearchTerm !== "",
32
+ enabled: enabled && (allowEmptyTerm || trimmedSearchTerm !== ""),
33
33
  // `Tất cả` waits for this query, so a failing engine must not hold it
34
34
  // through the default three retries.
35
35
  retry: 1,
@@ -56,6 +56,8 @@ export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = P
56
56
  hasNextPage,
57
57
  isFetchingNextPage,
58
58
  isLoading,
59
+ /** True until the first page arrives, including while the query waits to be enabled. */
60
+ isPending,
59
61
  isError,
60
62
  };
61
63
  };
@@ -137,7 +137,7 @@
137
137
  "group_notification_owner_transferred": "{{name}} đã chuyển quyền trưởng nhóm cho {{newOwner}}",
138
138
  "group_notification_member_kicked": "{{name}} đã xóa {{members}} khỏi nhóm",
139
139
  "group_notification_member_invited": "{{name}} đã thêm {{members}} vào nhóm",
140
- "group_notification_member_enter": "{{name}} đã vào nhóm",
140
+ "group_notification_member_enter": "{{name}} đã tham gia nhóm",
141
141
  "group_notification_member_enter_via_link": "{{name}} đã tham gia nhóm qua link",
142
142
  "group_notification_dismissed": "{{name}} đã giải tán nhóm",
143
143
  "group_notification_member_muted": "{{name}} đã tắt quyền gửi tin nhắn của {{target}}",
@@ -220,6 +220,7 @@
220
220
  "join_group": "Tham gia",
221
221
  "join_group_success": "Tham gia nhóm thành công",
222
222
  "join_group_failed": "Tham gia nhóm thất bại",
223
+ "public_group_joined": "Đã tham gia nhóm",
223
224
  "join_group_confirm_title": "Tham gia nhóm",
224
225
  "join_group_confirm_cta": "Tham gia nhóm",
225
226
  "join_link_unavailable_title": "Liên kết không còn khả dụng",
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "tabs": {
3
- "messages": "Tin nhắn"
3
+ "messages": "Tin nhắn",
4
+ "community": "Cộng đồng"
5
+ },
6
+ "community": {
7
+ "empty": "Không có nhóm cộng đồng nào"
4
8
  },
5
9
  "search_placeholder": "Tìm kiếm",
6
10
  "compose": {
@@ -97,6 +101,7 @@
97
101
  "options_title": "Tuỳ chọn",
98
102
  "input_placeholder": "Nhập tin nhắn...",
99
103
  "view_only_notice": "Bạn cần tham gia nhóm để gửi tin nhắn",
104
+ "cannot_send_notice": "Bạn không thể gửi tin nhắn vào nhóm này",
100
105
  "resolve_failed": "Không thể mở cuộc trò chuyện",
101
106
  "search": {
102
107
  "open": "Tìm tin nhắn",
@@ -26,6 +26,8 @@ export interface DChatConversationInboxProps {
26
26
  showPinThread?: boolean;
27
27
  /** Forwarded to the detail screen — see DChatConversationDetailProps.showThreadSearch. */
28
28
  showThreadSearch?: boolean;
29
+ /** BIZ: Tin nhắn | Cộng đồng tabs in the header, Cộng đồng listing public groups. Defaults to false. */
30
+ showCommunityTab?: boolean;
29
31
  }
30
32
  declare const DChatConversationInbox: (props: DChatConversationInboxProps) => import("react/jsx-runtime").JSX.Element;
31
33
  export default DChatConversationInbox;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation-inbox/index.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAM/D,MAAM,WAAW,2BAA2B;IAC1C,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,6FAA6F;IAC7F,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC;;;;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;IACpC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mHAAmH;IACnH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0FAA0F;IAC1F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AA4OD,QAAA,MAAM,sBAAsB,GAAI,OAAO,2BAA2B,4CAQjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation-inbox/index.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAM/D,MAAM,WAAW,2BAA2B;IAC1C,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,6FAA6F;IAC7F,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC;;;;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;IACpC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mHAAmH;IACnH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0FAA0F;IAC1F,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,wGAAwG;IACxG,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAqPD,QAAA,MAAM,sBAAsB,GAAI,OAAO,2BAA2B,4CAQjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
3
4
  import { useSearchParams } from "next/navigation";
4
5
  import { useTranslation } from "react-i18next";
5
6
  import { Empty } from "antd";
@@ -11,6 +12,8 @@ import { useChatContext } from "../../context/ChatContext";
11
12
  import { useConversationList } from "../../hooks/conversation/useConversationList";
12
13
  import { useActiveConversation } from "../../hooks/conversation/useActiveConversation";
13
14
  import { ConversationInboxHeader } from "../../components/conversation-inbox";
15
+ import { InboxTab } from "../../components/conversation-inbox/ConversationInboxHeader";
16
+ import PublicGroupList from "../../components/conversation-inbox/PublicGroupList";
14
17
  import ConversationInboxList from "../../components/conversation-inbox/ConversationInboxList";
15
18
  import { ContactSearchPanel } from "../../components/conversation-inbox/contact-search";
16
19
  import DChatConversationDetail from "../../screens/conversation-detail";
@@ -77,11 +80,15 @@ const useInboxConversationList = (chatCategory) => {
77
80
  const isApplicationTypeValid = Object.values(DChatApplicationType).includes(applicationType);
78
81
  return Object.assign(Object.assign({}, useConversationList({ enabled: isApplicationTypeValid, chatCategory })), { isApplicationTypeValid });
79
82
  };
80
- const DChatConversationInboxBody = ({ onSelectThread, className, moreOptions, showSearch = true, headerTitle, headerRightSlot, onCopyMessageLink, onSearchMessageRevoked, showSticker, showProfileActions, showPinThread, showThreadSearch, target, conversationList, }) => {
83
+ const DChatConversationInboxBody = ({ onSelectThread, className, moreOptions, showSearch = true, headerTitle, headerRightSlot, onCopyMessageLink, onSearchMessageRevoked, showSticker, showProfileActions, showPinThread, showThreadSearch, showCommunityTab = false, target, conversationList, }) => {
81
84
  const { t } = useTranslation("conversation-inbox");
82
85
  const selectedConversationId = useConversationStore((state) => state.selectedConversationId);
83
86
  const removedFromGroupModalOpen = useConversationStore((state) => state.removedFromGroupModalOpen);
84
87
  const [isSearchOpen, { setTrue: openSearch, setFalse: closeSearch }] = useBoolean(false);
88
+ // Kept in the body, not the list: the body stays mounted while a thread is
89
+ // open, so Back lands on the same tab.
90
+ const [activeTab, setActiveTab] = useState(InboxTab.Messages);
91
+ const isCommunityTab = showCommunityTab && activeTab === InboxTab.Community;
85
92
  const { threadId, userId, groupId, botId, messageId } = target;
86
93
  // Any of the four targets opens the detail screen — useResolveConversation
87
94
  // turns userId/groupId/botId into a thread (creating the bot one when
@@ -105,7 +112,7 @@ const DChatConversationInboxBody = ({ onSelectThread, className, moreOptions, sh
105
112
  // `relative` + `overflow-hidden` keep ContactSearchPanel's Drawer
106
113
  // (getContainer={false}) inside this column instead of escaping to
107
114
  // the viewport — same containment the conversation detail root uses.
108
- "relative flex h-full w-full flex-col overflow-hidden bg-white", className), children: [_jsx(ConversationInboxHeader, { onOpenSearch: showSearch ? openSearch : undefined, title: headerTitle, rightSlot: headerRightSlot }), !conversationList.isApplicationTypeValid ? (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Empty, { image: _jsx("div", { className: "flex justify-center", children: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-[var(--dc-color-primary-light)]" }) }), description: t("empty.no_conversations") }) })) : (_jsx(ConversationInboxList, { items: conversationList.items, isLoading: conversationList.isLoading, isFetchingNextPage: conversationList.isFetchingNextPage, hasNextPage: conversationList.hasNextPage, onLoadMore: conversationList.fetchNextPage, onSelectThread: onSelectThread, showPinThread: showPinThread })), _jsx(ContactSearchPanel, { open: isSearchOpen, onClose: closeSearch })] }));
115
+ "relative flex h-full w-full flex-col overflow-hidden bg-white", className), children: [_jsx(ConversationInboxHeader, { onOpenSearch: showSearch ? openSearch : undefined, title: headerTitle, rightSlot: headerRightSlot, activeTab: activeTab, onChangeTab: showCommunityTab ? setActiveTab : undefined }), isCommunityTab ? (_jsx(PublicGroupList, {})) : !conversationList.isApplicationTypeValid ? (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Empty, { image: _jsx("div", { className: "flex justify-center", children: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-[var(--dc-color-primary-light)]" }) }), description: t("empty.no_conversations") }) })) : (_jsx(ConversationInboxList, { items: conversationList.items, isLoading: conversationList.isLoading, isFetchingNextPage: conversationList.isFetchingNextPage, hasNextPage: conversationList.hasNextPage, onLoadMore: conversationList.fetchNextPage, onSelectThread: onSelectThread, showPinThread: showPinThread })), _jsx(ContactSearchPanel, { open: isSearchOpen, onClose: closeSearch })] }));
109
116
  };
110
117
  // ChatProvider's useUrlState prop picks which target-resolution variant
111
118
  // mounts — kept as two full components (not one with a conditional hook)
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/store/conversation.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAY3C,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,SAElD,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,oFAOpB,CAAC;AAUX,eAAO,MAAM,cAAc,GAAI,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,SAEhE,CAAC;AAEF,QAAA,MAAM,oBAAoB,gFA4hBvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/store/conversation.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAiB3C,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,SAElD,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,oFAOpB,CAAC;AAUX,eAAO,MAAM,cAAc,GAAI,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,SAEhE,CAAC;AAEF,QAAA,MAAM,oBAAoB,gFA0hBvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}
@@ -7,6 +7,10 @@ import { PeerType } from "../types/conversation";
7
7
  import useUserStore from "./user";
8
8
  import useAuthStore from "./auth";
9
9
  const CONVERSATION_SPLIT_COUNT = 500;
10
+ // Bumped on every updateCurrentConversation call and on clear: a group check
11
+ // that resolves after the user moved on (even back to the thread already set)
12
+ // must not apply its verdict or overwrite currentConversation.
13
+ let conversationSwitchSeq = 0;
10
14
  // Module-level (not store state): ChatProvider syncs this from its useUrlState
11
15
  // prop so clearSelectedConversation below can skip touching window.history in
12
16
  // bubble mode — this store has no React context access of its own.
@@ -118,6 +122,7 @@ const useConversationStore = create((set, get) => ({
118
122
  },
119
123
  updateCurrentConversation: async (conversation, options) => {
120
124
  var _a;
125
+ const switchSeq = ++conversationSwitchSeq;
121
126
  if (!conversation) {
122
127
  set({
123
128
  currentConversation: undefined,
@@ -168,6 +173,8 @@ const useConversationStore = create((set, get) => ({
168
173
  // fail closed until an unrelated WS event happens to refresh it.
169
174
  get().getCurrentMemberInGroupByReq(groupID),
170
175
  ]);
176
+ if (switchSeq !== conversationSwitchSeq)
177
+ return;
171
178
  }
172
179
  }
173
180
  // Derives view-only/block state from fetch/cache above for fresh
@@ -240,12 +247,9 @@ const useConversationStore = create((set, get) => ({
240
247
  conversationList: state.conversationList.map((conversation) => conversation.groupID === groupID
241
248
  ? Object.assign(Object.assign({}, conversation), { showName: groupInfo.groupName, faceURL: groupInfo.faceURL }) : conversation),
242
249
  }));
243
- // Kick-guard only — a response for a group the user has since navigated
244
- // away from is still allowed to populate the cache (that's what makes
245
- // reopening it fast); only a confirmed non-member response is dropped.
246
- if (get().nonMemberGroupID === groupID) {
247
- return;
248
- }
250
+ // No kick-guard here: group info grants no membership, and a kicked
251
+ // viewer needs its visibility to reopen a public group read-only instead
252
+ // of being blocked. Member/permission fetches keep the guard.
249
253
  set((state) => ({
250
254
  currentGroupInfo: new Map(state.currentGroupInfo).set(groupID, Object.assign({}, groupInfo)),
251
255
  }));
@@ -267,9 +271,9 @@ const useConversationStore = create((set, get) => ({
267
271
  console.error("Error fetching group members:", error);
268
272
  return;
269
273
  }
270
- // Kick-guard only (same rationale as getCurrentGroupInfoByReq above) —
271
- // runs concurrently with getSelfGroupPermissionByReq, so this can still
272
- // race a kick event; nonMemberGroupID is what actually protects here.
274
+ // Kick-guard: a response still in flight when a kick lands must not
275
+ // restore the membership; runs concurrently with getSelfGroupPermissionByReq,
276
+ // so nonMemberGroupID is what actually protects here.
273
277
  if (get().nonMemberGroupID === groupID || !memberInfo) {
274
278
  return;
275
279
  }
@@ -306,6 +310,7 @@ const useConversationStore = create((set, get) => ({
306
310
  });
307
311
  },
308
312
  clearSelectedConversation: (options) => {
313
+ conversationSwitchSeq += 1;
309
314
  // DeskConversationList's threadId-driven effect re-selects the same
310
315
  // conversation on its next run otherwise (conversationList changes
311
316
  // trigger it) — clobbering this reset right back.
@@ -483,8 +488,7 @@ const useConversationStore = create((set, get) => ({
483
488
  if (isSameInfo && isSameHeader)
484
489
  return state;
485
490
  return Object.assign({
486
- // Set directly: getCurrentGroupInfoByReq's kick-guard skips groups the
487
- // viewer has left, which is exactly who reaches this view.
491
+ // Set from this poll's response; no second getSpecifiedGroupsInfo call.
488
492
  currentGroupInfo: new Map(state.currentGroupInfo).set(groupID, Object.assign({}, latest)) }, (isSameHeader
489
493
  ? {}
490
494
  : {
@@ -142,7 +142,7 @@ export const getGroupNotificationText = (contentType, detailJson, t, senderNickn
142
142
  ? String(t(key, { name: entrantName }))
143
143
  : joinedViaLink
144
144
  ? `${entrantName} đã tham gia nhóm qua link`
145
- : `${entrantName} đã vào nhóm`;
145
+ : `${entrantName} đã tham gia nhóm`;
146
146
  }
147
147
  case MessageType.GroupDismissed:
148
148
  return t
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droppii-org/chat-sdk",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "Droppii React Chat SDK",
5
5
  "repository": {
6
6
  "type": "git",