@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.
- package/dist/assets/images/stickerInvalid.png +0 -0
- package/dist/components/conversation-detail/ConversationDetailInput.d.ts +3 -1
- package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
- package/dist/components/conversation-detail/ConversationDetailInput.js +35 -5
- package/dist/components/conversation-detail/sticker-picker/StickerCategoryTabs.d.ts +9 -0
- package/dist/components/conversation-detail/sticker-picker/StickerCategoryTabs.d.ts.map +1 -0
- package/dist/components/conversation-detail/sticker-picker/StickerCategoryTabs.js +38 -0
- package/dist/components/conversation-detail/sticker-picker/StickerPicker.d.ts +7 -0
- package/dist/components/conversation-detail/sticker-picker/StickerPicker.d.ts.map +1 -0
- package/dist/components/conversation-detail/sticker-picker/StickerPicker.js +113 -0
- package/dist/components/conversation-detail/sticker-picker/StickerSection.d.ts +12 -0
- package/dist/components/conversation-detail/sticker-picker/StickerSection.d.ts.map +1 -0
- package/dist/components/conversation-detail/sticker-picker/StickerSection.js +46 -0
- package/dist/components/conversation-detail/sticker-picker/constants.d.ts +8 -0
- package/dist/components/conversation-detail/sticker-picker/constants.d.ts.map +1 -0
- package/dist/components/conversation-detail/sticker-picker/constants.js +1 -0
- package/dist/components/message/item/StickerMessage.d.ts.map +1 -1
- package/dist/components/message/item/StickerMessage.js +3 -5
- package/dist/components/message/reply/ReplyPreviewMedia.d.ts.map +1 -1
- package/dist/components/message/reply/ReplyPreviewMedia.js +8 -2
- package/dist/constants/images.d.ts +1 -0
- package/dist/constants/images.d.ts.map +1 -1
- package/dist/constants/images.js +2 -0
- package/dist/hooks/common/useOutsideTapDismiss.d.ts +12 -0
- package/dist/hooks/common/useOutsideTapDismiss.d.ts.map +1 -0
- package/dist/hooks/common/useOutsideTapDismiss.js +81 -0
- package/dist/hooks/message/useSendMessage.d.ts +7 -1
- package/dist/hooks/message/useSendMessage.d.ts.map +1 -1
- package/dist/hooks/message/useSendMessage.js +45 -2
- package/dist/hooks/sticker/useStickers.d.ts +5 -0
- package/dist/hooks/sticker/useStickers.d.ts.map +1 -0
- package/dist/hooks/sticker/useStickers.js +39 -0
- package/dist/locales/vi/conversation-inbox.json +6 -0
- package/dist/screens/conversation-detail/index.d.ts.map +1 -1
- package/dist/screens/conversation-detail/index.js +2 -1
- package/dist/screens/conversation-inbox/index.d.ts +2 -0
- package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
- package/dist/screens/conversation-inbox/index.js +2 -2
- package/dist/services/query.d.ts +3 -0
- package/dist/services/query.d.ts.map +1 -1
- package/dist/services/query.js +3 -0
- package/dist/services/routes.d.ts +3 -0
- package/dist/services/routes.d.ts.map +1 -1
- package/dist/services/routes.js +3 -0
- package/dist/styles/global.css +1 -1
- package/dist/types/chat.d.ts +1 -0
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/sticker.d.ts +20 -0
- package/dist/types/sticker.d.ts.map +1 -0
- package/dist/types/sticker.js +1 -0
- package/package.json +1 -1
|
Binary file
|
|
@@ -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":"
|
|
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 }))
|
|
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 @@
|
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
11
|
-
|
|
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,
|
|
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 = (
|
|
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" }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/constants/images.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/constants/images.ts"],"names":[],"mappings":"AAwBA,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC"}
|
package/dist/constants/images.js
CHANGED
|
@@ -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
|