@droppii-org/chat-sdk 0.6.38 → 0.6.40

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 (51) hide show
  1. package/dist/assets/images/stickerInvalid.png +0 -0
  2. package/dist/components/conversation-detail/ConversationDetailInput.d.ts +3 -1
  3. package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
  4. package/dist/components/conversation-detail/ConversationDetailInput.js +35 -5
  5. package/dist/components/conversation-detail/sticker-picker/StickerCategoryTabs.d.ts +9 -0
  6. package/dist/components/conversation-detail/sticker-picker/StickerCategoryTabs.d.ts.map +1 -0
  7. package/dist/components/conversation-detail/sticker-picker/StickerCategoryTabs.js +38 -0
  8. package/dist/components/conversation-detail/sticker-picker/StickerPicker.d.ts +7 -0
  9. package/dist/components/conversation-detail/sticker-picker/StickerPicker.d.ts.map +1 -0
  10. package/dist/components/conversation-detail/sticker-picker/StickerPicker.js +113 -0
  11. package/dist/components/conversation-detail/sticker-picker/StickerSection.d.ts +12 -0
  12. package/dist/components/conversation-detail/sticker-picker/StickerSection.d.ts.map +1 -0
  13. package/dist/components/conversation-detail/sticker-picker/StickerSection.js +46 -0
  14. package/dist/components/conversation-detail/sticker-picker/constants.d.ts +8 -0
  15. package/dist/components/conversation-detail/sticker-picker/constants.d.ts.map +1 -0
  16. package/dist/components/conversation-detail/sticker-picker/constants.js +1 -0
  17. package/dist/components/message/item/StickerMessage.d.ts.map +1 -1
  18. package/dist/components/message/item/StickerMessage.js +3 -5
  19. package/dist/components/message/reply/ReplyPreviewMedia.d.ts.map +1 -1
  20. package/dist/components/message/reply/ReplyPreviewMedia.js +8 -2
  21. package/dist/constants/images.d.ts +1 -0
  22. package/dist/constants/images.d.ts.map +1 -1
  23. package/dist/constants/images.js +2 -0
  24. package/dist/hooks/common/useOutsideTapDismiss.d.ts +12 -0
  25. package/dist/hooks/common/useOutsideTapDismiss.d.ts.map +1 -0
  26. package/dist/hooks/common/useOutsideTapDismiss.js +81 -0
  27. package/dist/hooks/message/useSendMessage.d.ts +7 -1
  28. package/dist/hooks/message/useSendMessage.d.ts.map +1 -1
  29. package/dist/hooks/message/useSendMessage.js +45 -2
  30. package/dist/hooks/sticker/useStickers.d.ts +5 -0
  31. package/dist/hooks/sticker/useStickers.d.ts.map +1 -0
  32. package/dist/hooks/sticker/useStickers.js +39 -0
  33. package/dist/locales/vi/conversation-inbox.json +6 -0
  34. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  35. package/dist/screens/conversation-detail/index.js +2 -1
  36. package/dist/screens/conversation-inbox/index.d.ts +2 -0
  37. package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
  38. package/dist/screens/conversation-inbox/index.js +2 -2
  39. package/dist/services/query.d.ts +3 -0
  40. package/dist/services/query.d.ts.map +1 -1
  41. package/dist/services/query.js +3 -0
  42. package/dist/services/routes.d.ts +3 -0
  43. package/dist/services/routes.d.ts.map +1 -1
  44. package/dist/services/routes.js +3 -0
  45. package/dist/styles/global.css +1 -1
  46. package/dist/types/chat.d.ts +1 -0
  47. package/dist/types/chat.d.ts.map +1 -1
  48. package/dist/types/sticker.d.ts +20 -0
  49. package/dist/types/sticker.d.ts.map +1 -0
  50. package/dist/types/sticker.js +1 -0
  51. package/package.json +1 -1
@@ -5,7 +5,9 @@ export interface ConversationDetailInputProps {
5
5
  onInputChange?: (value: string) => void;
6
6
  moreOptions?: MoreOptionConfig[];
7
7
  onOpenBotMenu?: () => void;
8
+ /** Shows the sticker picker entry in the composer. Defaults to true. */
9
+ showSticker?: boolean;
8
10
  }
9
- declare const ConversationDetailInput: ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, }: ConversationDetailInputProps) => import("react/jsx-runtime").JSX.Element;
11
+ declare const ConversationDetailInput: ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, showSticker, }: ConversationDetailInputProps) => import("react/jsx-runtime").JSX.Element;
10
12
  export default ConversationDetailInput;
11
13
  //# sourceMappingURL=ConversationDetailInput.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAmB/D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,QAAA,MAAM,uBAAuB,GAAI,4EAM9B,4BAA4B,4CAosB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AA2BA,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;CACvB;AAED,QAAA,MAAM,uBAAuB,GAAI,yFAO9B,4BAA4B,4CA0vB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -15,11 +15,13 @@ import useMessageDraftStore from "../../store/messageDraft";
15
15
  import { emit } from "../../utils/events";
16
16
  import { extractLinkUrls } from "../../utils/link";
17
17
  import { useFetchUrlMetadata } from "../../hooks/common/useFetchUrlMetadata";
18
+ import { useOutsideTapDismiss } from "../../hooks/common/useOutsideTapDismiss";
18
19
  import { processAndValidateDocuments, processAndValidateFiles, } from "../../utils/fileValidation";
19
20
  import { FilePreviewList } from "../../components/message/footer/FilePreview";
20
21
  import LinkInputPreview from "../../components/message/footer/LinkInputPreview";
21
22
  import { ReplyPreviewBar } from "../../components/message/reply/ReplyPreviewBar";
22
23
  import ConversationMoreOptionPanel from "./ConversationMoreOptionPanel";
24
+ import StickerPicker from "./sticker-picker/StickerPicker";
23
25
  import { useMentionTrigger } from "./mention/useMentionTrigger";
24
26
  import { useMentionOptions } from "../../components/message/footer/mention/useMentionOptions";
25
27
  import { useMentionTokens } from "./mention/useMentionTokens";
@@ -31,7 +33,7 @@ const MAX_LINES = 5;
31
33
  const CONTAINER_MIN_HEIGHT = 48;
32
34
  const FALLBACK_LINE_HEIGHT = 25.6;
33
35
  const MAX_MENTIONED_USERS = 10;
34
- const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, }) => {
36
+ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, showSticker = true, }) => {
35
37
  var _a;
36
38
  const { t } = useTranslation("conversation-inbox");
37
39
  const { t: tCommon } = useTranslation("common");
@@ -75,7 +77,7 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
75
77
  const sendMessageOptions = useMemo(() => ({
76
78
  scrollEvent: "CONVERSATION_CHAT_SCROLL_TO_BOTTOM_IF_AT_BOTTOM",
77
79
  }), []);
78
- const { sendMergeMessage } = useSendMessage(sendMessageOptions);
80
+ const { sendMergeMessage, sendStickerMessage } = useSendMessage(sendMessageOptions);
79
81
  const { reportActivity: reportTyping, stopTyping } = useReportTypingState(conversationId);
80
82
  const userID = useAuthStore((state) => state.userID);
81
83
  const setDraft = useMessageDraftStore((state) => state.setDraft);
@@ -95,6 +97,15 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
95
97
  const [isSending, setIsSending] = useState(false);
96
98
  const [previewDismissed, setPreviewDismissed] = useState(false);
97
99
  const [isMoreOptionOpen, { setFalse: closeMoreOption, toggle: toggleMoreOption },] = useBoolean(false);
100
+ const [isStickerOpen, { setFalse: closeSticker, toggle: toggleSticker },] = useBoolean(false);
101
+ useOutsideTapDismiss({
102
+ enabled: isMoreOptionOpen || isStickerOpen,
103
+ insideRef: containerRef,
104
+ onDismiss: () => {
105
+ closeMoreOption();
106
+ closeSticker();
107
+ },
108
+ });
98
109
  const [caretIndex, setCaretIndex] = useState(0);
99
110
  const rawMentionTrigger = useMentionTrigger(value, caretIndex);
100
111
  // Escape dismisses the menu for the CURRENT trigger span without erasing
@@ -169,10 +180,11 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
169
180
  }, [getTextareaHeights]);
170
181
  useEffect(() => {
171
182
  closeMoreOption();
183
+ closeSticker();
172
184
  setListUploadFiles([]);
173
185
  setPendingVideoUids([]);
174
186
  resetTokens();
175
- }, [conversationId, userID, closeMoreOption, resetTokens]);
187
+ }, [conversationId, userID, closeMoreOption, closeSticker, resetTokens]);
176
188
  useEffect(() => {
177
189
  setSelectedMentionIndex(0);
178
190
  }, [mentionTrigger === null || mentionTrigger === void 0 ? void 0 : mentionTrigger.triggerStart, mentionTrigger === null || mentionTrigger === void 0 ? void 0 : mentionTrigger.query]);
@@ -349,6 +361,7 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
349
361
  const handleFocus = () => {
350
362
  setIsFocused(true);
351
363
  closeMoreOption();
364
+ closeSticker();
352
365
  emit("CONVERSATION_CHAT_SCROLL_TO_BOTTOM_IF_AT_BOTTOM");
353
366
  };
354
367
  const handleBlur = () => {
@@ -359,9 +372,24 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
359
372
  var _a;
360
373
  if (!isMoreOptionOpen) {
361
374
  (_a = textareaRef.current) === null || _a === void 0 ? void 0 : _a.blur();
375
+ closeSticker();
362
376
  }
363
377
  toggleMoreOption();
364
378
  };
379
+ const handleToggleSticker = () => {
380
+ var _a;
381
+ if (!isStickerOpen) {
382
+ (_a = textareaRef.current) === null || _a === void 0 ? void 0 : _a.blur();
383
+ closeMoreOption();
384
+ }
385
+ toggleSticker();
386
+ };
387
+ // Picker stays open after a send so several stickers can go out in a row.
388
+ const handleSelectSticker = (sticker) => {
389
+ if (!isConversationReady)
390
+ return;
391
+ void sendStickerMessage(sticker);
392
+ };
365
393
  const insertNewline = (textarea) => {
366
394
  const { selectionStart, selectionEnd } = textarea;
367
395
  const nextValue = value.slice(0, selectionStart) + "\n" + value.slice(selectionEnd);
@@ -467,8 +495,10 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
467
495
  : "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) => {
468
496
  var _a;
469
497
  return handleChange(event.target.value, (_a = event.target.selectionStart) !== null && _a !== void 0 ? _a : event.target.value.length);
470
- }, onSelect: handleSelectionChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : t("thread_detail.input_placeholder"), "data-testid": "conversation-detail-message-input", className: "conversation-message-input-scrollbar min-h-0 min-w-0 flex-1 resize-none appearance-none border-0 bg-transparent p-0 text-[16px] leading-[1.6] tracking-[0.16px] text-black outline-none ring-0 placeholder:text-gray-400 focus:border-0 focus:outline-none focus:ring-0" }), isMentionMenuOpen && mentionMenuPosition && (_jsx(BizMentionMenu, { options: mentionOptions, selectedIndex: selectedMentionIndex, onSelect: selectMentionOption, onHoverIndex: setSelectedMentionIndex, position: mentionMenuPosition, limitReached: mentionLimitReached, limitMax: MAX_MENTIONED_USERS, visible: true }))] }) }), _jsx("div", { className: "flex shrink-0 items-center pb-1", children: _jsx("button", { type: "button", onClick: () => void handleSend(), disabled: !canSend, "data-testid": "conversation-detail-send", className: clsx("flex h-10 w-10 items-center justify-center rounded-full", canSend
498
+ }, onSelect: handleSelectionChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : t("thread_detail.input_placeholder"), "data-testid": "conversation-detail-message-input", className: "conversation-message-input-scrollbar min-h-0 min-w-0 flex-1 resize-none appearance-none border-0 bg-transparent p-0 text-[16px] leading-[1.6] tracking-[0.16px] text-black outline-none ring-0 placeholder:text-gray-400 focus:border-0 focus:outline-none focus:ring-0" }), isMentionMenuOpen && mentionMenuPosition && (_jsx(BizMentionMenu, { options: mentionOptions, selectedIndex: selectedMentionIndex, onSelect: selectMentionOption, onHoverIndex: setSelectedMentionIndex, position: mentionMenuPosition, limitReached: mentionLimitReached, limitMax: MAX_MENTIONED_USERS, visible: true })), showSticker && (_jsx("button", { type: "button", onClick: handleToggleSticker, "aria-label": t("thread_detail.sticker.toggle"), "aria-expanded": isStickerOpen, "data-testid": "conversation-detail-sticker-toggle", className: "flex shrink-0 items-center justify-center", children: _jsx(Icon, { icon: isStickerOpen ? "sticker-b" : "sticker-o", size: 24, className: isStickerOpen
499
+ ? "text-[var(--dc-color-primary)]"
500
+ : "text-gray-600" }) }))] }) }), _jsx("div", { className: "flex shrink-0 items-center pb-1", children: _jsx("button", { type: "button", onClick: () => void handleSend(), disabled: !canSend, "data-testid": "conversation-detail-send", className: clsx("flex h-10 w-10 items-center justify-center rounded-full", canSend
471
501
  ? "bg-[var(--dc-color-primary)]"
472
- : "bg-[var(--dc-color-primary)] opacity-25"), children: _jsx(Icon, { icon: "send-b", size: 20, className: "text-white" }) }) })] }), isMoreOptionOpen && (_jsx(ConversationMoreOptionPanel, { disabled: !isConversationReady, options: moreOptions, onTriggerMediaInput: () => { var _a; return (_a = mediaInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, onTriggerDocumentInput: () => { var _a; return (_a = documentInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } }))] }));
502
+ : "bg-[var(--dc-color-primary)] opacity-25"), children: _jsx(Icon, { icon: "send-b", size: 20, className: "text-white" }) }) })] }), isMoreOptionOpen && (_jsx(ConversationMoreOptionPanel, { disabled: !isConversationReady, options: moreOptions, onTriggerMediaInput: () => { var _a; return (_a = mediaInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, onTriggerDocumentInput: () => { var _a; return (_a = documentInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } })), showSticker && isStickerOpen && (_jsx(StickerPicker, { onSelectSticker: handleSelectSticker }))] }));
473
503
  };
474
504
  export default ConversationDetailInput;
@@ -0,0 +1,9 @@
1
+ import { type StickerTab } from "./constants";
2
+ interface StickerCategoryTabsProps {
3
+ tabs: StickerTab[];
4
+ activeTabId: string | null;
5
+ onSelectTab: (tabId: string) => void;
6
+ }
7
+ declare const StickerCategoryTabs: ({ tabs, activeTabId, onSelectTab, }: StickerCategoryTabsProps) => import("react/jsx-runtime").JSX.Element;
8
+ export default StickerCategoryTabs;
9
+ //# sourceMappingURL=StickerCategoryTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StickerCategoryTabs.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/sticker-picker/StickerCategoryTabs.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAErE,UAAU,wBAAwB;IAChC,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAID,QAAA,MAAM,mBAAmB,GAAI,qCAI1B,wBAAwB,4CAiE1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from "react";
4
+ import clsx from "clsx";
5
+ import { Icon } from "../../../components/icon";
6
+ import { RECENT_STICKER_TAB_ID } from "./constants";
7
+ const TAB_ROW_PADDING = 12;
8
+ const StickerCategoryTabs = ({ tabs, activeTabId, onSelectTab, }) => {
9
+ const rowRef = useRef(null);
10
+ const tabRefs = useRef(new Map());
11
+ // Scrolls only the tab row: scrollIntoView would also drag the host page
12
+ // when the picker sits partly off-screen.
13
+ useEffect(() => {
14
+ const row = rowRef.current;
15
+ const tab = activeTabId ? tabRefs.current.get(activeTabId) : undefined;
16
+ if (!row || !tab)
17
+ return;
18
+ const tabRight = tab.offsetLeft + tab.offsetWidth;
19
+ if (tab.offsetLeft < row.scrollLeft) {
20
+ row.scrollLeft = tab.offsetLeft - TAB_ROW_PADDING;
21
+ }
22
+ else if (tabRight > row.scrollLeft + row.clientWidth) {
23
+ row.scrollLeft = tabRight - row.clientWidth + TAB_ROW_PADDING;
24
+ }
25
+ }, [activeTabId]);
26
+ return (_jsx("div", { ref: rowRef, role: "tablist", "data-testid": "sticker-picker-tabs", className: "relative flex h-[52px] shrink-0 items-center gap-3 overflow-x-auto px-3 py-2 [scrollbar-width:none]", children: tabs.map((tab) => {
27
+ const isActive = tab.id === activeTabId;
28
+ return (_jsx("button", { ref: (element) => {
29
+ if (element)
30
+ tabRefs.current.set(tab.id, element);
31
+ else
32
+ tabRefs.current.delete(tab.id);
33
+ }, type: "button", role: "tab", "aria-selected": isActive, "aria-label": tab.name, "data-testid": "sticker-picker-tab", "data-tab-id": tab.id, onClick: () => onSelectTab(tab.id), className: clsx("flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border p-1", isActive
34
+ ? "border-[#505759] bg-[#393E40]/10"
35
+ : "border-transparent"), children: tab.id === RECENT_STICKER_TAB_ID ? (_jsx(Icon, { icon: "time-circle-b", size: 24, className: "text-[#393E40]" })) : tab.iconUrl ? (_jsx("img", { src: tab.iconUrl, alt: "", className: "h-7 w-7 object-contain" })) : (_jsx("span", { className: "h-7 w-7 rounded bg-gray-100" })) }, tab.id));
36
+ }) }));
37
+ };
38
+ export default StickerCategoryTabs;
@@ -0,0 +1,7 @@
1
+ import type { StickerItem } from "../../../types/sticker";
2
+ interface StickerPickerProps {
3
+ onSelectSticker: (sticker: StickerItem) => void;
4
+ }
5
+ declare const StickerPicker: ({ onSelectSticker }: StickerPickerProps) => import("react/jsx-runtime").JSX.Element;
6
+ export default StickerPicker;
7
+ //# sourceMappingURL=StickerPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StickerPicker.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/sticker-picker/StickerPicker.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,UAAU,kBAAkB;IAC1B,eAAe,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;CACjD;AAMD,QAAA,MAAM,aAAa,GAAI,qBAAqB,kBAAkB,4CA0J7D,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -0,0 +1,113 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { useStickerCategories, useStickerHistory, } from "../../../hooks/sticker/useStickers";
6
+ import StickerCategoryTabs from "./StickerCategoryTabs";
7
+ import StickerSection from "./StickerSection";
8
+ import { RECENT_STICKER_TAB_ID } from "./constants";
9
+ // Long enough for a smooth scrollTo to finish, so the scroll-spy doesn't
10
+ // flick the active tab through every section passed on the way.
11
+ const TAB_SCROLL_LOCK_MS = 600;
12
+ const StickerPicker = ({ onSelectSticker }) => {
13
+ const { t } = useTranslation("conversation-inbox");
14
+ const categoriesQuery = useStickerCategories();
15
+ const historyQuery = useStickerHistory();
16
+ const recentStickers = useMemo(() => { var _a; return (_a = historyQuery.data) !== null && _a !== void 0 ? _a : []; }, [historyQuery.data]);
17
+ const tabs = useMemo(() => {
18
+ var _a;
19
+ const categoryTabs = ((_a = categoriesQuery.data) !== null && _a !== void 0 ? _a : []).map((category) => {
20
+ var _a;
21
+ return ({
22
+ id: category.id,
23
+ name: category.name,
24
+ iconUrl: (_a = category.icon) === null || _a === void 0 ? void 0 : _a.fullUrl,
25
+ stickerCount: category.stickerCount,
26
+ });
27
+ });
28
+ if (recentStickers.length === 0)
29
+ return categoryTabs;
30
+ return [
31
+ {
32
+ id: RECENT_STICKER_TAB_ID,
33
+ name: t("thread_detail.sticker.recent"),
34
+ stickerCount: recentStickers.length,
35
+ },
36
+ ...categoryTabs,
37
+ ];
38
+ }, [categoriesQuery.data, recentStickers.length, t]);
39
+ // Wait for history too: a Recent tab arriving after the categories would
40
+ // shove every section down and flip the default tab under the user.
41
+ const isLoading = categoriesQuery.isLoading ||
42
+ (historyQuery.isLoading && !historyQuery.isError);
43
+ const [activeTabId, setActiveTabId] = useState(null);
44
+ const [scrollRoot, setScrollRoot] = useState(null);
45
+ const sectionRefs = useRef(new Map());
46
+ const isTabScrollLockedRef = useRef(false);
47
+ const lockTimerRef = useRef(null);
48
+ useEffect(() => {
49
+ var _a, _b;
50
+ if (isLoading)
51
+ return;
52
+ if (!activeTabId || !tabs.some((tab) => tab.id === activeTabId)) {
53
+ setActiveTabId((_b = (_a = tabs[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null);
54
+ }
55
+ }, [activeTabId, isLoading, tabs]);
56
+ useEffect(() => () => {
57
+ if (lockTimerRef.current)
58
+ clearTimeout(lockTimerRef.current);
59
+ }, []);
60
+ const registerSectionRef = useCallback((tabId, element) => {
61
+ if (element)
62
+ sectionRefs.current.set(tabId, element);
63
+ else
64
+ sectionRefs.current.delete(tabId);
65
+ }, []);
66
+ const handleScroll = useCallback(() => {
67
+ var _a, _b;
68
+ if (!scrollRoot || isTabScrollLockedRef.current)
69
+ return;
70
+ const { scrollTop, clientHeight, scrollHeight } = scrollRoot;
71
+ const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
72
+ let nextTabId = (_b = (_a = tabs[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
73
+ for (const tab of tabs) {
74
+ const section = sectionRefs.current.get(tab.id);
75
+ if (section && section.offsetTop <= scrollTop + 1)
76
+ nextTabId = tab.id;
77
+ }
78
+ if (isAtBottom && tabs.length > 0)
79
+ nextTabId = tabs[tabs.length - 1].id;
80
+ setActiveTabId(nextTabId);
81
+ }, [scrollRoot, tabs]);
82
+ const handleSelectTab = useCallback((tabId) => {
83
+ setActiveTabId(tabId);
84
+ const section = sectionRefs.current.get(tabId);
85
+ if (!scrollRoot || !section)
86
+ return;
87
+ isTabScrollLockedRef.current = true;
88
+ if (lockTimerRef.current)
89
+ clearTimeout(lockTimerRef.current);
90
+ lockTimerRef.current = setTimeout(() => {
91
+ isTabScrollLockedRef.current = false;
92
+ }, TAB_SCROLL_LOCK_MS);
93
+ if (typeof scrollRoot.scrollTo === "function") {
94
+ scrollRoot.scrollTo({ top: section.offsetTop, behavior: "smooth" });
95
+ }
96
+ else {
97
+ scrollRoot.scrollTop = section.offsetTop;
98
+ }
99
+ }, [scrollRoot]);
100
+ const renderBody = () => {
101
+ if (isLoading) {
102
+ return (_jsx("div", { className: "grid grid-cols-4 gap-2 px-3 py-3", children: Array.from({ length: 8 }, (_, index) => (_jsx("div", { "data-testid": "sticker-picker-skeleton", className: "aspect-square w-full animate-pulse rounded-lg bg-gray-100" }, index))) }));
103
+ }
104
+ if (categoriesQuery.isError || tabs.length === 0) {
105
+ return (_jsx("p", { className: "px-3 py-6 text-center text-sm text-gray-500", children: categoriesQuery.isError
106
+ ? t("thread_detail.sticker.load_failed")
107
+ : t("thread_detail.sticker.empty") }));
108
+ }
109
+ return tabs.map((tab) => (_jsx(StickerSection, { tab: tab, recentStickers: recentStickers, scrollRoot: scrollRoot, registerRef: registerSectionRef, onSelectSticker: onSelectSticker }, tab.id)));
110
+ };
111
+ return (_jsxs("div", { "data-testid": "sticker-picker", "data-cp-name": "StickerPicker", className: "flex h-[312px] flex-col bg-white", children: [!isLoading && tabs.length > 0 && (_jsx(StickerCategoryTabs, { tabs: tabs, activeTabId: activeTabId, onSelectTab: handleSelectTab })), _jsx("div", { ref: setScrollRoot, onScroll: handleScroll, "data-testid": "sticker-picker-scroll", className: "relative min-h-0 flex-1 overflow-y-auto", children: renderBody() })] }));
112
+ };
113
+ export default StickerPicker;
@@ -0,0 +1,12 @@
1
+ import type { StickerItem } from "../../../types/sticker";
2
+ import { type StickerTab } from "./constants";
3
+ interface StickerSectionProps {
4
+ tab: StickerTab;
5
+ recentStickers: StickerItem[];
6
+ scrollRoot: HTMLElement | null;
7
+ registerRef: (tabId: string, element: HTMLElement | null) => void;
8
+ onSelectSticker: (sticker: StickerItem) => void;
9
+ }
10
+ declare const StickerSection: ({ tab, recentStickers, scrollRoot, registerRef, onSelectSticker, }: StickerSectionProps) => import("react/jsx-runtime").JSX.Element;
11
+ export default StickerSection;
12
+ //# sourceMappingURL=StickerSection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StickerSection.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/sticker-picker/StickerSection.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAIrE,UAAU,mBAAmB;IAC3B,GAAG,EAAE,UAAU,CAAC;IAChB,cAAc,EAAE,WAAW,EAAE,CAAC;IAC9B,UAAU,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAClE,eAAe,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;CACjD;AAyCD,QAAA,MAAM,cAAc,GAAI,oEAMrB,mBAAmB,4CAiErB,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import clsx from "clsx";
5
+ import { images } from "../../../constants/images";
6
+ import { useStickersByCategory } from "../../../hooks/sticker/useStickers";
7
+ import { RECENT_STICKER_TAB_ID } from "./constants";
8
+ const PLACEHOLDER_CELL_COUNT = 4;
9
+ const StickerCell = ({ sticker, onSelect, }) => {
10
+ var _a;
11
+ const [isLoaded, setIsLoaded] = useState(false);
12
+ const [hasError, setHasError] = useState(false);
13
+ return (_jsx("button", { type: "button", "aria-label": sticker.name, "data-testid": "sticker-picker-item", onClick: () => onSelect(sticker), className: clsx("relative aspect-square w-full rounded-lg", !isLoaded && "animate-pulse bg-gray-100"), children: _jsx("img", { src: hasError ? images.stickerInvalid : (_a = sticker.file) === null || _a === void 0 ? void 0 : _a.fullUrl, alt: "", loading: "lazy", onLoad: () => setIsLoaded(true), onError: () => {
14
+ setHasError(true);
15
+ setIsLoaded(true);
16
+ }, className: clsx("h-full w-full object-contain", !isLoaded && "opacity-0") }) }));
17
+ };
18
+ const StickerSection = ({ tab, recentStickers, scrollRoot, registerRef, onSelectSticker, }) => {
19
+ const sectionRef = useRef(null);
20
+ const isRecent = tab.id === RECENT_STICKER_TAB_ID;
21
+ const [hasBeenVisible, setHasBeenVisible] = useState(() => typeof IntersectionObserver === "undefined");
22
+ // Categories load on first approach, like chat-mobile, so opening the
23
+ // picker doesn't fire one request per category up front.
24
+ useEffect(() => {
25
+ const element = sectionRef.current;
26
+ if (hasBeenVisible || isRecent || !element || !scrollRoot)
27
+ return;
28
+ const observer = new IntersectionObserver((entries) => {
29
+ if (entries.some((entry) => entry.isIntersecting)) {
30
+ setHasBeenVisible(true);
31
+ }
32
+ }, { root: scrollRoot, rootMargin: "200px 0px" });
33
+ observer.observe(element);
34
+ return () => observer.disconnect();
35
+ }, [hasBeenVisible, isRecent, scrollRoot]);
36
+ const { data: categoryStickers, isError } = useStickersByCategory(tab.id, !isRecent && hasBeenVisible);
37
+ const stickers = isRecent ? recentStickers : categoryStickers;
38
+ const placeholderCount = tab.stickerCount || PLACEHOLDER_CELL_COUNT;
39
+ return (_jsxs("div", { ref: (element) => {
40
+ sectionRef.current = element;
41
+ registerRef(tab.id, element);
42
+ }, "data-testid": "sticker-picker-section", "data-tab-id": tab.id, children: [_jsx("p", { className: "px-3 py-1 text-[14px] font-bold leading-[22px] text-[#747B7E]", children: tab.name }), isError ? null : (_jsx("div", { className: "grid grid-cols-4 gap-2 px-3 pb-3", children: stickers
43
+ ? stickers.map((sticker) => (_jsx(StickerCell, { sticker: sticker, onSelect: onSelectSticker }, sticker.id)))
44
+ : Array.from({ length: placeholderCount }, (_, index) => (_jsx("div", { "data-testid": "sticker-picker-skeleton", className: "aspect-square w-full animate-pulse rounded-lg bg-gray-100" }, index))) }))] }));
45
+ };
46
+ export default StickerSection;
@@ -0,0 +1,8 @@
1
+ export declare const RECENT_STICKER_TAB_ID = "recent";
2
+ export interface StickerTab {
3
+ id: string;
4
+ name: string;
5
+ iconUrl?: string;
6
+ stickerCount: number;
7
+ }
8
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/sticker-picker/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAE9C,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB"}
@@ -0,0 +1 @@
1
+ export const RECENT_STICKER_TAB_ID = "recent";
@@ -1 +1 @@
1
- {"version":3,"file":"StickerMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/message/item/StickerMessage.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG3D,UAAU,uBAAuB;IAC/B,OAAO,EAAE,WAAW,CAAC;CACtB;AAID,QAAA,MAAM,kBAAkB,GAAI,aAAa,uBAAuB,4CAyB/D,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"StickerMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/message/item/StickerMessage.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG3D,UAAU,uBAAuB;IAC/B,OAAO,EAAE,WAAW,CAAC;CACtB;AAID,QAAA,MAAM,kBAAkB,GAAI,aAAa,uBAAuB,4CAe/D,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -1,15 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import clsx from "clsx";
4
- import { Icon } from "../../../components/icon";
4
+ import { images } from "../../../constants/images";
5
5
  const STICKER_SIZE_CLASS = "h-[120px] w-[120px]";
6
6
  const StickerMessageItem = ({ message }) => {
7
7
  var _a;
8
8
  const [hasLoadError, setHasLoadError] = useState(false);
9
9
  const stickerUrl = (_a = message.stickerElem) === null || _a === void 0 ? void 0 : _a.content;
10
- if (!stickerUrl || hasLoadError) {
11
- return (_jsx("div", { className: clsx("flex items-center justify-center rounded-xl bg-black/5", STICKER_SIZE_CLASS), children: _jsx(Icon, { icon: "image-o", size: 32, className: "text-gray-400" }) }));
12
- }
13
- return (_jsx("img", { src: stickerUrl, alt: "", className: clsx("block object-cover", STICKER_SIZE_CLASS), onError: () => setHasLoadError(true) }));
10
+ const isInvalid = !stickerUrl || hasLoadError;
11
+ return (_jsx("img", { src: isInvalid ? images.stickerInvalid : stickerUrl, alt: "", "data-testid": "sticker-message-image", "data-invalid": isInvalid || undefined, className: clsx("block object-contain", STICKER_SIZE_CLASS), onError: () => setHasLoadError(true) }));
14
12
  };
15
13
  export default StickerMessageItem;
@@ -1 +1 @@
1
- {"version":3,"file":"ReplyPreviewMedia.d.ts","sourceRoot":"","sources":["../../../../src/components/message/reply/ReplyPreviewMedia.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,8BAA8B,CAAC;AAKxE,UAAU,sBAAsB;IAC9B,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,QAAA,MAAM,iBAAiB,GAAI,aAAa,sBAAsB,mDAoD7D,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"ReplyPreviewMedia.d.ts","sourceRoot":"","sources":["../../../../src/components/message/reply/ReplyPreviewMedia.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,8BAA8B,CAAC;AAKxE,UAAU,sBAAsB;IAC9B,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,QAAA,MAAM,iBAAiB,GAAI,aAAa,sBAAsB,mDAgE7D,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -4,7 +4,7 @@ import { documentIcon } from "../../../assets/svg";
4
4
  import { Icon } from "../../../components/icon";
5
5
  import { parseUrlMetadata } from "../../../utils/common";
6
6
  const ReplyPreviewMedia = ({ message }) => {
7
- var _a, _b, _c, _d, _e, _f;
7
+ var _a, _b, _c, _d, _e, _f, _g;
8
8
  if (message.contentType === MessageType.FileMessage) {
9
9
  return (_jsx("div", { className: "flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden [&>svg]:h-6 [&>svg]:w-6", children: documentIcon }));
10
10
  }
@@ -19,8 +19,14 @@ const ReplyPreviewMedia = ({ message }) => {
19
19
  const posterUrl = (_e = message.videoElem) === null || _e === void 0 ? void 0 : _e.snapshotUrl;
20
20
  return (_jsxs("div", { className: "relative h-[42px] w-[42px] shrink-0 overflow-hidden rounded-lg bg-gray-900", children: [posterUrl && (_jsx("img", { src: posterUrl, alt: "", className: "h-full w-full object-cover" })), _jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx("div", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-black/60", children: _jsx(Icon, { icon: "play-o", size: 12, className: "text-white" }) }) })] }));
21
21
  }
22
+ if (message.contentType === MessageType.StickerMessage) {
23
+ const stickerUrl = (_f = message.stickerElem) === null || _f === void 0 ? void 0 : _f.content;
24
+ if (!stickerUrl)
25
+ return null;
26
+ return (_jsx("img", { src: stickerUrl, alt: "", className: "h-[42px] w-[42px] shrink-0 object-contain" }));
27
+ }
22
28
  if (message.contentType === MessageType.UrlTextMessage) {
23
- const imageUrl = (_f = parseUrlMetadata(message.ex)) === null || _f === void 0 ? void 0 : _f.image;
29
+ const imageUrl = (_g = parseUrlMetadata(message.ex)) === null || _g === void 0 ? void 0 : _g.image;
24
30
  if (!imageUrl)
25
31
  return null;
26
32
  return (_jsx("img", { src: imageUrl, alt: "", className: "h-[42px] w-[42px] shrink-0 rounded-lg object-cover" }));
@@ -4,5 +4,6 @@ export declare const images: {
4
4
  emptyResources: string;
5
5
  muteNotificationWarning: string;
6
6
  searchEmpty: string;
7
+ stickerInvalid: string;
7
8
  };
8
9
  //# sourceMappingURL=images.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/constants/images.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,MAAM;;;;;;CAMlB,CAAC"}
1
+ {"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/constants/images.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC"}
@@ -4,10 +4,12 @@ const imageFailed = new URL("../assets/images/imageFailed.png", import.meta.url)
4
4
  const emptyResources = new URL("../assets/images/emptyResources.png", import.meta.url).href;
5
5
  const muteNotificationWarning = new URL("../assets/images/muteNotificationWarning.png", import.meta.url).href;
6
6
  const searchEmpty = new URL("../assets/images/searchEmpty.png", import.meta.url).href;
7
+ const stickerInvalid = new URL("../assets/images/stickerInvalid.png", import.meta.url).href;
7
8
  export const images = {
8
9
  conversationBg,
9
10
  imageFailed,
10
11
  emptyResources,
11
12
  muteNotificationWarning,
12
13
  searchEmpty,
14
+ stickerInvalid,
13
15
  };
@@ -0,0 +1,12 @@
1
+ import { type RefObject } from "react";
2
+ /** Marks the area where a tap closes the open composer panel (the thread timeline). */
3
+ export declare const COMPOSER_DISMISS_ZONE_ATTR = "data-composer-dismiss-zone";
4
+ interface UseOutsideTapDismissOptions {
5
+ enabled: boolean;
6
+ /** Taps inside this element never dismiss (the composer and its panels). */
7
+ insideRef: RefObject<HTMLElement | null>;
8
+ onDismiss: () => void;
9
+ }
10
+ export declare const useOutsideTapDismiss: ({ enabled, insideRef, onDismiss, }: UseOutsideTapDismissOptions) => void;
11
+ export {};
12
+ //# sourceMappingURL=useOutsideTapDismiss.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOutsideTapDismiss.d.ts","sourceRoot":"","sources":["../../../src/hooks/common/useOutsideTapDismiss.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE1D,uFAAuF;AACvF,eAAO,MAAM,0BAA0B,+BAA+B,CAAC;AAOvE,UAAU,2BAA2B;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,SAAS,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzC,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AA6BD,eAAO,MAAM,oBAAoB,GAAI,oCAIlC,2BAA2B,SAuD7B,CAAC"}
@@ -0,0 +1,81 @@
1
+ import { useEffect, useRef } from "react";
2
+ /** Marks the area where a tap closes the open composer panel (the thread timeline). */
3
+ export const COMPOSER_DISMISS_ZONE_ATTR = "data-composer-dismiss-zone";
4
+ // Past this much pointer travel the gesture is a scroll/drag, not a tap.
5
+ const TAP_MOVE_TOLERANCE_PX = 10;
6
+ // Upper bound for a click that never arrives (pointerup on another element).
7
+ const SWALLOW_FALLBACK_MS = 1000;
8
+ const isInDismissZone = (target, inside) => {
9
+ if (!(target instanceof Element))
10
+ return false;
11
+ if (inside === null || inside === void 0 ? void 0 : inside.contains(target))
12
+ return false;
13
+ return target.closest(`[${COMPOSER_DISMISS_ZONE_ATTR}]`) !== null;
14
+ };
15
+ // The dismissing tap's click must not hit what sits under it; the window ends at
16
+ // the next pointerdown, so a browser-delayed click is still swallowed.
17
+ const swallowNextClick = () => {
18
+ const stop = () => {
19
+ window.removeEventListener("click", swallow, { capture: true });
20
+ window.removeEventListener("pointerdown", stop, { capture: true });
21
+ clearTimeout(fallbackTimer);
22
+ };
23
+ const swallow = (event) => {
24
+ event.preventDefault();
25
+ event.stopImmediatePropagation();
26
+ stop();
27
+ };
28
+ window.addEventListener("click", swallow, { capture: true });
29
+ window.addEventListener("pointerdown", stop, { capture: true });
30
+ const fallbackTimer = setTimeout(stop, SWALLOW_FALLBACK_MS);
31
+ };
32
+ export const useOutsideTapDismiss = ({ enabled, insideRef, onDismiss, }) => {
33
+ const onDismissRef = useRef(onDismiss);
34
+ onDismissRef.current = onDismiss;
35
+ useEffect(() => {
36
+ if (!enabled)
37
+ return;
38
+ let tapStart = null;
39
+ const hasMoved = (event) => !!tapStart &&
40
+ Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y) >
41
+ TAP_MOVE_TOLERANCE_PX;
42
+ // A second pointer landing mid-tap (pinch, two-finger touch) makes this a
43
+ // multi-touch gesture, so the pending tap is dropped rather than resumed.
44
+ const handlePointerDown = (event) => {
45
+ const isPrimaryTap = event.isPrimary !== false && event.button === 0;
46
+ tapStart =
47
+ isPrimaryTap && isInDismissZone(event.target, insideRef.current)
48
+ ? { x: event.clientX, y: event.clientY, pointerId: event.pointerId }
49
+ : null;
50
+ };
51
+ const handlePointerMove = (event) => {
52
+ if ((tapStart === null || tapStart === void 0 ? void 0 : tapStart.pointerId) === event.pointerId && hasMoved(event)) {
53
+ tapStart = null;
54
+ }
55
+ };
56
+ const handlePointerUp = (event) => {
57
+ if (!tapStart || tapStart.pointerId !== event.pointerId)
58
+ return;
59
+ const isTap = !hasMoved(event) && isInDismissZone(event.target, insideRef.current);
60
+ tapStart = null;
61
+ if (!isTap)
62
+ return;
63
+ swallowNextClick();
64
+ onDismissRef.current();
65
+ };
66
+ // Touch scrolling cancels the pointer once the browser takes over the pan.
67
+ const handlePointerCancel = () => {
68
+ tapStart = null;
69
+ };
70
+ document.addEventListener("pointerdown", handlePointerDown, true);
71
+ document.addEventListener("pointermove", handlePointerMove, true);
72
+ document.addEventListener("pointerup", handlePointerUp, true);
73
+ document.addEventListener("pointercancel", handlePointerCancel, true);
74
+ return () => {
75
+ document.removeEventListener("pointerdown", handlePointerDown, true);
76
+ document.removeEventListener("pointermove", handlePointerMove, true);
77
+ document.removeEventListener("pointerup", handlePointerUp, true);
78
+ document.removeEventListener("pointercancel", handlePointerCancel, true);
79
+ };
80
+ }, [enabled, insideRef]);
81
+ };
@@ -3,6 +3,7 @@ import { type CustomMessagePayload } from "../../types/customMessage";
3
3
  import { BotFlowMeta, ExtendMessageInfo, FileMsgParamsByFile, ImageMsgParamsByFile, MentionOffset, MentionSendPayload, UrlMetadataInfo, VideoMsgParamsByFile } from "../../types/chat";
4
4
  import { UploadFile } from "antd";
5
5
  import { ISessionResponse } from "../../types/dto";
6
+ import type { StickerItem } from "../../types/sticker";
6
7
  export declare const isMediaResendable: (message: {
7
8
  clientMsgID: string;
8
9
  contentType: number;
@@ -15,6 +16,7 @@ export declare const createFileMessageByFile: (file: FileMsgParamsByFile) => Pro
15
16
  export declare const AT_ALL_TAG = "AtAllTag";
16
17
  export declare const createAtTextMessage: (params: AtMsgParams) => Promise<MessageItem | null>;
17
18
  export declare const createUrlTextMessage: (text: string, urls: string[]) => Promise<MessageItem | null>;
19
+ export declare const createStickerMessage: (stickerUrl: string) => Promise<MessageItem | null>;
18
20
  export declare const createCustomMessageByParams: (params: CustomMsgParams) => Promise<MessageItem | null>;
19
21
  export declare const createCustomMessage: (payload: CustomMessagePayload) => Promise<MessageItem | null>;
20
22
  export declare const createQuoteMessage: (text: string, quotedMessage: MessageItem, urls?: string[]) => Promise<MessageItem | null>;
@@ -37,13 +39,17 @@ export declare const useSendMessage: (options?: UseSendMessageOptions) => {
37
39
  sendCustomMessage: (payload: CustomMessagePayload, sendOptions?: {
38
40
  currentSession?: ISessionResponse;
39
41
  }) => Promise<boolean>;
42
+ sendStickerMessage: (sticker: StickerItem, sendOptions?: {
43
+ currentSession?: ISessionResponse;
44
+ }) => Promise<boolean>;
40
45
  resendMessage: (failedMessage: MessageItem) => Promise<void>;
41
46
  };
42
- export declare const generateExtendMessageInfo: ({ richText, urlMetadata, currentSession, flow, mentionOffsets, }: {
47
+ export declare const generateExtendMessageInfo: ({ richText, urlMetadata, currentSession, flow, mentionOffsets, stickerId, }: {
43
48
  richText?: string;
44
49
  urlMetadata?: UrlMetadataInfo;
45
50
  currentSession?: ISessionResponse;
46
51
  flow?: BotFlowMeta;
47
52
  mentionOffsets?: MentionOffset[];
53
+ stickerId?: string;
48
54
  }) => ExtendMessageInfo;
49
55
  //# sourceMappingURL=useSendMessage.d.ts.map