@droppii-org/chat-sdk 0.7.17 → 0.7.20

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 (46) hide show
  1. package/dist/components/canned-response/CannedResponsePanel.d.ts.map +1 -1
  2. package/dist/components/canned-response/CannedResponsePanel.js +10 -3
  3. package/dist/components/canned-response/insertCannedMedia.d.ts +18 -0
  4. package/dist/components/canned-response/insertCannedMedia.d.ts.map +1 -0
  5. package/dist/components/canned-response/insertCannedMedia.js +101 -0
  6. package/dist/components/canned-response/useInsertCannedReply.d.ts +3 -0
  7. package/dist/components/canned-response/useInsertCannedReply.d.ts.map +1 -0
  8. package/dist/components/canned-response/useInsertCannedReply.js +33 -0
  9. package/dist/components/conversation/DeskBackgroundSyncStatus.d.ts +3 -1
  10. package/dist/components/conversation/DeskBackgroundSyncStatus.d.ts.map +1 -1
  11. package/dist/components/conversation/DeskBackgroundSyncStatus.js +3 -2
  12. package/dist/components/conversation/ThreadCard.d.ts +3 -1
  13. package/dist/components/conversation/ThreadCard.d.ts.map +1 -1
  14. package/dist/components/conversation/ThreadCard.js +3 -3
  15. package/dist/components/conversation-detail/ConversationDetailBootStatus.d.ts +4 -0
  16. package/dist/components/conversation-detail/ConversationDetailBootStatus.d.ts.map +1 -0
  17. package/dist/components/conversation-detail/ConversationDetailBootStatus.js +15 -0
  18. package/dist/components/message/footer/MediaActions.d.ts +3 -1
  19. package/dist/components/message/footer/MediaActions.d.ts.map +1 -1
  20. package/dist/components/message/footer/MediaActions.js +5 -38
  21. package/dist/components/message/footer/MediaUploadButtons.d.ts +13 -0
  22. package/dist/components/message/footer/MediaUploadButtons.d.ts.map +1 -0
  23. package/dist/components/message/footer/MediaUploadButtons.js +50 -0
  24. package/dist/components/message/footer/mergePickedUploadFiles.d.ts +9 -0
  25. package/dist/components/message/footer/mergePickedUploadFiles.d.ts.map +1 -0
  26. package/dist/components/message/footer/mergePickedUploadFiles.js +20 -0
  27. package/dist/components/rich-text-editor/RichTextEditor.d.ts +10 -1
  28. package/dist/components/rich-text-editor/RichTextEditor.d.ts.map +1 -1
  29. package/dist/components/rich-text-editor/RichTextEditor.js +9 -9
  30. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  31. package/dist/hooks/global/useGlobalEvent.js +4 -2
  32. package/dist/index.d.ts +5 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -0
  35. package/dist/locales/vi/common.json +5 -1
  36. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  37. package/dist/screens/conversation-detail/index.js +4 -1
  38. package/dist/styles/global.css +1 -1
  39. package/dist/types/chat.d.ts +6 -1
  40. package/dist/types/chat.d.ts.map +1 -1
  41. package/dist/types/dto.d.ts +14 -0
  42. package/dist/types/dto.d.ts.map +1 -1
  43. package/dist/utils/mediaUrl.d.ts +3 -0
  44. package/dist/utils/mediaUrl.d.ts.map +1 -0
  45. package/dist/utils/mediaUrl.js +3 -0
  46. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CannedResponsePanel.d.ts","sourceRoot":"","sources":["../../../src/components/canned-response/CannedResponsePanel.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAE,mBAAmB,EAA8B,MAAM,cAAc,CAAC;AAQ/E,UAAU,wBAAwB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACnD;AAaD,QAAA,MAAM,mBAAmB,GAAI,oCAI1B,wBAAwB,4CAmJ1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"CannedResponsePanel.d.ts","sourceRoot":"","sources":["../../../src/components/canned-response/CannedResponsePanel.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAE,mBAAmB,EAA8B,MAAM,cAAc,CAAC;AAQ/E,UAAU,wBAAwB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACnD;AAaD,QAAA,MAAM,mBAAmB,GAAI,oCAI1B,wBAAwB,4CAwJ1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -12,7 +12,7 @@ import { useMessageFooterContext } from "../../components/message/footer";
12
12
  import { CannedResponseVisibleScope } from "../../types/chat";
13
13
  import { sanitizeHtml } from "../../utils/common";
14
14
  import emitter from "../../utils/events";
15
- import { insertCannedContent } from "./insertCannedContent";
15
+ import { useInsertCannedReply } from "./useInsertCannedReply";
16
16
  import { buildCannedScopes } from "./cannedScopes";
17
17
  import CannedScopeChips, { ALL_SCOPES_KEY } from "./CannedScopeChips";
18
18
  import CannedResponseRow from "./CannedResponseRow";
@@ -35,7 +35,14 @@ const CannedResponsePanel = ({ query, viewedTeamId, onCreate, }) => {
35
35
  const activeScopes = useMemo(() => (filter === ALL_SCOPES_KEY ? scopes : scopes.filter((s) => s.key === filter)), [scopes, filter]);
36
36
  const { rows, isLoading, isError } = useSearchCannedAcrossScopes(activeScopes, debouncedQuery);
37
37
  const settling = query !== debouncedQuery;
38
- const insertRow = useCallback((row) => insertCannedContent(editor, sanitizeHtml(row.content || "")), [editor]);
38
+ const insertReply = useInsertCannedReply();
39
+ const insertRow = useCallback((row) => {
40
+ var _a, _b;
41
+ return insertReply(sanitizeHtml(row.content || ""), {
42
+ images: (_a = row.images) !== null && _a !== void 0 ? _a : [],
43
+ files: (_b = row.files) !== null && _b !== void 0 ? _b : [],
44
+ });
45
+ }, [insertReply]);
39
46
  useEffect(() => {
40
47
  const onSelect = (index) => {
41
48
  const row = index === null ? undefined : rows[Math.min(index, rows.length - 1)];
@@ -60,7 +67,7 @@ const CannedResponsePanel = ({ query, viewedTeamId, onCreate, }) => {
60
67
  onCreate === null || onCreate === void 0 ? void 0 : onCreate({
61
68
  visibilityScope: isPersonal ? "PRIVATE" : "TEAM",
62
69
  teamId: isPersonal ? undefined : (_a = scope === null || scope === void 0 ? void 0 : scope.teamId) !== null && _a !== void 0 ? _a : viewedTeamId,
63
- onCreated: (content) => editor.focus(() => insertCannedContent(editor, sanitizeHtml(content))),
70
+ onCreated: (content, media) => editor.focus(() => insertReply(sanitizeHtml(content), media)),
64
71
  });
65
72
  };
66
73
  const noQuery = query.length === 0;
@@ -0,0 +1,18 @@
1
+ import { type UploadFile } from "antd";
2
+ import type { TFunction } from "i18next";
3
+ import type { CannedMediaPayload } from "../../types/chat";
4
+ import type { ICannedMedia } from "../../types/dto";
5
+ export declare const cannedMediaToFile: (m: ICannedMedia) => Promise<File>;
6
+ export declare const mergeCannedUploads: (prev: UploadFile[], incoming: UploadFile[]) => {
7
+ list: UploadFile[];
8
+ skippedDoc: boolean;
9
+ };
10
+ interface InsertCannedMediaParams {
11
+ media?: CannedMediaPayload;
12
+ current: UploadFile[];
13
+ setFiles: (updater: (prev: UploadFile[]) => UploadFile[]) => void;
14
+ t: TFunction;
15
+ }
16
+ export declare const insertCannedMedia: ({ media, current, setFiles, t, }: InsertCannedMediaParams) => Promise<void>;
17
+ export {};
18
+ //# sourceMappingURL=insertCannedMedia.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insertCannedMedia.d.ts","sourceRoot":"","sources":["../../../src/components/canned-response/insertCannedMedia.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAchD,eAAO,MAAM,iBAAiB,GAAU,GAAG,YAAY,KAAG,OAAO,CAAC,IAAI,CAuBrE,CAAC;AAGF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,UAAU,EAAE,EAClB,UAAU,UAAU,EAAE,KACrB;IAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAa3C,CAAC;AAEF,UAAU,uBAAuB;IAC/B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,IAAI,CAAC;IAClE,CAAC,EAAE,SAAS,CAAC;CACd;AAED,eAAO,MAAM,iBAAiB,GAAU,kCAKrC,uBAAuB,kBAkDzB,CAAC"}
@@ -0,0 +1,101 @@
1
+ import { message as antdMessage } from "antd";
2
+ import { basename, isSafeMediaUrl } from "../../utils/mediaUrl";
3
+ import { createUploadFile, DOCUMENT_MIME_TYPES, MAX_DOCUMENT_SIZE_MB, MAX_IMAGE_SIZE_MB, } from "../../utils/fileValidation";
4
+ const DOWNLOAD_TIMEOUT_MS = 30000;
5
+ const isDocument = (type) => DOCUMENT_MIME_TYPES.includes(type || "");
6
+ // Downloads a saved canned attachment into a File so the normal send pipeline can upload it.
7
+ export const cannedMediaToFile = async (m) => {
8
+ var _a, _b;
9
+ const maxMb = isDocument(m.contentType) ? MAX_DOCUMENT_SIZE_MB : MAX_IMAGE_SIZE_MB;
10
+ const tooBig = (bytes) => !!bytes && bytes / 1024 / 1024 > maxMb;
11
+ if (tooBig(m.fileSize)) {
12
+ throw new Error(`Canned media ${m.fullUrl} exceeds ${maxMb}MB`);
13
+ }
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
16
+ let blob;
17
+ try {
18
+ const res = await fetch(m.fullUrl, { signal: controller.signal });
19
+ if (!res.ok)
20
+ throw new Error(`Fetch ${m.fullUrl} failed: ${res.status}`);
21
+ blob = await res.blob();
22
+ }
23
+ finally {
24
+ clearTimeout(timer);
25
+ }
26
+ // fileSize is optional on saved media, so re-check what was actually downloaded.
27
+ if (tooBig(blob.size)) {
28
+ throw new Error(`Canned media ${m.fullUrl} exceeds ${maxMb}MB`);
29
+ }
30
+ return new File([blob], (_b = (_a = m.fileName) !== null && _a !== void 0 ? _a : m.name) !== null && _b !== void 0 ? _b : basename(m.streamId), {
31
+ type: m.contentType || blob.type,
32
+ });
33
+ };
34
+ // Appends incoming uploads; keeps at most one document and never replaces the agent's own.
35
+ export const mergeCannedUploads = (prev, incoming) => {
36
+ let hasDoc = prev.some((f) => isDocument(f.type));
37
+ let skippedDoc = false;
38
+ const accepted = incoming.filter((f) => {
39
+ if (!isDocument(f.type))
40
+ return true;
41
+ if (hasDoc) {
42
+ skippedDoc = true;
43
+ return false;
44
+ }
45
+ hasDoc = true;
46
+ return true;
47
+ });
48
+ return { list: [...prev, ...accepted], skippedDoc };
49
+ };
50
+ export const insertCannedMedia = async ({ media, current, setFiles, t, }) => {
51
+ var _a, _b;
52
+ const loadingKey = `canned-media-loading-${Date.now()}-${Math.random()}`;
53
+ try {
54
+ const items = [...((_a = media === null || media === void 0 ? void 0 : media.images) !== null && _a !== void 0 ? _a : []), ...((_b = media === null || media === void 0 ? void 0 : media.files) !== null && _b !== void 0 ? _b : [])].filter((m) => m && isSafeMediaUrl(m.fullUrl));
55
+ if (!items.length)
56
+ return;
57
+ // Skip surplus docs before downloading; the updater re-checks against the live list.
58
+ let hasDoc = current.some((f) => isDocument(f.type));
59
+ let skippedDoc = false;
60
+ const toFetch = items.filter((m) => {
61
+ if (!isDocument(m.contentType))
62
+ return true;
63
+ if (hasDoc) {
64
+ skippedDoc = true;
65
+ return false;
66
+ }
67
+ hasDoc = true;
68
+ return true;
69
+ });
70
+ if (skippedDoc)
71
+ antdMessage.warning(t("canned_media_doc_limit"));
72
+ if (!toFetch.length)
73
+ return;
74
+ antdMessage.loading({ content: t("canned_media_loading"), key: loadingKey, duration: 0 });
75
+ const results = await Promise.allSettled(toFetch.map(cannedMediaToFile));
76
+ const uploads = results.flatMap((r) => r.status === "fulfilled" ? [createUploadFile(r.value)] : []);
77
+ if (uploads.length) {
78
+ let warned = false;
79
+ setFiles((prev) => {
80
+ const merged = mergeCannedUploads(prev, uploads);
81
+ // The agent may have attached a document while downloading; updater can run twice.
82
+ if (merged.skippedDoc && !warned) {
83
+ warned = true;
84
+ antdMessage.warning(t("canned_media_doc_limit"));
85
+ }
86
+ return merged.list;
87
+ });
88
+ }
89
+ if (uploads.length < results.length) {
90
+ results.forEach((r) => r.status === "rejected" && console.error(r.reason));
91
+ antdMessage.error(t("canned_media_load_failed"));
92
+ }
93
+ }
94
+ catch (error) {
95
+ console.error("Failed to insert canned media:", error);
96
+ antdMessage.error(t("canned_media_load_failed"));
97
+ }
98
+ finally {
99
+ antdMessage.destroy(loadingKey);
100
+ }
101
+ };
@@ -0,0 +1,3 @@
1
+ import type { CannedMediaPayload } from "../../types/chat";
2
+ export declare const useInsertCannedReply: () => (content: string, media?: CannedMediaPayload) => void;
3
+ //# sourceMappingURL=useInsertCannedReply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useInsertCannedReply.d.ts","sourceRoot":"","sources":["../../../src/components/canned-response/useInsertCannedReply.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAKvD,eAAO,MAAM,oBAAoB,kBAOnB,MAAM,UAAU,kBAAkB,SAqB/C,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { useCallback } from "react";
2
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
3
+ import { useLatest } from "ahooks";
4
+ import { useTranslation } from "react-i18next";
5
+ import { useMessageFooterContext } from "../../components/message/footer";
6
+ import useConversationStore from "../../store/conversation";
7
+ import { insertCannedContent } from "./insertCannedContent";
8
+ import { insertCannedMedia } from "./insertCannedMedia";
9
+ // Inserts the reply text now, then appends its images/files to the composer as they download.
10
+ export const useInsertCannedReply = () => {
11
+ const { t } = useTranslation();
12
+ const [editor] = useLexicalComposerContext();
13
+ const { listUploadFiles, setListUploadFiles } = useMessageFooterContext();
14
+ const filesRef = useLatest(listUploadFiles);
15
+ return useCallback((content, media) => {
16
+ var _a, _b;
17
+ insertCannedContent(editor, content);
18
+ // The footer is not remounted on conversation switch, so drop late downloads for another thread.
19
+ const conversationId = (_a = useConversationStore.getState().conversationData) === null || _a === void 0 ? void 0 : _a.conversationID;
20
+ void insertCannedMedia({
21
+ media,
22
+ current: (_b = filesRef.current) !== null && _b !== void 0 ? _b : [],
23
+ setFiles: (updater) => {
24
+ var _a;
25
+ if (((_a = useConversationStore.getState().conversationData) === null || _a === void 0 ? void 0 : _a.conversationID) !==
26
+ conversationId)
27
+ return;
28
+ setListUploadFiles(updater);
29
+ },
30
+ t,
31
+ });
32
+ }, [editor, filesRef, setListUploadFiles, t]);
33
+ };
@@ -1,4 +1,6 @@
1
1
  /** Under the list header while a fresh device's first sync keeps pulling the other conversations. */
2
- declare const DeskBackgroundSyncStatus: () => import("react/jsx-runtime").JSX.Element | null;
2
+ declare const DeskBackgroundSyncStatus: ({ className }: {
3
+ className?: string;
4
+ }) => import("react/jsx-runtime").JSX.Element | null;
3
5
  export default DeskBackgroundSyncStatus;
4
6
  //# sourceMappingURL=DeskBackgroundSyncStatus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DeskBackgroundSyncStatus.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskBackgroundSyncStatus.tsx"],"names":[],"mappings":"AAQA,qGAAqG;AACrG,QAAA,MAAM,wBAAwB,sDA+B7B,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
1
+ {"version":3,"file":"DeskBackgroundSyncStatus.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskBackgroundSyncStatus.tsx"],"names":[],"mappings":"AASA,qGAAqG;AACrG,QAAA,MAAM,wBAAwB,GAAI,eAAe;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,mDA+BtE,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import clsx from "clsx";
3
4
  import { useTranslation } from "react-i18next";
4
5
  import BootProgressBar from "../../components/boot/BootProgressBar";
5
6
  import useBootStore, { CONVERSATIONS_SYNCED_PROGRESS, selectIsSyncingInBackground, } from "../../store/boot";
6
7
  /** Under the list header while a fresh device's first sync keeps pulling the other conversations. */
7
- const DeskBackgroundSyncStatus = () => {
8
+ const DeskBackgroundSyncStatus = ({ className }) => {
8
9
  const { t } = useTranslation("boot");
9
10
  const isSyncing = useBootStore(selectIsSyncingInBackground);
10
11
  const progress = useBootStore((state) => state.syncProgress);
@@ -12,6 +13,6 @@ const DeskBackgroundSyncStatus = () => {
12
13
  return null;
13
14
  // Core reports exactly 10 when the conversations are stored; only later values say much.
14
15
  const showPercent = progress > CONVERSATIONS_SYNCED_PROGRESS;
15
- return (_jsxs("div", { "data-cp-name": "DeskBackgroundSyncStatus", className: "flex flex-col gap-1.5 border-b border-gray-25 px-4 py-2", children: [_jsxs("p", { role: "status", className: "m-0 flex items-center gap-2 text-xs text-gray-600", children: [_jsx("span", { "aria-hidden": "true", className: "h-3 w-3 shrink-0 rounded-full border-2 border-primary-25 border-t-primary-400 motion-safe:animate-spin" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: t("desk.background_sync") }), showPercent && (_jsxs("span", { className: "font-semibold tabular-nums text-primary-400", children: [progress, "%"] }))] }), _jsx(BootProgressBar, { mode: showPercent ? "progress" : "indeterminate", value: progress, label: t("sync_progress_label") })] }));
16
+ return (_jsxs("div", { "data-cp-name": "DeskBackgroundSyncStatus", className: clsx("flex flex-col gap-1.5 border-b border-gray-25 px-4 py-2", className), children: [_jsxs("p", { role: "status", className: "m-0 flex items-center gap-2 text-xs text-gray-600", children: [_jsx("span", { "aria-hidden": "true", className: "h-3 w-3 shrink-0 rounded-full border-2 border-primary-25 border-t-primary-400 motion-safe:animate-spin" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: t("desk.background_sync") }), showPercent && (_jsxs("span", { className: "font-semibold tabular-nums text-primary-400", children: [progress, "%"] }))] }), _jsx(BootProgressBar, { mode: showPercent ? "progress" : "indeterminate", value: progress, label: t("sync_progress_label") })] }));
16
17
  };
17
18
  export default DeskBackgroundSyncStatus;
@@ -11,7 +11,9 @@ export interface ThreadCardProps {
11
11
  muteIndicator?: ReactNode;
12
12
  mentionIndicator?: ReactNode;
13
13
  pinIndicator?: ReactNode;
14
+ /** Draws placeholder bars for the preview and time while the host still loads them. */
15
+ isPreviewLoading?: boolean;
14
16
  }
15
- declare const ThreadCard: ({ avatar, title, preview, time, unreadCount, isSelected, onClick, testId, muteIndicator, mentionIndicator, pinIndicator, }: ThreadCardProps) => import("react/jsx-runtime").JSX.Element;
17
+ declare const ThreadCard: ({ avatar, title, preview, time, unreadCount, isSelected, onClick, testId, muteIndicator, mentionIndicator, pinIndicator, isPreviewLoading, }: ThreadCardProps) => import("react/jsx-runtime").JSX.Element;
16
18
  export default ThreadCard;
17
19
  //# sourceMappingURL=ThreadCard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadCard.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/ThreadCard.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAqBvC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC;CAC1B;AAED,QAAA,MAAM,UAAU,GAAI,4HAYjB,eAAe,4CAgDjB,CAAC;AAEF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"ThreadCard.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/ThreadCard.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAqBvC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,QAAA,MAAM,UAAU,GAAI,8IAajB,eAAe,4CA8DjB,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -7,10 +7,10 @@ const UnreadBadge = ({ count }) => {
7
7
  const isWide = label.length > 1;
8
8
  return (_jsx("span", { className: clsx("inline-flex h-5 shrink-0 items-center justify-center rounded-[32px] border-2 border-white bg-gradient-danger-light px-[3px] text-center text-[12px] font-semibold leading-[160%] text-white", isWide ? "min-w-8" : "min-w-5"), "data-cp-name": "UnreadBadge", children: label }));
9
9
  };
10
- const ThreadCard = ({ avatar, title, preview, time, unreadCount = 0, isSelected = false, onClick, testId, muteIndicator, mentionIndicator, pinIndicator, }) => {
10
+ const ThreadCard = ({ avatar, title, preview, time, unreadCount = 0, isSelected = false, onClick, testId, muteIndicator, mentionIndicator, pinIndicator, isPreviewLoading = false, }) => {
11
11
  const hasUnread = unreadCount > 0;
12
- return (_jsxs("button", { type: "button", onClick: onClick, "data-testid": testId, "data-cp-name": "ThreadCard", className: clsx("relative flex w-full shrink-0 items-start gap-2 border-b border-gray-25 p-3 text-left transition-colors hover:bg-[#f5f5f5] focus:outline-none focus-visible:ring-2 focus-visible:ring-chat-ring", isSelected ? "bg-blue-50" : "bg-white"), children: [isSelected && (_jsx("span", { className: "absolute bottom-0 left-0 top-0 w-1 bg-blue-500" })), avatar, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex min-h-[25.6px] items-center justify-between gap-1", children: [_jsx("div", { className: "min-w-0 flex-1", children: title }), muteIndicator, time && (_jsx("span", { className: "shrink-0 text-[12px] font-normal leading-[160%] text-gray-400 [font-feature-settings:'liga'_off,'clig'_off]", children: time }))] }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: clsx("min-w-0 flex-1 truncate text-[14px] leading-[160%] [font-feature-settings:'liga'_off,'clig'_off]", hasUnread
12
+ return (_jsxs("button", { type: "button", onClick: onClick, "data-testid": testId, "data-cp-name": "ThreadCard", className: clsx("relative flex w-full shrink-0 items-start gap-2 border-b border-gray-25 p-3 text-left transition-colors hover:bg-[#f5f5f5] focus:outline-none focus-visible:ring-2 focus-visible:ring-chat-ring", isSelected ? "bg-blue-50" : "bg-white"), children: [isSelected && (_jsx("span", { className: "absolute bottom-0 left-0 top-0 w-1 bg-blue-500" })), avatar, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex min-h-[25.6px] items-center justify-between gap-1", children: [_jsx("div", { className: "min-w-0 flex-1", children: title }), muteIndicator, time ? (_jsx("span", { className: "shrink-0 text-[12px] font-normal leading-[160%] text-gray-400 [font-feature-settings:'liga'_off,'clig'_off]", children: time })) : (isPreviewLoading && (_jsx("span", { "aria-hidden": "true", className: "h-3 w-9 shrink-0 rounded bg-gray-25 motion-safe:animate-pulse" })))] }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [isPreviewLoading && !preview ? (_jsx("span", { "aria-hidden": "true", "data-cp-name": "ThreadCardPreviewSkeleton", className: "flex h-[22.4px] min-w-0 flex-1 items-center", children: _jsx("span", { className: "h-3 w-4/5 rounded bg-gray-25 motion-safe:animate-pulse" }) })) : (_jsx("p", { className: clsx("min-w-0 flex-1 truncate text-[14px] leading-[160%] [font-feature-settings:'liga'_off,'clig'_off]", hasUnread
13
13
  ? "font-[510] text-black"
14
- : "font-normal tracking-[0.14px] text-gray-600"), children: preview }), mentionIndicator, pinIndicator, hasUnread && _jsx(UnreadBadge, { count: unreadCount })] })] })] }));
14
+ : "font-normal tracking-[0.14px] text-gray-600"), children: preview })), mentionIndicator, pinIndicator, hasUnread && _jsx(UnreadBadge, { count: unreadCount })] })] })] }));
15
15
  };
16
16
  export default ThreadCard;
@@ -0,0 +1,4 @@
1
+ /** A thread opened while the chat engine starts: name the boot step instead of a bare spinner. */
2
+ declare const ConversationDetailBootStatus: () => import("react/jsx-runtime").JSX.Element;
3
+ export default ConversationDetailBootStatus;
4
+ //# sourceMappingURL=ConversationDetailBootStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationDetailBootStatus.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailBootStatus.tsx"],"names":[],"mappings":"AAQA,kGAAkG;AAClG,QAAA,MAAM,4BAA4B,+CAKjC,CAAC;AAEF,eAAe,4BAA4B,CAAC"}
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Spin } from "antd";
4
+ import InboxBootStatus from "../../components/conversation-inbox/InboxBootStatus";
5
+ import { useChatBootPhase } from "../../hooks/init/useChatBootPhase";
6
+ const DETAIL_SLOW_AFTER_MS = 8000;
7
+ /** A thread opened while the chat engine starts: name the boot step instead of a bare spinner. */
8
+ const ConversationDetailBootStatus = () => {
9
+ const boot = useChatBootPhase({ slowAfterMs: DETAIL_SLOW_AFTER_MS });
10
+ // Booted already (a later reconnect sync): only the thread is loading.
11
+ if (boot.phase === "ready")
12
+ return _jsx(Spin, {});
13
+ return _jsx(InboxBootStatus, { boot: boot });
14
+ };
15
+ export default ConversationDetailBootStatus;
@@ -6,7 +6,9 @@ interface MediaActionsProps {
6
6
  showMediaUpload?: boolean;
7
7
  crmComposer?: boolean;
8
8
  hideEmojiOnPhone?: boolean;
9
+ acceptVideo?: boolean;
10
+ singleDocument?: boolean;
9
11
  }
10
- export default function MediaActions({ listUploadFiles, onFilesChange, buttonClassName, showMediaUpload, crmComposer, hideEmojiOnPhone, }: MediaActionsProps): import("react/jsx-runtime").JSX.Element;
12
+ export default function MediaActions({ listUploadFiles, onFilesChange, buttonClassName, showMediaUpload, crmComposer, hideEmojiOnPhone, acceptVideo, singleDocument, }: MediaActionsProps): import("react/jsx-runtime").JSX.Element;
11
13
  export {};
12
14
  //# sourceMappingURL=MediaActions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MediaActions.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaActions.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAkB,UAAU,EAA0B,MAAM,MAAM,CAAC;AAuD1E,UAAU,iBAAiB;IACzB,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,eAAe,EACf,aAAa,EACb,eAAe,EACf,eAAsB,EACtB,WAAmB,EACnB,gBAAwB,GACzB,EAAE,iBAAiB,2CA+LnB"}
1
+ {"version":3,"file":"MediaActions.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaActions.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;AA8B1C,UAAU,iBAAiB;IACzB,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,eAAe,EACf,aAAa,EACb,eAAe,EACf,eAAsB,EACtB,WAAmB,EACnB,gBAAwB,EACxB,WAAkB,EAClB,cAAsB,GACvB,EAAE,iBAAiB,2CA6HnB"}
@@ -1,24 +1,21 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
5
5
  import { $getSelection, $isRangeSelection } from "lexical";
6
- import { Button, Upload, message as antdMessage } from "antd";
6
+ import { Button } from "antd";
7
7
  import clsx from "clsx";
8
8
  import { useTranslation } from "react-i18next";
9
9
  import EmojiPicker from "./EmojiPicker";
10
10
  import { useEscapeLayer } from "../../../hooks/common/useEscapeLayer";
11
11
  import { useCloseOnSidePanelOpen } from "../../../hooks/common/useCloseOnSidePanelOpen";
12
- import { Icon } from "../../../components/icon";
13
- import { createUploadFile, validateFile, } from "../../../utils/fileValidation";
14
- import { DOCUMENT_TYPES } from "./editorConfig";
12
+ import MediaUploadButtons from "./MediaUploadButtons";
15
13
  import CannedResponseIcon from "../../../assets/svg/canned-response";
16
14
  import { useMessageFooterContext } from ".";
17
15
  import useAuthStore from "../../../store/auth";
18
16
  import { StrokeIcon } from "../../../components/icon/StrokeIcon";
19
17
  const EmojiIcon = (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M7.59997 12.4C7.59997 12.4 8.49997 13.2 9.99997 13.2C11.5 13.2 12.4 12.4 12.4 12.4M14 8.39997C14 8.8418 13.6418 9.19997 13.2 9.19997C12.7581 9.19997 12.4 8.8418 12.4 8.39997C12.4 7.95814 12.7581 7.59997 13.2 7.59997C13.6418 7.59997 14 7.95814 14 8.39997ZM18 9.99997C18 14.4182 14.4182 18 9.99997 18C5.58169 18 1.99997 14.4182 1.99997 9.99997C1.99997 5.58169 5.58169 1.99997 9.99997 1.99997C14.4182 1.99997 18 5.58169 18 9.99997ZM7.59997 8.39997C7.59997 8.8418 7.2418 9.19997 6.79997 9.19997C6.35814 9.19997 5.99997 8.8418 5.99997 8.39997C5.99997 7.95814 6.35814 7.59997 6.79997 7.59997C7.2418 7.59997 7.59997 7.95814 7.59997 8.39997Z", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
20
- const AttachIcon = (_jsx("svg", { width: "22", height: "22", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M15.6 10.0001V11.2001C15.6 14.5137 12.9137 17.2001 9.59998 17.2001C6.28626 17.2001 3.59998 14.5137 3.59998 11.2001V6.79999C3.59998 4.59085 5.39084 2.79999 7.59998 2.79999C9.8091 2.79999 11.6 4.59085 11.6 6.79999V11.2C11.6 12.3045 10.7045 13.2 9.59998 13.2C8.49542 13.2 7.59998 12.3045 7.59998 11.2V7.99999", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
21
- export default function MediaActions({ listUploadFiles, onFilesChange, buttonClassName, showMediaUpload = true, crmComposer = false, hideEmojiOnPhone = false, }) {
18
+ export default function MediaActions({ listUploadFiles, onFilesChange, buttonClassName, showMediaUpload = true, crmComposer = false, hideEmojiOnPhone = false, acceptVideo = true, singleDocument = false, }) {
22
19
  const [editor] = useLexicalComposerContext();
23
20
  const { t } = useTranslation();
24
21
  const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@@ -40,36 +37,6 @@ export default function MediaActions({ listUploadFiles, onFilesChange, buttonCla
40
37
  });
41
38
  setShowEmojiPicker(false);
42
39
  }, [editor]);
43
- const beforeUploadImagesAndVideo = (file) => {
44
- const validation = validateFile(file, t);
45
- if (!validation.isValid) {
46
- antdMessage.error(validation.error);
47
- return Upload.LIST_IGNORE;
48
- }
49
- return false;
50
- };
51
- const beforeUploadFile = (file) => {
52
- const isAllowed = DOCUMENT_TYPES.includes(file.type);
53
- if (!isAllowed) {
54
- antdMessage.error(t("invalid_document_format", { fileName: file.name }));
55
- return Upload.LIST_IGNORE;
56
- }
57
- return false;
58
- };
59
- const handleChange = (info) => {
60
- let newList = [...info.fileList];
61
- const lastFile = info.file;
62
- if (DOCUMENT_TYPES.includes(lastFile.type || "")) {
63
- newList = newList.filter((f) => !DOCUMENT_TYPES.includes(f.type || ""));
64
- newList.push(lastFile);
65
- }
66
- const normalizedList = newList.map((file) => {
67
- var _a;
68
- const originFile = ((_a = file.originFileObj) !== null && _a !== void 0 ? _a : file);
69
- return Object.assign(Object.assign({}, createUploadFile(originFile)), { uid: file.uid });
70
- });
71
- onFilesChange(normalizedList);
72
- };
73
40
  const onPressCanned = useCallback(async () => {
74
41
  if (!isOpenCanned) {
75
42
  editor.focus();
@@ -105,5 +72,5 @@ export default function MediaActions({ listUploadFiles, onFilesChange, buttonCla
105
72
  return (_jsxs("div", { "data-cp-name": "MediaActions", className: clsx("flex items-center relative", crmComposer ? "gap-0.5" : "gap-1"), ref: containerRef, children: [isCrm && (_jsx(Button, Object.assign({ type: "text", shape: "default", className: clsx(btnClass, isOpenCanned && activeCls), onClick: onPressCanned }, crmLabel("composer_quick_reply"), { children: crmComposer ? (_jsx(StrokeIcon, { name: "hash", size: 18 })) : (_jsx(CannedResponseIcon, { className: clsx("hover:text-gray-700", isOpenCanned && "text-blue-600") })) }))), _jsx(Button, Object.assign({ type: "text", shape: "default", className: clsx(btnClass, hideEmojiOnPhone && "phone:!hidden", showEmojiPicker &&
106
73
  (crmComposer
107
74
  ? activeCls
108
- : "bg-[var(--dc-color-primary-light)] text-[var(--dc-color-primary)]")), onClick: () => setShowEmojiPicker(!showEmojiPicker) }, crmLabel("composer_emoji"), { children: crmComposer ? _jsx(StrokeIcon, { name: "smile", size: 18 }) : EmojiIcon })), showMediaUpload && (_jsxs(_Fragment, { children: [_jsx(Upload, { accept: "image/jpeg, image/png, image/jpg, video/*", beforeUpload: beforeUploadImagesAndVideo, multiple: true, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, children: _jsx(Button, Object.assign({ type: "text", shape: "default", className: btnClass }, crmLabel("composer_attach_image"), { children: crmComposer ? (_jsx(StrokeIcon, { name: "image", size: 18 })) : (_jsx(Icon, { icon: "image-02-o", size: 22 })) })) }), _jsx(Upload, { accept: ".doc,.docx,.pdf", beforeUpload: beforeUploadFile, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, children: _jsx(Button, Object.assign({ type: "text", shape: "default", className: btnClass }, crmLabel("composer_attach_file"), { children: crmComposer ? _jsx(StrokeIcon, { name: "paperclip", size: 18 }) : AttachIcon })) })] })), showEmojiPicker && (_jsx(EmojiPicker, { className: crmComposer ? "max-lg:w-80" : undefined, onEmojiSelect: handleEmojiSelect, onClose: () => setShowEmojiPicker(false) }))] }));
75
+ : "bg-[var(--dc-color-primary-light)] text-[var(--dc-color-primary)]")), onClick: () => setShowEmojiPicker(!showEmojiPicker) }, crmLabel("composer_emoji"), { children: crmComposer ? _jsx(StrokeIcon, { name: "smile", size: 18 }) : EmojiIcon })), showMediaUpload && (_jsx(MediaUploadButtons, { listUploadFiles: listUploadFiles, onFilesChange: onFilesChange, btnClass: btnClass, crmComposer: crmComposer, crmLabel: crmLabel, acceptVideo: acceptVideo, singleDocument: singleDocument })), showEmojiPicker && (_jsx(EmojiPicker, { className: crmComposer ? "max-lg:w-80" : undefined, onEmojiSelect: handleEmojiSelect, onClose: () => setShowEmojiPicker(false) }))] }));
109
76
  }
@@ -0,0 +1,13 @@
1
+ import { UploadFile } from "antd";
2
+ interface MediaUploadButtonsProps {
3
+ listUploadFiles: UploadFile[];
4
+ onFilesChange: (files: UploadFile[]) => void;
5
+ btnClass: string;
6
+ crmComposer: boolean;
7
+ crmLabel: (key: string) => Record<string, string | undefined>;
8
+ acceptVideo?: boolean;
9
+ singleDocument?: boolean;
10
+ }
11
+ export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btnClass, crmComposer, crmLabel, acceptVideo, singleDocument, }: MediaUploadButtonsProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
13
+ //# sourceMappingURL=MediaUploadButtons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaUploadButtons.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaUploadButtons.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkB,UAAU,EAA0B,MAAM,MAAM,CAAC;AA2B1E,UAAU,uBAAuB;IAC/B,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAE9D,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,WAAkB,EAClB,cAAsB,GACvB,EAAE,uBAAuB,2CA6FzB"}
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Upload, message as antdMessage } from "antd";
3
+ import { useTranslation } from "react-i18next";
4
+ import { Icon } from "../../../components/icon";
5
+ import { StrokeIcon } from "../../../components/icon/StrokeIcon";
6
+ import { MAX_DOCUMENT_SIZE_MB, validateFile } from "../../../utils/fileValidation";
7
+ import { DOCUMENT_TYPES } from "./editorConfig";
8
+ import { mergePickedUploadFiles } from "./mergePickedUploadFiles";
9
+ const AttachIcon = (_jsx("svg", { width: "22", height: "22", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M15.6 10.0001V11.2001C15.6 14.5137 12.9137 17.2001 9.59998 17.2001C6.28626 17.2001 3.59998 14.5137 3.59998 11.2001V6.79999C3.59998 4.59085 5.39084 2.79999 7.59998 2.79999C9.8091 2.79999 11.6 4.59085 11.6 6.79999V11.2C11.6 12.3045 10.7045 13.2 9.59998 13.2C8.49542 13.2 7.59998 12.3045 7.59998 11.2V7.99999", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
10
+ export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btnClass, crmComposer, crmLabel, acceptVideo = true, singleDocument = false, }) {
11
+ const { t } = useTranslation();
12
+ const documentLocked = singleDocument &&
13
+ listUploadFiles.some((f) => DOCUMENT_TYPES.includes(f.type || ""));
14
+ const beforeUploadImagesAndVideo = (file) => {
15
+ if (!acceptVideo && file.type.startsWith("video/")) {
16
+ antdMessage.error(t("invalid_image_format", { fileName: file.name }));
17
+ return Upload.LIST_IGNORE;
18
+ }
19
+ const validation = validateFile(file, t);
20
+ if (!validation.isValid) {
21
+ antdMessage.error(validation.error);
22
+ return Upload.LIST_IGNORE;
23
+ }
24
+ return false;
25
+ };
26
+ const beforeUploadFile = (file) => {
27
+ // The disabled button stops clicks; this also stops files set on the input directly.
28
+ if (documentLocked)
29
+ return Upload.LIST_IGNORE;
30
+ const isAllowed = DOCUMENT_TYPES.includes(file.type);
31
+ if (!isAllowed) {
32
+ antdMessage.error(t("invalid_document_format", { fileName: file.name }));
33
+ return Upload.LIST_IGNORE;
34
+ }
35
+ if (file.size / 1024 / 1024 > MAX_DOCUMENT_SIZE_MB) {
36
+ antdMessage.error(t("file_size_exceeded", {
37
+ fileName: file.name,
38
+ maxSize: MAX_DOCUMENT_SIZE_MB,
39
+ }));
40
+ return Upload.LIST_IGNORE;
41
+ }
42
+ return false;
43
+ };
44
+ const handleChange = (info) => {
45
+ onFilesChange(mergePickedUploadFiles(listUploadFiles, info.fileList, info.file));
46
+ };
47
+ return (_jsxs(_Fragment, { children: [_jsx(Upload, { accept: acceptVideo
48
+ ? "image/jpeg, image/png, image/jpg, video/*"
49
+ : "image/jpeg, image/png, image/jpg", beforeUpload: beforeUploadImagesAndVideo, multiple: true, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, children: _jsx(Button, Object.assign({ type: "text", shape: "default", className: btnClass }, crmLabel("composer_attach_image"), { children: crmComposer ? (_jsx(StrokeIcon, { name: "image", size: 18 })) : (_jsx(Icon, { icon: "image-02-o", size: 22 })) })) }), _jsx(Upload, { accept: ".doc,.docx,.pdf", beforeUpload: beforeUploadFile, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, disabled: documentLocked, children: _jsx(Button, Object.assign({ type: "text", shape: "default", className: btnClass, disabled: documentLocked }, crmLabel("composer_attach_file"), { children: crmComposer ? _jsx(StrokeIcon, { name: "paperclip", size: 18 }) : AttachIcon })) })] }));
50
+ }
@@ -0,0 +1,9 @@
1
+ import type { UploadFile } from "antd";
2
+ /**
3
+ * Turns antd's picked fileList into the composer list. Only one document is
4
+ * kept (the last picked). Items already in `current` (matched by uid) are kept
5
+ * as-is so URL-only attachments keep their url/response/status; newly picked
6
+ * Files are wrapped with createUploadFile.
7
+ */
8
+ export declare function mergePickedUploadFiles(current: UploadFile[], pickedList: UploadFile[], lastFile: UploadFile): UploadFile[];
9
+ //# sourceMappingURL=mergePickedUploadFiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergePickedUploadFiles.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/mergePickedUploadFiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAKvC;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,UAAU,EAAE,EACrB,UAAU,EAAE,UAAU,EAAE,EACxB,QAAQ,EAAE,UAAU,GACnB,UAAU,EAAE,CAgBd"}
@@ -0,0 +1,20 @@
1
+ import { createUploadFile } from "../../../utils/fileValidation";
2
+ import { DOCUMENT_TYPES } from "./editorConfig";
3
+ /**
4
+ * Turns antd's picked fileList into the composer list. Only one document is
5
+ * kept (the last picked). Items already in `current` (matched by uid) are kept
6
+ * as-is so URL-only attachments keep their url/response/status; newly picked
7
+ * Files are wrapped with createUploadFile.
8
+ */
9
+ export function mergePickedUploadFiles(current, pickedList, lastFile) {
10
+ let newList = [...pickedList];
11
+ if (DOCUMENT_TYPES.includes(lastFile.type || "")) {
12
+ newList = newList.filter((f) => !DOCUMENT_TYPES.includes(f.type || ""));
13
+ newList.push(lastFile);
14
+ }
15
+ const byUid = new Map(current.map((f) => [f.uid, f]));
16
+ return newList.map((file) => {
17
+ var _a, _b;
18
+ return (_a = byUid.get(file.uid)) !== null && _a !== void 0 ? _a : Object.assign(Object.assign({}, createUploadFile(((_b = file.originFileObj) !== null && _b !== void 0 ? _b : file))), { uid: file.uid });
19
+ });
20
+ }
@@ -8,7 +8,16 @@ interface RichTextEditorProps {
8
8
  showMediaUpload?: boolean;
9
9
  /** CRM: hide the emoji button on phones (the system keyboard has emoji). */
10
10
  hideEmojiOnPhone?: boolean;
11
+ /**
12
+ * Controlled attachment list. When provided the editor renders it and reports
13
+ * every change through onFilesChange; otherwise it keeps its own state.
14
+ */
15
+ files?: UploadFile[];
16
+ /** false: the image picker rejects video (default true). */
17
+ acceptVideo?: boolean;
18
+ /** true: one document max; attach-file is disabled while one is attached. */
19
+ singleDocument?: boolean;
11
20
  }
12
- export default function RichTextEditor({ initialHtml, placeholder, onChange, onFilesChange, minHeight, showMediaUpload, hideEmojiOnPhone, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
21
+ export default function RichTextEditor({ initialHtml, placeholder, onChange, onFilesChange, minHeight, showMediaUpload, hideEmojiOnPhone, files, acceptVideo, singleDocument, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
13
22
  export {};
14
23
  //# sourceMappingURL=RichTextEditor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../../src/components/rich-text-editor/RichTextEditor.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AA2ClC,UAAU,mBAAmB;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EACrC,WAAW,EACX,WAAW,EACX,QAAQ,EACR,aAAa,EACb,SAAmB,EACnB,eAAsB,EACtB,gBAAwB,GACzB,EAAE,mBAAmB,2CAkErB"}
1
+ {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../../src/components/rich-text-editor/RichTextEditor.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AA2ClC,UAAU,mBAAmB;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EACrC,WAAW,EACX,WAAW,EACX,QAAQ,EACR,aAAa,EACb,SAAmB,EACnB,eAAsB,EACtB,gBAAwB,EACxB,KAAK,EACL,WAAkB,EAClB,cAAsB,GACvB,EAAE,mBAAmB,2CAmErB"}
@@ -39,8 +39,9 @@ function HtmlPlugin({ initialHtml }) {
39
39
  }, [editor, initialHtml]);
40
40
  return null;
41
41
  }
42
- export default function RichTextEditor({ initialHtml, placeholder, onChange, onFilesChange, minHeight = "160px", showMediaUpload = true, hideEmojiOnPhone = false, }) {
43
- const [listUploadFiles, setListUploadFiles] = useState([]);
42
+ export default function RichTextEditor({ initialHtml, placeholder, onChange, onFilesChange, minHeight = "160px", showMediaUpload = true, hideEmojiOnPhone = false, files, acceptVideo = true, singleDocument = false, }) {
43
+ const [innerFiles, setInnerFiles] = useState([]);
44
+ const listUploadFiles = files !== null && files !== void 0 ? files : innerFiles;
44
45
  const handleChange = (editorState, editor, tags) => {
45
46
  if (tags.has("initial-html"))
46
47
  return;
@@ -51,14 +52,13 @@ export default function RichTextEditor({ initialHtml, placeholder, onChange, onF
51
52
  });
52
53
  };
53
54
  const handleRemoveFile = (file) => {
54
- const newList = listUploadFiles.filter((f) => f.uid !== file.uid);
55
- setListUploadFiles(newList);
56
- onFilesChange === null || onFilesChange === void 0 ? void 0 : onFilesChange(newList);
55
+ handleFilesChange(listUploadFiles.filter((f) => f.uid !== file.uid));
57
56
  };
58
- const handleFilesChange = (files) => {
59
- setListUploadFiles(files);
60
- onFilesChange === null || onFilesChange === void 0 ? void 0 : onFilesChange(files);
57
+ const handleFilesChange = (next) => {
58
+ if (files === undefined)
59
+ setInnerFiles(next);
60
+ onFilesChange === null || onFilesChange === void 0 ? void 0 : onFilesChange(next);
61
61
  };
62
62
  return (_jsxs(LexicalComposer, { initialConfig: EDITOR_CONFIG, children: [_jsxs("div", { className: "border border-gray-200 rounded-lg", children: [_jsx(FilePreviewList, { files: listUploadFiles, onRemove: handleRemoveFile }), _jsx("div", { className: "relative", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "px-3 py-2 text-sm outline-none", style: { minHeight } }), ErrorBoundary: LexicalErrorBoundary, placeholder: placeholder ? (_jsx("div", { className: "absolute top-2 left-3 pointer-events-none", children: _jsx("p", { className: "text-gray-400 text-sm", children: placeholder }) })) : null }) }), _jsxs("div", { className: clsx("flex items-center border-t border-gray-100 px-2 py-1", hideEmojiOnPhone &&
63
- "flex-wrap max-sm:[&_[data-cp-name=ToolbarPlugin]]:!gap-1 max-sm:[&_[data-cp-name=ToolbarPlugin]]:!px-1"), children: [_jsx(ToolbarPlugin, {}), _jsx(MediaActions, { listUploadFiles: listUploadFiles, onFilesChange: handleFilesChange, showMediaUpload: showMediaUpload, hideEmojiOnPhone: hideEmojiOnPhone })] })] }), _jsx(LinkPlugin, {}), _jsx(ListPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true }), _jsx(HtmlPlugin, { initialHtml: initialHtml })] }));
63
+ "flex-wrap max-sm:[&_[data-cp-name=ToolbarPlugin]]:!gap-1 max-sm:[&_[data-cp-name=ToolbarPlugin]]:!px-1"), children: [_jsx(ToolbarPlugin, {}), _jsx(MediaActions, { listUploadFiles: listUploadFiles, onFilesChange: handleFilesChange, showMediaUpload: showMediaUpload, hideEmojiOnPhone: hideEmojiOnPhone, acceptVideo: acceptVideo, singleDocument: singleDocument })] })] }), _jsx(LinkPlugin, {}), _jsx(ListPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true }), _jsx(HtmlPlugin, { initialHtml: initialHtml })] }));
64
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAkDA,eAAO,MAAM,cAAc,YA4nB1B,CAAC"}
1
+ {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAkDA,eAAO,MAAM,cAAc,YA8nB1B,CAAC"}
@@ -243,16 +243,18 @@ export const useGlobalEvent = () => {
243
243
  const syncProgressHandler = ({ data }) => {
244
244
  useBootStore.getState().setSyncProgress(data);
245
245
  };
246
+ // Sync status first: the boot store re-renders at once, and ending the background
247
+ // phase while the status still says Loading would make useCanLoadChatData flicker off.
246
248
  const syncFinishHandler = () => {
247
- useBootStore.getState().markFirstSyncDone();
248
249
  updateSyncStatus(SyncStatus.Success);
250
+ useBootStore.getState().markFirstSyncDone();
249
251
  getSelfUserInfo();
250
252
  loadConversationList();
251
253
  void refreshCurrentGroupState();
252
254
  };
253
255
  const syncFailedHandler = () => {
254
- useBootStore.getState().markFirstSyncDone();
255
256
  updateSyncStatus(SyncStatus.Failed);
257
+ useBootStore.getState().markFirstSyncDone();
256
258
  };
257
259
  // The bot behind the open thread changed (flow swapped, type switched,
258
260
  // activated/deactivated). Everything the bot UI renders is react-query