@repzo/widget-react-native 1.3.0 → 1.4.0
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/CHANGELOG.md +179 -0
- package/README.md +72 -16
- package/lib/commonjs/api/client.js +113 -0
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/attachments/file-size.js +52 -0
- package/lib/commonjs/attachments/file-size.js.map +1 -0
- package/lib/commonjs/attachments/local-previews.js +40 -0
- package/lib/commonjs/attachments/local-previews.js.map +1 -0
- package/lib/commonjs/attachments/pickers.js +76 -28
- package/lib/commonjs/attachments/pickers.js.map +1 -1
- package/lib/commonjs/i18n.js +24 -3
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +13 -3
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +486 -63
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/storage.js +3 -1
- package/lib/commonjs/storage.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +40 -16
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/AttachmentChips.js +109 -40
- package/lib/commonjs/ui/components/AttachmentChips.js.map +1 -1
- package/lib/commonjs/ui/components/ConversationRow.js +65 -10
- package/lib/commonjs/ui/components/ConversationRow.js.map +1 -1
- package/lib/commonjs/ui/components/ImageViewer.js +231 -25
- package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
- package/lib/commonjs/ui/components/RatingCard.js +5 -2
- package/lib/commonjs/ui/components/RatingCard.js.map +1 -1
- package/lib/commonjs/ui/components/ZoomableImage.js +58 -10
- package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -1
- package/lib/commonjs/ui/components/icons.js +22 -16
- package/lib/commonjs/ui/components/icons.js.map +1 -1
- package/lib/commonjs/ui/hooks/use-retained-state.js +39 -0
- package/lib/commonjs/ui/hooks/use-retained-state.js.map +1 -0
- package/lib/commonjs/ui/screens/chat/ChatView.js +666 -171
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +150 -3
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/commonjs/ui/screens/conversations/ConversationsList.js +1 -0
- package/lib/commonjs/ui/screens/conversations/ConversationsList.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.js +38 -55
- package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.styles.js +36 -45
- package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/commonjs/ui/styles/theme.js +1 -0
- package/lib/commonjs/ui/styles/theme.js.map +1 -1
- package/lib/commonjs/ui/utils/conversations.js +17 -1
- package/lib/commonjs/ui/utils/conversations.js.map +1 -1
- package/lib/commonjs/ui/utils/html.js +13 -0
- package/lib/commonjs/ui/utils/html.js.map +1 -1
- package/lib/commonjs/ui/utils/image-load-queue.js +114 -0
- package/lib/commonjs/ui/utils/image-load-queue.js.map +1 -0
- package/lib/commonjs/ui/utils/inline-attachments.js +88 -0
- package/lib/commonjs/ui/utils/inline-attachments.js.map +1 -0
- package/lib/module/api/client.js +113 -0
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/attachments/file-size.js +47 -0
- package/lib/module/attachments/file-size.js.map +1 -0
- package/lib/module/attachments/local-previews.js +33 -0
- package/lib/module/attachments/local-previews.js.map +1 -0
- package/lib/module/attachments/pickers.js +74 -27
- package/lib/module/attachments/pickers.js.map +1 -1
- package/lib/module/i18n.js +24 -3
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +13 -3
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/widget-store.js +488 -65
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/storage.js +2 -0
- package/lib/module/storage.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +40 -16
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/AttachmentChips.js +111 -42
- package/lib/module/ui/components/AttachmentChips.js.map +1 -1
- package/lib/module/ui/components/ConversationRow.js +65 -10
- package/lib/module/ui/components/ConversationRow.js.map +1 -1
- package/lib/module/ui/components/ImageViewer.js +233 -27
- package/lib/module/ui/components/ImageViewer.js.map +1 -1
- package/lib/module/ui/components/RatingCard.js +5 -2
- package/lib/module/ui/components/RatingCard.js.map +1 -1
- package/lib/module/ui/components/ZoomableImage.js +58 -10
- package/lib/module/ui/components/ZoomableImage.js.map +1 -1
- package/lib/module/ui/components/icons.js +21 -15
- package/lib/module/ui/components/icons.js.map +1 -1
- package/lib/module/ui/hooks/use-retained-state.js +34 -0
- package/lib/module/ui/hooks/use-retained-state.js.map +1 -0
- package/lib/module/ui/screens/chat/ChatView.js +672 -177
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +152 -4
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/ui/screens/conversations/ConversationsList.js +1 -0
- package/lib/module/ui/screens/conversations/ConversationsList.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.js +40 -57
- package/lib/module/ui/screens/home/HomeView.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.styles.js +36 -45
- package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/module/ui/styles/theme.js +1 -0
- package/lib/module/ui/styles/theme.js.map +1 -1
- package/lib/module/ui/utils/conversations.js +15 -1
- package/lib/module/ui/utils/conversations.js.map +1 -1
- package/lib/module/ui/utils/html.js +12 -0
- package/lib/module/ui/utils/html.js.map +1 -1
- package/lib/module/ui/utils/image-load-queue.js +109 -0
- package/lib/module/ui/utils/image-load-queue.js.map +1 -0
- package/lib/module/ui/utils/inline-attachments.js +85 -0
- package/lib/module/ui/utils/inline-attachments.js.map +1 -0
- package/lib/typescript/commonjs/api/client.d.ts +35 -1
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/attachments/file-size.d.ts +15 -0
- package/lib/typescript/commonjs/attachments/file-size.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachments/local-previews.d.ts +14 -0
- package/lib/typescript/commonjs/attachments/local-previews.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachments/pickers.d.ts +35 -10
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +13 -3
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +67 -4
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage.d.ts +2 -0
- package/lib/typescript/commonjs/storage.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +58 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts +13 -3
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts +15 -1
- package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +26 -6
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +10 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts +11 -0
- package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +4 -4
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +154 -2
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +30 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -5
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +30 -35
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts +5 -6
- package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/styles/theme.d.ts +2 -0
- package/lib/typescript/commonjs/ui/styles/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/conversations.d.ts +10 -2
- package/lib/typescript/commonjs/ui/utils/conversations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/html.d.ts +5 -0
- package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts +44 -0
- package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts +29 -0
- package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts.map +1 -0
- package/lib/typescript/module/api/client.d.ts +35 -1
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/attachments/file-size.d.ts +15 -0
- package/lib/typescript/module/attachments/file-size.d.ts.map +1 -0
- package/lib/typescript/module/attachments/local-previews.d.ts +14 -0
- package/lib/typescript/module/attachments/local-previews.d.ts.map +1 -0
- package/lib/typescript/module/attachments/pickers.d.ts +35 -10
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +13 -3
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +67 -4
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/storage.d.ts +2 -0
- package/lib/typescript/module/storage.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +58 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts +13 -3
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ConversationRow.d.ts +15 -1
- package/lib/typescript/module/ui/components/ConversationRow.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ImageViewer.d.ts +26 -6
- package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts +10 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/icons.d.ts +2 -1
- package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/use-retained-state.d.ts +11 -0
- package/lib/typescript/module/ui/hooks/use-retained-state.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +4 -4
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +154 -2
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +30 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -5
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +30 -35
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts +5 -6
- package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/styles/theme.d.ts +2 -0
- package/lib/typescript/module/ui/styles/theme.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/conversations.d.ts +10 -2
- package/lib/typescript/module/ui/utils/conversations.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/html.d.ts +5 -0
- package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/image-load-queue.d.ts +44 -0
- package/lib/typescript/module/ui/utils/image-load-queue.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/inline-attachments.d.ts +29 -0
- package/lib/typescript/module/ui/utils/inline-attachments.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/api/client.ts +148 -2
- package/src/attachments/file-size.ts +42 -0
- package/src/attachments/local-previews.ts +32 -0
- package/src/attachments/pickers.ts +90 -30
- package/src/i18n.ts +24 -3
- package/src/locales.ts +13 -3
- package/src/state/widget-store.ts +519 -70
- package/src/storage.ts +2 -0
- package/src/types.ts +58 -0
- package/src/ui/WidgetProvider.tsx +50 -12
- package/src/ui/components/AttachmentChips.tsx +100 -29
- package/src/ui/components/ConversationRow.tsx +90 -9
- package/src/ui/components/ImageViewer.tsx +250 -28
- package/src/ui/components/RatingCard.tsx +5 -2
- package/src/ui/components/ZoomableImage.tsx +58 -5
- package/src/ui/components/icons.tsx +10 -12
- package/src/ui/hooks/use-retained-state.ts +35 -0
- package/src/ui/screens/chat/ChatView.styles.ts +151 -3
- package/src/ui/screens/chat/ChatView.tsx +760 -213
- package/src/ui/screens/chat/ChatView.types.ts +30 -1
- package/src/ui/screens/conversations/ConversationsList.tsx +1 -0
- package/src/ui/screens/conversations/ConversationsList.types.ts +2 -0
- package/src/ui/screens/home/HomeView.styles.ts +36 -45
- package/src/ui/screens/home/HomeView.tsx +54 -70
- package/src/ui/screens/home/HomeView.types.ts +5 -6
- package/src/ui/styles/theme.ts +6 -0
- package/src/ui/utils/conversations.ts +16 -2
- package/src/ui/utils/html.ts +26 -0
- package/src/ui/utils/image-load-queue.ts +108 -0
- package/src/ui/utils/inline-attachments.ts +71 -0
|
@@ -7,6 +7,7 @@ exports.ChatView = ChatView;
|
|
|
7
7
|
var _react = require("react");
|
|
8
8
|
var _reactNative = require("react-native");
|
|
9
9
|
var _agentUiBlocks = require("../../../agent-ui-blocks.js");
|
|
10
|
+
var _localPreviews = require("../../../attachments/local-previews.js");
|
|
10
11
|
var _pickers = require("../../../attachments/pickers.js");
|
|
11
12
|
var _i18n = require("../../../i18n.js");
|
|
12
13
|
var _AgentUiBlocks = require("../../components/AgentUiBlocks.js");
|
|
@@ -21,11 +22,14 @@ var _icons = require("../../components/icons.js");
|
|
|
21
22
|
var _TeamAvatars = require("../../components/TeamAvatars.js");
|
|
22
23
|
var _Markdown = require("../../components/Markdown.js");
|
|
23
24
|
var _useReduceMotion = require("../../hooks/use-reduce-motion.js");
|
|
25
|
+
var _useRetainedState = require("../../hooks/use-retained-state.js");
|
|
24
26
|
var _useTopInset = require("../../hooks/use-top-inset.js");
|
|
25
27
|
var _global = require("../../styles/global.js");
|
|
26
28
|
var _theme = require("../../styles/theme.js");
|
|
27
29
|
var _conversations = require("../../utils/conversations.js");
|
|
28
30
|
var _html = require("../../utils/html.js");
|
|
31
|
+
var _imageLoadQueue = require("../../utils/image-load-queue.js");
|
|
32
|
+
var _inlineAttachments = require("../../utils/inline-attachments.js");
|
|
29
33
|
var _safeUrl = require("../../utils/safe-url.js");
|
|
30
34
|
var _voice = require("../../../audio/voice.js");
|
|
31
35
|
var _VoiceNote = require("../../components/VoiceNote.js");
|
|
@@ -38,12 +42,63 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
38
42
|
* Chat thread — message list, typing indicator and composer.
|
|
39
43
|
* Port of `client/src/widget/components/ChatView.tsx`.
|
|
40
44
|
*
|
|
41
|
-
* Mobile differences:
|
|
42
|
-
*
|
|
43
|
-
*
|
|
45
|
+
* Mobile differences: the thread is an inverted list, so it opens on the
|
|
46
|
+
* newest message and older pages load as the visitor scrolls up, and the
|
|
47
|
+
* attach button only shows when the host app has a picker to open (see
|
|
44
48
|
* `attachments/pickers.ts`).
|
|
45
49
|
*/
|
|
46
50
|
|
|
51
|
+
/** A message with what its place in the thread decides about it. */
|
|
52
|
+
|
|
53
|
+
const rowKey = row => row.message.id;
|
|
54
|
+
|
|
55
|
+
/** How far above the newest message the visitor scrolls before "jump to latest" shows. */
|
|
56
|
+
const JUMP_TO_LATEST_OFFSET = 240;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Workstation's refusal of a message that names an expired upload. Only this
|
|
60
|
+
* one puts the files up again: a deleted conversation or a rejected message
|
|
61
|
+
* would refuse the fresh uploads too.
|
|
62
|
+
*/
|
|
63
|
+
const UPLOAD_EXPIRED_STATUS = 410;
|
|
64
|
+
/**
|
|
65
|
+
* An upload this close to expiring goes up again with the message instead of
|
|
66
|
+
* being named by its token, which could lapse before the message arrives.
|
|
67
|
+
*/
|
|
68
|
+
const UPLOAD_EXPIRY_MARGIN_MS = 10 * 60 * 1000;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Photos above this take turns to download, two at a time; smaller ones load
|
|
72
|
+
* at once. Two 50 MB photos stay well inside Android's download pool (see
|
|
73
|
+
* `ImageLoadQueue`), and ordinary photos never wait.
|
|
74
|
+
*/
|
|
75
|
+
const LARGE_IMAGE_BYTES = 10_000_000;
|
|
76
|
+
const LARGE_IMAGE_LOADS_AT_ONCE = 2;
|
|
77
|
+
/** A photo that loads quicker than this never shows its loader. */
|
|
78
|
+
const PHOTO_LOADER_DELAY_MS = 200;
|
|
79
|
+
|
|
80
|
+
/** A photo download that waits its turn; the phone's own copy never does. */
|
|
81
|
+
const isLargeDownload = (url, size) => (0, _safeUrl.isHttpUrl)(url) && (size ?? 0) > LARGE_IMAGE_BYTES;
|
|
82
|
+
const ImageQueueContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
83
|
+
const subscribeNowhere = () => () => {};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether an image may load yet, and the callback that tells the queue it
|
|
87
|
+
* has (or failed). Images the queue doesn't handle load right away.
|
|
88
|
+
*/
|
|
89
|
+
function useImageTurn(key, priority, queued) {
|
|
90
|
+
const queue = (0, _react.useContext)(ImageQueueContext);
|
|
91
|
+
const active = !!queue && queued;
|
|
92
|
+
(0, _react.useEffect)(() => active ? queue.add(key, priority) : undefined, [active, queue, key, priority]);
|
|
93
|
+
const turn = (0, _react.useSyncExternalStore)(queue ? queue.subscribe : subscribeNowhere, () => active ? queue.canLoad(key) : true);
|
|
94
|
+
const settled = (0, _react.useCallback)(() => queue?.finish(key), [queue, key]);
|
|
95
|
+
return [turn, settled];
|
|
96
|
+
}
|
|
97
|
+
function ThreadGap() {
|
|
98
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
99
|
+
style: _ChatViewStyles.styles.threadGap
|
|
100
|
+
});
|
|
101
|
+
}
|
|
47
102
|
function ChatView({
|
|
48
103
|
messages,
|
|
49
104
|
loading,
|
|
@@ -63,10 +118,13 @@ function ChatView({
|
|
|
63
118
|
labels,
|
|
64
119
|
attachmentSources = [],
|
|
65
120
|
onPickAttachments,
|
|
121
|
+
onUploadAttachment,
|
|
66
122
|
onSendMessage,
|
|
123
|
+
onRetryMessage,
|
|
67
124
|
onIdentifyVisitor,
|
|
68
125
|
onRateConversation,
|
|
69
126
|
defaultLocale,
|
|
127
|
+
photoAlbums = true,
|
|
70
128
|
onLoadMore,
|
|
71
129
|
onTyping,
|
|
72
130
|
onBack,
|
|
@@ -76,30 +134,46 @@ function ChatView({
|
|
|
76
134
|
const [composerFocused, setComposerFocused] = (0, _react.useState)(false);
|
|
77
135
|
const [drafts, setDrafts] = (0, _react.useState)([]);
|
|
78
136
|
const [menuOpen, setMenuOpen] = (0, _react.useState)(false);
|
|
79
|
-
const [
|
|
137
|
+
const [viewerGallery, setViewerGallery] = (0, _react.useState)(null);
|
|
80
138
|
const [picking, setPicking] = (0, _react.useState)(false);
|
|
81
139
|
// Only attachment sends hold the composer; text sends stay fire-and-forget.
|
|
82
140
|
const [sending, setSending] = (0, _react.useState)(false);
|
|
83
141
|
const [notice, setNotice] = (0, _react.useState)(null);
|
|
84
142
|
const reduceMotion = (0, _useReduceMotion.useReduceMotion)();
|
|
85
|
-
const
|
|
143
|
+
const listRef = (0, _react.useRef)(null);
|
|
144
|
+
// One per thread: the screen remounts for each conversation.
|
|
145
|
+
const [largeImageQueue] = (0, _react.useState)(() => new _imageLoadQueue.ImageLoadQueue(LARGE_IMAGE_LOADS_AT_ONCE));
|
|
86
146
|
const typingStopTimer = (0, _react.useRef)(null);
|
|
87
147
|
const noticeTimer = (0, _react.useRef)(null);
|
|
88
148
|
const pickerTimer = (0, _react.useRef)(null);
|
|
89
149
|
const mounted = (0, _react.useRef)(false);
|
|
150
|
+
// Uploads in flight, by draft id. A removed chip cancels its own; a retry
|
|
151
|
+
// replaces it, and the replaced attempt's result is dropped.
|
|
152
|
+
const uploads = (0, _react.useRef)(new Map());
|
|
153
|
+
// Files already put up again after a refused send; never twice, so a
|
|
154
|
+
// refusal uploads can't fix costs one round, not one per tap on Send.
|
|
155
|
+
const reuploaded = (0, _react.useRef)(new Set());
|
|
156
|
+
// Set as the visitor sends: the list scrolls down once their bubble is in
|
|
157
|
+
// it. Scrolled up, scrolling first let the list hold its place as the new
|
|
158
|
+
// row arrived, which could cut the scroll short.
|
|
159
|
+
const followSend = (0, _react.useRef)(false);
|
|
90
160
|
(0, _react.useEffect)(() => {
|
|
91
161
|
if (prefillText) setInput(prefillText);
|
|
92
162
|
}, [prefillText]);
|
|
93
163
|
|
|
94
164
|
// The provider keys this screen by the store's thread generation. Pending
|
|
95
|
-
// picks and sends belong to this mount, even before a draft gets a
|
|
165
|
+
// picks, uploads and sends belong to this mount, even before a draft gets a
|
|
166
|
+
// server ID.
|
|
96
167
|
(0, _react.useEffect)(() => {
|
|
97
168
|
mounted.current = true;
|
|
169
|
+
const inFlight = uploads.current;
|
|
98
170
|
return () => {
|
|
99
171
|
mounted.current = false;
|
|
100
172
|
if (typingStopTimer.current) clearTimeout(typingStopTimer.current);
|
|
101
173
|
if (noticeTimer.current) clearTimeout(noticeTimer.current);
|
|
102
174
|
if (pickerTimer.current) clearTimeout(pickerTimer.current);
|
|
175
|
+
inFlight.forEach(upload => upload.cancel());
|
|
176
|
+
inFlight.clear();
|
|
103
177
|
};
|
|
104
178
|
}, []);
|
|
105
179
|
const showNotice = (0, _react.useCallback)(message => {
|
|
@@ -107,50 +181,66 @@ function ChatView({
|
|
|
107
181
|
setNotice(message);
|
|
108
182
|
if (message) noticeTimer.current = setTimeout(() => setNotice(null), 5000);
|
|
109
183
|
}, []);
|
|
184
|
+
|
|
185
|
+
// The list is inverted, so offset 0 is the newest message: the thread opens
|
|
186
|
+
// there without scrolling, stays there as the keyboard rises or replies
|
|
187
|
+
// arrive, and older pages land above it without moving the reader.
|
|
110
188
|
const scrollToBottom = (0, _react.useCallback)((animated = true) => {
|
|
111
|
-
|
|
189
|
+
listRef.current?.scrollToOffset({
|
|
190
|
+
offset: 0,
|
|
112
191
|
animated
|
|
113
192
|
});
|
|
114
193
|
}, []);
|
|
115
194
|
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// handler below covers later growth (loads, images, incoming messages).
|
|
119
|
-
(0, _react.useEffect)(() => {
|
|
120
|
-
const frame = requestAnimationFrame(() => scrollToBottom(false));
|
|
121
|
-
return () => cancelAnimationFrame(frame);
|
|
122
|
-
}, [scrollToBottom]);
|
|
123
|
-
|
|
124
|
-
// "Load earlier" PREPENDS history; skipping one auto-scroll keeps the reader
|
|
125
|
-
// anchored where they were instead of yanking them back to the bottom.
|
|
126
|
-
const skipNextAutoScroll = (0, _react.useRef)(false);
|
|
195
|
+
// Fires as the visitor nears the oldest message; the row up there is the
|
|
196
|
+
// fallback when a page fails to load.
|
|
127
197
|
const handleLoadEarlier = (0, _react.useCallback)(() => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}, [onLoadMore]);
|
|
131
|
-
const handleContentSizeChange = (0, _react.useCallback)(() => {
|
|
132
|
-
if (skipNextAutoScroll.current) {
|
|
133
|
-
skipNextAutoScroll.current = false;
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
scrollToBottom(false);
|
|
137
|
-
}, [scrollToBottom]);
|
|
138
|
-
|
|
139
|
-
// The keyboard shortens the thread instead of scrolling it, so whatever was
|
|
140
|
-
// at the bottom — the newest message, an agent form — would slide in behind
|
|
141
|
-
// the composer. Re-pin on every shrink, unanimated, so the list tracks the
|
|
142
|
-
// keyboard as it rises rather than jumping once it lands.
|
|
143
|
-
const viewportHeight = (0, _react.useRef)(0);
|
|
144
|
-
const handleViewportLayout = (0, _react.useCallback)(event => {
|
|
145
|
-
const {
|
|
146
|
-
height
|
|
147
|
-
} = event.nativeEvent.layout;
|
|
148
|
-
const shrank = viewportHeight.current - height > 1;
|
|
149
|
-
viewportHeight.current = height;
|
|
150
|
-
if (shrank) scrollToBottom(false);
|
|
151
|
-
}, [scrollToBottom]);
|
|
198
|
+
if (hasMoreMessages && !loading) onLoadMore();
|
|
199
|
+
}, [hasMoreMessages, loading, onLoadMore]);
|
|
152
200
|
const showAttach = attachmentSources.length > 0 && !!onPickAttachments;
|
|
153
|
-
|
|
201
|
+
// Send waits until every picked file is up, so the message goes out at once.
|
|
202
|
+
const uploadsPending = drafts.some(draft => draft.uploadStatus === 'uploading' || draft.uploadStatus === 'failed');
|
|
203
|
+
const canSend = !sending && !uploadsPending && (input.trim().length > 0 || drafts.length > 0);
|
|
204
|
+
const updateDraft = (0, _react.useCallback)((id, patch) => setDrafts(current => current.map(draft => draft.id === id ? {
|
|
205
|
+
...draft,
|
|
206
|
+
...patch
|
|
207
|
+
} : draft)), []);
|
|
208
|
+
|
|
209
|
+
/** Puts a picked local file up while the visitor writes; inline files need nothing. */
|
|
210
|
+
const startUpload = draft => {
|
|
211
|
+
if (!onUploadAttachment || !draft.fileUri) return;
|
|
212
|
+
uploads.current.get(draft.id)?.cancel();
|
|
213
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
214
|
+
const attempt = {
|
|
215
|
+
cancel: () => controller?.abort()
|
|
216
|
+
};
|
|
217
|
+
uploads.current.set(draft.id, attempt);
|
|
218
|
+
updateDraft(draft.id, {
|
|
219
|
+
uploadStatus: 'uploading',
|
|
220
|
+
uploadToken: undefined,
|
|
221
|
+
uploadExpiresAt: undefined
|
|
222
|
+
});
|
|
223
|
+
onUploadAttachment(draft, controller?.signal).then(({
|
|
224
|
+
uploadToken,
|
|
225
|
+
expiresAt
|
|
226
|
+
}) => {
|
|
227
|
+
if (!mounted.current || uploads.current.get(draft.id) !== attempt) return;
|
|
228
|
+
uploads.current.delete(draft.id);
|
|
229
|
+
updateDraft(draft.id, {
|
|
230
|
+
uploadStatus: 'uploaded',
|
|
231
|
+
uploadToken,
|
|
232
|
+
uploadExpiresAt: expiresAt
|
|
233
|
+
});
|
|
234
|
+
}, error => {
|
|
235
|
+
if (!mounted.current || uploads.current.get(draft.id) !== attempt) return;
|
|
236
|
+
console.warn('[repzo-widget] attachment upload failed', error);
|
|
237
|
+
uploads.current.delete(draft.id);
|
|
238
|
+
updateDraft(draft.id, {
|
|
239
|
+
uploadStatus: 'failed'
|
|
240
|
+
});
|
|
241
|
+
showNotice(labels.attach.uploadFailedNotice);
|
|
242
|
+
});
|
|
243
|
+
};
|
|
154
244
|
const problemLabel = problem => {
|
|
155
245
|
switch (problem) {
|
|
156
246
|
case 'tooMany':
|
|
@@ -175,6 +265,8 @@ function ChatView({
|
|
|
175
265
|
if (!mounted.current) return;
|
|
176
266
|
if (result.added.length > 0) {
|
|
177
267
|
setDrafts(current => [...current, ...result.added].slice(0, _pickers.MAX_ATTACHMENTS));
|
|
268
|
+
// Opted out, the files go up when the visitor taps Send instead.
|
|
269
|
+
if ((0, _pickers.uploadsOnPick)()) result.added.forEach(startUpload);
|
|
178
270
|
}
|
|
179
271
|
showNotice(problemLabel(result.problem));
|
|
180
272
|
} finally {
|
|
@@ -214,18 +306,24 @@ function ChatView({
|
|
|
214
306
|
setInput('');
|
|
215
307
|
if (typingStopTimer.current) clearTimeout(typingStopTimer.current);
|
|
216
308
|
onTyping(false);
|
|
217
|
-
|
|
309
|
+
followSend.current = true;
|
|
218
310
|
if (drafts.length === 0) {
|
|
219
311
|
// The failed bubble already surfaces a failure to the visitor.
|
|
220
312
|
onSendMessage(trimmed).catch(() => {});
|
|
221
313
|
return;
|
|
222
314
|
}
|
|
223
315
|
const sent = drafts;
|
|
316
|
+
// Named by its token while that lasts; otherwise the file goes up again
|
|
317
|
+
// as the message is sent.
|
|
318
|
+
const sendBy = Date.now() + UPLOAD_EXPIRY_MARGIN_MS;
|
|
319
|
+
const stillUploaded = draft => !!draft.uploadToken && (draft.uploadExpiresAt ?? Infinity) > sendBy;
|
|
224
320
|
setSending(true);
|
|
225
321
|
showNotice(null);
|
|
226
322
|
onSendMessage(trimmed, sent.map(draft => ({
|
|
227
323
|
name: draft.name,
|
|
228
324
|
url: draft.dataUrl,
|
|
325
|
+
fileUri: draft.fileUri,
|
|
326
|
+
uploadToken: stillUploaded(draft) ? draft.uploadToken : undefined,
|
|
229
327
|
contentType: draft.contentType,
|
|
230
328
|
size: draft.size,
|
|
231
329
|
previewUrl: draft.previewUri
|
|
@@ -241,6 +339,15 @@ function ChatView({
|
|
|
241
339
|
setInput(current => current || trimmed);
|
|
242
340
|
const status = error?.status;
|
|
243
341
|
showNotice(status === 413 ? labels.attach.tooLargeTogether : labels.failedToSend);
|
|
342
|
+
// An upload expired before this phone's clock said it would: put the
|
|
343
|
+
// files sent by token up again, once. After a lost connection the
|
|
344
|
+
// uploads still stand for the next try.
|
|
345
|
+
if (status === UPLOAD_EXPIRED_STATUS) {
|
|
346
|
+
sent.filter(draft => stillUploaded(draft) && !reuploaded.current.has(draft.id)).forEach(draft => {
|
|
347
|
+
reuploaded.current.add(draft.id);
|
|
348
|
+
startUpload(draft);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
244
351
|
}).finally(() => {
|
|
245
352
|
if (mounted.current) setSending(false);
|
|
246
353
|
});
|
|
@@ -260,11 +367,18 @@ function ChatView({
|
|
|
260
367
|
phone: stringPayload(action.payload.phone)
|
|
261
368
|
});
|
|
262
369
|
}
|
|
370
|
+
followSend.current = true;
|
|
263
371
|
onSendMessage((0, _agentUiBlocks.formatAgentUiActionMessage)(action), undefined, action).catch(() => {});
|
|
264
|
-
|
|
265
|
-
}, [onIdentifyVisitor, onSendMessage, scrollToBottom]);
|
|
372
|
+
}, [onIdentifyVisitor, onSendMessage]);
|
|
266
373
|
const handleSubmitProcedureForm = (0, _react.useCallback)(body => void onSendMessage(body).catch(() => {}), [onSendMessage]);
|
|
267
374
|
|
|
375
|
+
// The retried bubble moves to the end of the thread; follow it there.
|
|
376
|
+
// A second failure shows on the bubble again.
|
|
377
|
+
const handleRetryMessage = (0, _react.useCallback)(messageId => {
|
|
378
|
+
followSend.current = true;
|
|
379
|
+
onRetryMessage?.(messageId).catch(() => {});
|
|
380
|
+
}, [onRetryMessage]);
|
|
381
|
+
|
|
268
382
|
// Recomputed only when the list changes — never on a keystroke.
|
|
269
383
|
const thread = (0, _react.useMemo)(() => deriveThread(messages), [messages]);
|
|
270
384
|
const locales = (0, _react.useMemo)(() => [locale, defaultLocale], [locale, defaultLocale]);
|
|
@@ -283,6 +397,143 @@ function ChatView({
|
|
|
283
397
|
const headerControl = {
|
|
284
398
|
backgroundColor: (0, _theme.withAlpha)(headerTextColor, 0.16)
|
|
285
399
|
};
|
|
400
|
+
|
|
401
|
+
// Newest first, for the inverted list; each row still compares itself with
|
|
402
|
+
// the message sent before it.
|
|
403
|
+
const rows = (0, _react.useMemo)(() => messages.map((message, index) => {
|
|
404
|
+
const previous = index > 0 ? messages[index - 1] : undefined;
|
|
405
|
+
return {
|
|
406
|
+
message,
|
|
407
|
+
startsDay: !previous || !(0, _conversations.isSameDay)(message.createdAt, previous.createdAt),
|
|
408
|
+
// Name only at the top of a run, the way every messenger does it.
|
|
409
|
+
startsRun: !previous || previous.direction === 'inbound' || previous.messageType === 'system' || previous.senderName !== message.senderName
|
|
410
|
+
};
|
|
411
|
+
}).reverse(), [messages]);
|
|
412
|
+
(0, _react.useEffect)(() => {
|
|
413
|
+
if (!followSend.current) return;
|
|
414
|
+
followSend.current = false;
|
|
415
|
+
scrollToBottom();
|
|
416
|
+
}, [rows, scrollToBottom]);
|
|
417
|
+
|
|
418
|
+
// The thread's message ids when the visitor scrolled away from the newest,
|
|
419
|
+
// null while it's in view. Teammate replies newer than all of them are
|
|
420
|
+
// counted on the button. A set, not the newest id alone: that may be the
|
|
421
|
+
// visitor's own bubble, whose temporary id changes once the server has it.
|
|
422
|
+
const [awayFrom, setAwayFrom] = (0, _react.useState)(null);
|
|
423
|
+
const rowIds = (0, _react.useRef)([]);
|
|
424
|
+
(0, _react.useEffect)(() => {
|
|
425
|
+
rowIds.current = rows.map(row => row.message.id);
|
|
426
|
+
}, [rows]);
|
|
427
|
+
const handleScroll = (0, _react.useCallback)(event => {
|
|
428
|
+
// Inverted: the offset is the distance up from the newest message.
|
|
429
|
+
const away = event.nativeEvent.contentOffset.y > JUMP_TO_LATEST_OFFSET;
|
|
430
|
+
setAwayFrom(current => away ? current ?? new Set(rowIds.current) : null);
|
|
431
|
+
}, []);
|
|
432
|
+
const unseenReplies = (0, _react.useMemo)(() => {
|
|
433
|
+
if (!awayFrom) return 0;
|
|
434
|
+
let count = 0;
|
|
435
|
+
for (const {
|
|
436
|
+
message
|
|
437
|
+
} of rows) {
|
|
438
|
+
if (awayFrom.has(message.id)) break;
|
|
439
|
+
if (message.direction !== 'inbound' && message.messageType !== 'system') count += 1;
|
|
440
|
+
}
|
|
441
|
+
return count;
|
|
442
|
+
}, [awayFrom, rows]);
|
|
443
|
+
const handleJumpToLatest = (0, _react.useCallback)(() => scrollToBottom(), [scrollToBottom]);
|
|
444
|
+
|
|
445
|
+
// Android only: it detaches rows scrolled out of view, where a large photo
|
|
446
|
+
// still downloading would never finish and keep its turn. Keyed as in usePhotoSource.
|
|
447
|
+
const handleViewableItemsChanged = (0, _react.useCallback)(({
|
|
448
|
+
viewableItems
|
|
449
|
+
}) => largeImageQueue.setOnScreen(viewableItems.flatMap(({
|
|
450
|
+
item
|
|
451
|
+
}) => (item.message.attachments ?? []).map(attachment => attachment.id || attachment.url))), [largeImageQueue]);
|
|
452
|
+
const renderRow = (0, _react.useCallback)(({
|
|
453
|
+
item: {
|
|
454
|
+
message,
|
|
455
|
+
startsDay,
|
|
456
|
+
startsRun
|
|
457
|
+
}
|
|
458
|
+
}) => {
|
|
459
|
+
const isAgentMessage = message.direction !== 'inbound' && message.messageType !== 'system';
|
|
460
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
461
|
+
style: _ChatViewStyles.styles.threadRow,
|
|
462
|
+
children: [startsDay ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
463
|
+
style: _ChatViewStyles.styles.daySeparator,
|
|
464
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
465
|
+
style: _ChatViewStyles.styles.dayLine
|
|
466
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
467
|
+
style: _ChatViewStyles.styles.dayLabel,
|
|
468
|
+
children: (0, _conversations.formatDayLabel)(message.createdAt, locale, labels)
|
|
469
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
470
|
+
style: _ChatViewStyles.styles.dayLine
|
|
471
|
+
})]
|
|
472
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_ErrorBoundary.WidgetErrorBoundary, {
|
|
473
|
+
fallback: null,
|
|
474
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(MessageBubble, {
|
|
475
|
+
message: message,
|
|
476
|
+
color: color,
|
|
477
|
+
labels: labels,
|
|
478
|
+
onOpenImage: setViewerGallery,
|
|
479
|
+
onRateConversation: onRateConversation,
|
|
480
|
+
locales: locales,
|
|
481
|
+
senderLabel: isAgentMessage && (startsRun || startsDay) ? message.senderName || agentName || labels.team : null,
|
|
482
|
+
timeLabel: (0, _conversations.formatClockTime)(message.createdAt, locale),
|
|
483
|
+
receiptLabel: message.id === thread.lastVisitorMessageId ? receiptLabel(message, labels) : undefined,
|
|
484
|
+
onSubmitProcedureForm: message.id === thread.procedureFormMessageId ? handleSubmitProcedureForm : undefined,
|
|
485
|
+
onAgentUiAction: thread.agentBlocksShown.has(message.id) ? handleAgentUiAction : undefined,
|
|
486
|
+
onRetry: onRetryMessage && (0, _conversations.canRetryMessage)(message) ? handleRetryMessage : undefined,
|
|
487
|
+
photoAlbums: photoAlbums
|
|
488
|
+
})
|
|
489
|
+
})]
|
|
490
|
+
});
|
|
491
|
+
}, [agentName, color, handleAgentUiAction, handleRetryMessage, handleSubmitProcedureForm, labels, locale, locales, onRateConversation, onRetryMessage, photoAlbums, thread]);
|
|
492
|
+
const loadEarlierRow = hasMoreMessages ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
493
|
+
onPress: handleLoadEarlier,
|
|
494
|
+
accessibilityRole: "button",
|
|
495
|
+
style: _ChatViewStyles.styles.loadMore,
|
|
496
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
497
|
+
style: [_ChatViewStyles.styles.loadMoreLabel, {
|
|
498
|
+
color
|
|
499
|
+
}],
|
|
500
|
+
children: loading ? labels.loading : labels.loadEarlier
|
|
501
|
+
})
|
|
502
|
+
}) : null;
|
|
503
|
+
|
|
504
|
+
// Under the newest message.
|
|
505
|
+
const threadTail = awaitingTeammate || agentTyping ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
506
|
+
style: _ChatViewStyles.styles.threadTail,
|
|
507
|
+
children: [awaitingTeammate ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
508
|
+
style: _ChatViewStyles.styles.waitingRow,
|
|
509
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TeamAvatars.TeamAvatars, {
|
|
510
|
+
avatars: teamAvatars,
|
|
511
|
+
size: 20,
|
|
512
|
+
color: color
|
|
513
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
514
|
+
style: _ChatViewStyles.styles.waitingText,
|
|
515
|
+
children: labels.waitingForTeammate
|
|
516
|
+
})]
|
|
517
|
+
}) : null, agentTyping ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
518
|
+
style: _ChatViewStyles.styles.typingRow,
|
|
519
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
520
|
+
style: _ChatViewStyles.styles.typingBubble,
|
|
521
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
522
|
+
index: 0,
|
|
523
|
+
reduceMotion: reduceMotion
|
|
524
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
525
|
+
index: 1,
|
|
526
|
+
reduceMotion: reduceMotion
|
|
527
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
528
|
+
index: 2,
|
|
529
|
+
reduceMotion: reduceMotion
|
|
530
|
+
})]
|
|
531
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
532
|
+
style: _ChatViewStyles.styles.typingLabel,
|
|
533
|
+
children: labels.thinking
|
|
534
|
+
})]
|
|
535
|
+
}) : null]
|
|
536
|
+
}) : null;
|
|
286
537
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
287
538
|
style: _global.globalStyles.screen,
|
|
288
539
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_Gradient.GradientBackground, {
|
|
@@ -387,29 +638,76 @@ function ChatView({
|
|
|
387
638
|
color: headerTextColor
|
|
388
639
|
})
|
|
389
640
|
})]
|
|
390
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.
|
|
391
|
-
ref: scrollRef,
|
|
641
|
+
}), messages.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
392
642
|
style: _global.globalStyles.fill,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
643
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(ImageQueueContext.Provider, {
|
|
644
|
+
value: largeImageQueue,
|
|
645
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
646
|
+
ref: listRef,
|
|
647
|
+
data: rows,
|
|
648
|
+
inverted: true,
|
|
649
|
+
keyExtractor: rowKey,
|
|
650
|
+
renderItem: renderRow,
|
|
651
|
+
style: _global.globalStyles.fill,
|
|
652
|
+
contentContainerStyle: _ChatViewStyles.styles.threadContent,
|
|
653
|
+
ItemSeparatorComponent: ThreadGap
|
|
654
|
+
// Inverted, the header sits under the newest message and the footer
|
|
655
|
+
// above the oldest.
|
|
656
|
+
,
|
|
657
|
+
ListHeaderComponent: threadTail,
|
|
658
|
+
ListHeaderComponentStyle: _ChatViewStyles.styles.threadTailInList,
|
|
659
|
+
ListFooterComponent: loadEarlierRow,
|
|
660
|
+
onEndReached: handleLoadEarlier,
|
|
661
|
+
onEndReachedThreshold: 0.5,
|
|
662
|
+
onScroll: handleScroll,
|
|
663
|
+
scrollEventThrottle: 100
|
|
664
|
+
// A reply arriving below doesn't move what a visitor reads further
|
|
665
|
+
// up; near the newest message the list follows it into view.
|
|
666
|
+
,
|
|
667
|
+
maintainVisibleContentPosition: {
|
|
668
|
+
minIndexForVisible: 0,
|
|
669
|
+
autoscrollToTopThreshold: JUMP_TO_LATEST_OFFSET
|
|
670
|
+
},
|
|
671
|
+
onViewableItemsChanged: _reactNative.Platform.OS === 'android' ? handleViewableItemsChanged : undefined,
|
|
672
|
+
keyboardShouldPersistTaps: "handled"
|
|
673
|
+
// Interactive dismissal drags the keyboard down under the finger while
|
|
674
|
+
// the composer stays where the last keyboard event left it, which opens
|
|
675
|
+
// a gap under the thread; on-drag hands us a normal hide event instead.
|
|
676
|
+
,
|
|
677
|
+
keyboardDismissMode: "on-drag"
|
|
678
|
+
})
|
|
679
|
+
}), awayFrom ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
680
|
+
onPress: handleJumpToLatest,
|
|
404
681
|
accessibilityRole: "button",
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
682
|
+
accessibilityLabel: unseenReplies ? (0, _i18n.formatLabel)(labels.jumpToLatestNew, {
|
|
683
|
+
count: unseenReplies
|
|
684
|
+
}) : labels.jumpToLatest,
|
|
685
|
+
hitSlop: 8,
|
|
686
|
+
style: ({
|
|
687
|
+
pressed
|
|
688
|
+
}) => [_ChatViewStyles.styles.jumpToLatest, pressed && _global.globalStyles.pressed],
|
|
689
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.ChevronIcon, {
|
|
690
|
+
size: 18,
|
|
691
|
+
direction: "down",
|
|
692
|
+
color: _theme.palette.text
|
|
693
|
+
}), unseenReplies ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
694
|
+
style: [_ChatViewStyles.styles.jumpBadge, {
|
|
695
|
+
backgroundColor: color
|
|
409
696
|
}],
|
|
410
|
-
children:
|
|
411
|
-
|
|
412
|
-
|
|
697
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
698
|
+
style: [_ChatViewStyles.styles.jumpBadgeText, {
|
|
699
|
+
color: (0, _theme.contrastingTextColor)(color)
|
|
700
|
+
}],
|
|
701
|
+
children: unseenReplies > 9 ? '9+' : unseenReplies
|
|
702
|
+
})
|
|
703
|
+
}) : null]
|
|
704
|
+
}) : null]
|
|
705
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
|
|
706
|
+
style: _global.globalStyles.fill,
|
|
707
|
+
contentContainerStyle: [_ChatViewStyles.styles.messagesContent, isEmptyThread && _ChatViewStyles.styles.messagesContentEmpty, isLoadingThread && _ChatViewStyles.styles.messagesContentLoading],
|
|
708
|
+
keyboardShouldPersistTaps: "handled",
|
|
709
|
+
keyboardDismissMode: "on-drag",
|
|
710
|
+
children: [loadEarlierRow, isLoadingThread ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_MessagesSkeleton.MessagesSkeleton, {
|
|
413
711
|
color: color,
|
|
414
712
|
label: labels.loading
|
|
415
713
|
}) : null, isEmptyThread ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
@@ -426,69 +724,7 @@ function ChatView({
|
|
|
426
724
|
style: _ChatViewStyles.styles.emptyText,
|
|
427
725
|
children: labels.emptyConversation
|
|
428
726
|
})]
|
|
429
|
-
}) : null,
|
|
430
|
-
const previous = index > 0 ? messages[index - 1] : undefined;
|
|
431
|
-
const startsDay = !previous || !(0, _conversations.isSameDay)(message.createdAt, previous.createdAt);
|
|
432
|
-
const isAgentMessage = message.direction !== 'inbound' && message.messageType !== 'system';
|
|
433
|
-
// Name only at the top of a run, the way every messenger does it.
|
|
434
|
-
const startsRun = !previous || previous.direction === 'inbound' || previous.messageType === 'system' || previous.senderName !== message.senderName;
|
|
435
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_react.Fragment, {
|
|
436
|
-
children: [startsDay ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
437
|
-
style: _ChatViewStyles.styles.daySeparator,
|
|
438
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
439
|
-
style: _ChatViewStyles.styles.dayLine
|
|
440
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
441
|
-
style: _ChatViewStyles.styles.dayLabel,
|
|
442
|
-
children: (0, _conversations.formatDayLabel)(message.createdAt, locale, labels)
|
|
443
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
444
|
-
style: _ChatViewStyles.styles.dayLine
|
|
445
|
-
})]
|
|
446
|
-
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_ErrorBoundary.WidgetErrorBoundary, {
|
|
447
|
-
fallback: null,
|
|
448
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(MessageBubble, {
|
|
449
|
-
message: message,
|
|
450
|
-
color: color,
|
|
451
|
-
labels: labels,
|
|
452
|
-
onOpenImage: setViewerImage,
|
|
453
|
-
onRateConversation: onRateConversation,
|
|
454
|
-
locales: locales,
|
|
455
|
-
senderLabel: isAgentMessage && (startsRun || startsDay) ? message.senderName || agentName || labels.team : null,
|
|
456
|
-
timeLabel: (0, _conversations.formatClockTime)(message.createdAt, locale),
|
|
457
|
-
receiptLabel: message.id === thread.lastVisitorMessageId ? receiptLabel(message, labels) : undefined,
|
|
458
|
-
onSubmitProcedureForm: message.id === thread.procedureFormMessageId ? handleSubmitProcedureForm : undefined,
|
|
459
|
-
onAgentUiAction: thread.agentBlocksShown.has(message.id) ? handleAgentUiAction : undefined
|
|
460
|
-
})
|
|
461
|
-
})]
|
|
462
|
-
}, message.id);
|
|
463
|
-
}), awaitingTeammate ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
464
|
-
style: _ChatViewStyles.styles.waitingRow,
|
|
465
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TeamAvatars.TeamAvatars, {
|
|
466
|
-
avatars: teamAvatars,
|
|
467
|
-
size: 20,
|
|
468
|
-
color: color
|
|
469
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
470
|
-
style: _ChatViewStyles.styles.waitingText,
|
|
471
|
-
children: labels.waitingForTeammate
|
|
472
|
-
})]
|
|
473
|
-
}) : null, agentTyping ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
474
|
-
style: _ChatViewStyles.styles.typingRow,
|
|
475
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
476
|
-
style: _ChatViewStyles.styles.typingBubble,
|
|
477
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
478
|
-
index: 0,
|
|
479
|
-
reduceMotion: reduceMotion
|
|
480
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
481
|
-
index: 1,
|
|
482
|
-
reduceMotion: reduceMotion
|
|
483
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
484
|
-
index: 2,
|
|
485
|
-
reduceMotion: reduceMotion
|
|
486
|
-
})]
|
|
487
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
488
|
-
style: _ChatViewStyles.styles.typingLabel,
|
|
489
|
-
children: labels.thinking
|
|
490
|
-
})]
|
|
491
|
-
}) : null]
|
|
727
|
+
}) : null, threadTail]
|
|
492
728
|
}), offlineMessage ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
493
729
|
style: _ChatViewStyles.styles.offlineBanner,
|
|
494
730
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -505,10 +741,18 @@ function ChatView({
|
|
|
505
741
|
}) : null, drafts.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AttachmentChips.AttachmentChips, {
|
|
506
742
|
attachments: drafts,
|
|
507
743
|
disabled: sending,
|
|
508
|
-
|
|
744
|
+
labels: labels.attach,
|
|
509
745
|
onRemove: id => {
|
|
746
|
+
uploads.current.get(id)?.cancel();
|
|
747
|
+
uploads.current.delete(id);
|
|
510
748
|
setDrafts(current => current.filter(draft => draft.id !== id));
|
|
511
749
|
showNotice(null);
|
|
750
|
+
},
|
|
751
|
+
onRetry: id => {
|
|
752
|
+
const draft = drafts.find(item => item.id === id);
|
|
753
|
+
if (!draft) return;
|
|
754
|
+
showNotice(null);
|
|
755
|
+
startUpload(draft);
|
|
512
756
|
}
|
|
513
757
|
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
514
758
|
style: [_ChatViewStyles.styles.composer, showAttach && _ChatViewStyles.styles.composerWithAttach, composerFocused && _ChatViewStyles.styles.composerFocused, composerFocused && {
|
|
@@ -528,7 +772,7 @@ function ChatView({
|
|
|
528
772
|
style: ({
|
|
529
773
|
pressed
|
|
530
774
|
}) => [_ChatViewStyles.styles.attachButton, (pressed || sending || picking) && _global.globalStyles.pressed],
|
|
531
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.
|
|
775
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.PlusIcon, {
|
|
532
776
|
size: 20,
|
|
533
777
|
color: _theme.palette.textMuted
|
|
534
778
|
})
|
|
@@ -546,6 +790,7 @@ function ChatView({
|
|
|
546
790
|
onPress: handleSend,
|
|
547
791
|
disabled: !canSend,
|
|
548
792
|
accessibilityRole: "button",
|
|
793
|
+
accessibilityLabel: labels.send,
|
|
549
794
|
accessibilityState: {
|
|
550
795
|
disabled: !canSend
|
|
551
796
|
},
|
|
@@ -568,11 +813,11 @@ function ChatView({
|
|
|
568
813
|
onSelect: handleMenuSelect,
|
|
569
814
|
onClose: () => setMenuOpen(false)
|
|
570
815
|
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImageViewer.ImageViewer, {
|
|
571
|
-
|
|
816
|
+
gallery: viewerGallery,
|
|
572
817
|
labels: labels.viewer,
|
|
573
|
-
onClose: () =>
|
|
818
|
+
onClose: () => setViewerGallery(null),
|
|
574
819
|
onOpenExternally: url => {
|
|
575
|
-
|
|
820
|
+
setViewerGallery(null);
|
|
576
821
|
void (0, _safeUrl.openExternalUrl)(url);
|
|
577
822
|
}
|
|
578
823
|
})]
|
|
@@ -594,7 +839,9 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
594
839
|
onAgentUiAction,
|
|
595
840
|
onOpenImage,
|
|
596
841
|
onRateConversation,
|
|
597
|
-
locales
|
|
842
|
+
locales,
|
|
843
|
+
onRetry,
|
|
844
|
+
photoAlbums
|
|
598
845
|
}) {
|
|
599
846
|
const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
|
|
600
847
|
if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
|
|
@@ -657,6 +904,56 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
657
904
|
const procedureForm = !isVisitor ? (0, _agentUiBlocks.getProcedureForm)(message.metadata) : null;
|
|
658
905
|
const agentUiBlocks = !isVisitor ? (0, _agentUiBlocks.getAgentUiBlocks)(message.metadata) : [];
|
|
659
906
|
const textColor = isVisitor ? _theme.palette.white : _theme.palette.ink;
|
|
907
|
+
// A reply with images between its paragraphs shows them where the teammate put them.
|
|
908
|
+
const inlineParts = isVisitor ? null : (0, _inlineAttachments.splitInlineAttachments)(message.body, message.attachments);
|
|
909
|
+
const files = inlineParts ? inlineParts.files : message.attachments ?? [];
|
|
910
|
+
// Two or more photos share one album; any other files follow it.
|
|
911
|
+
const photos = files.filter(attachment => (0, _pickers.isImageAttachment)(attachment.contentType));
|
|
912
|
+
const album = photoAlbums && photos.length > 1 ? photos : null;
|
|
913
|
+
const sentAt = Date.parse(message.createdAt) || 0;
|
|
914
|
+
// A tap on one of the message's photos opens them all, at that one, in the
|
|
915
|
+
// order the bubble shows them.
|
|
916
|
+
const openPhotos = tapped => {
|
|
917
|
+
const photos = [...(inlineParts?.parts.flatMap(part => 'attachment' in part ? [part.attachment] : []) ?? []), ...files].filter(attachment => (0, _pickers.isImageAttachment)(attachment.contentType)).map(attachment => {
|
|
918
|
+
const url = attachment.id === tapped.id ? tapped.url : (0, _localPreviews.localPreviewFor)(attachment.url) || attachment.url;
|
|
919
|
+
// The phone's copy may be gone by now; the stored one stands in.
|
|
920
|
+
const fallbackUrl = url !== attachment.url && (0, _safeUrl.isHttpUrl)(attachment.url) ? attachment.url : undefined;
|
|
921
|
+
return {
|
|
922
|
+
id: attachment.id,
|
|
923
|
+
url,
|
|
924
|
+
name: attachment.name,
|
|
925
|
+
large: isLargeDownload(url, attachment.size),
|
|
926
|
+
fallbackUrl
|
|
927
|
+
};
|
|
928
|
+
}).filter(photo => !!photo.url);
|
|
929
|
+
const index = photos.findIndex(photo => photo.id === tapped.id);
|
|
930
|
+
onOpenImage(index < 0 ? {
|
|
931
|
+
images: [tapped],
|
|
932
|
+
index: 0
|
|
933
|
+
} : {
|
|
934
|
+
images: photos.map(({
|
|
935
|
+
url,
|
|
936
|
+
name,
|
|
937
|
+
large,
|
|
938
|
+
fallbackUrl
|
|
939
|
+
}) => ({
|
|
940
|
+
url,
|
|
941
|
+
name,
|
|
942
|
+
large,
|
|
943
|
+
fallbackUrl
|
|
944
|
+
})),
|
|
945
|
+
index
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
const renderAttachment = attachment => /*#__PURE__*/(0, _jsxRuntime.jsx)(AttachmentPreview, {
|
|
949
|
+
loadPriority: sentAt,
|
|
950
|
+
attachment: attachment,
|
|
951
|
+
isVisitor: isVisitor,
|
|
952
|
+
color: color,
|
|
953
|
+
voiceLabels: labels.voice,
|
|
954
|
+
openLabel: labels.openAttachment,
|
|
955
|
+
onOpenImage: openPhotos
|
|
956
|
+
}, attachment.id);
|
|
660
957
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
661
958
|
style: [_ChatViewStyles.styles.messageRow, isVisitor ? _ChatViewStyles.styles.messageRowRight : _ChatViewStyles.styles.messageRowLeft],
|
|
662
959
|
children: [senderLabel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -666,7 +963,13 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
666
963
|
style: [_ChatViewStyles.styles.bubble, isVisitor ? [_ChatViewStyles.styles.bubbleVisitor, {
|
|
667
964
|
backgroundColor: color
|
|
668
965
|
}] : _ChatViewStyles.styles.bubbleAgent, rtl && (isVisitor ? _ChatViewStyles.styles.bubbleVisitorRTL : _ChatViewStyles.styles.bubbleAgentRTL), message.status === 'pending' && _ChatViewStyles.styles.bubblePending],
|
|
669
|
-
children: [
|
|
966
|
+
children: [inlineParts ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
967
|
+
style: _ChatViewStyles.styles.inlineParts,
|
|
968
|
+
children: inlineParts.parts.map((part, index) => 'text' in part ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Markdown.Markdown, {
|
|
969
|
+
content: part.text,
|
|
970
|
+
color: textColor
|
|
971
|
+
}, `text-${index}`) : renderAttachment(part.attachment))
|
|
972
|
+
}) : hasBody ? isVisitor ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
670
973
|
style: [_ChatViewStyles.styles.bubbleText, {
|
|
671
974
|
color: textColor
|
|
672
975
|
}],
|
|
@@ -674,21 +977,14 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
674
977
|
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_Markdown.Markdown, {
|
|
675
978
|
content: messageText,
|
|
676
979
|
color: textColor
|
|
677
|
-
}) : null,
|
|
678
|
-
style: [_ChatViewStyles.styles.attachments, hasBody && _ChatViewStyles.styles.attachmentsSpaced],
|
|
679
|
-
children:
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
onOpenImage: onOpenImage
|
|
686
|
-
}, attachment.id))
|
|
687
|
-
}) : null, message.status === 'failed' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
688
|
-
style: [_ChatViewStyles.styles.failedText, {
|
|
689
|
-
color: isVisitor ? (0, _theme.withAlpha)(_theme.palette.white, 0.7) : _theme.palette.danger
|
|
690
|
-
}],
|
|
691
|
-
children: labels.failedToSend
|
|
980
|
+
}) : null, files.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
981
|
+
style: [_ChatViewStyles.styles.attachments, (hasBody || !!inlineParts) && _ChatViewStyles.styles.attachmentsSpaced],
|
|
982
|
+
children: [album ? /*#__PURE__*/(0, _jsxRuntime.jsx)(PhotoAlbum, {
|
|
983
|
+
photos: album,
|
|
984
|
+
loadPriority: sentAt,
|
|
985
|
+
moreLabel: labels.morePhotos,
|
|
986
|
+
onOpen: openPhotos
|
|
987
|
+
}) : null, (album ? files.filter(attachment => !album.includes(attachment)) : files).map(renderAttachment)]
|
|
692
988
|
}) : null, timeLabel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
693
989
|
style: [_ChatViewStyles.styles.messageTime, {
|
|
694
990
|
color: isVisitor ? (0, _theme.withAlpha)(_theme.palette.white, 0.7) : _theme.palette.textSubtle
|
|
@@ -698,12 +994,28 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
698
994
|
}), receiptLabel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
699
995
|
style: _ChatViewStyles.styles.receiptText,
|
|
700
996
|
children: receiptLabel
|
|
997
|
+
}) : null, message.status === 'failed' ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
998
|
+
onPress: onRetry ? () => onRetry(message.id) : undefined,
|
|
999
|
+
disabled: !onRetry,
|
|
1000
|
+
accessibilityRole: onRetry ? 'button' : 'text',
|
|
1001
|
+
hitSlop: 8,
|
|
1002
|
+
style: ({
|
|
1003
|
+
pressed
|
|
1004
|
+
}) => [_ChatViewStyles.styles.failedRow, pressed && _global.globalStyles.pressed],
|
|
1005
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.AlertIcon, {
|
|
1006
|
+
size: 12,
|
|
1007
|
+
color: _theme.palette.danger
|
|
1008
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
1009
|
+
style: _ChatViewStyles.styles.failedText,
|
|
1010
|
+
children: onRetry ? `${labels.failedToSend} · ${labels.tapToRetry}` : labels.failedToSend
|
|
1011
|
+
})]
|
|
701
1012
|
}) : null, agentUiBlocks.length > 0 && onAgentUiAction ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AgentUiBlocks.AgentUiBlocks, {
|
|
702
1013
|
blocks: agentUiBlocks,
|
|
703
1014
|
color: color,
|
|
704
1015
|
labels: labels.blocks,
|
|
705
1016
|
onAction: onAgentUiAction
|
|
706
1017
|
}) : null, agentUiBlocks.length === 0 && procedureForm && onSubmitProcedureForm ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ProcedureFieldForm, {
|
|
1018
|
+
messageId: message.id,
|
|
707
1019
|
form: procedureForm,
|
|
708
1020
|
color: color,
|
|
709
1021
|
labels: labels,
|
|
@@ -717,26 +1029,196 @@ function receiptLabel(message, labels) {
|
|
|
717
1029
|
if (message.deletedAt || message.status === 'pending' || message.status === 'failed') return undefined;
|
|
718
1030
|
return message.status === 'read' ? labels.messageSeen : labels.messageSent;
|
|
719
1031
|
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Where a photo shows from, and whether it may load yet. `url` is empty when
|
|
1035
|
+
* the server sent a non-http(s) URL (see resolveUrl). The visitor's own
|
|
1036
|
+
* attachment points at the picked file until the server copy replaces it (or
|
|
1037
|
+
* is empty without one). A photo they sent this session keeps showing from
|
|
1038
|
+
* the phone after that: the stored copy may not load yet. Whichever fails to
|
|
1039
|
+
* load gives way to the next.
|
|
1040
|
+
*/
|
|
1041
|
+
function usePhotoSource(attachment, loadPriority) {
|
|
1042
|
+
const [failedUrls, setFailedUrls] = (0, _react.useState)([]);
|
|
1043
|
+
const [loadedUrl, setLoadedUrl] = (0, _react.useState)(null);
|
|
1044
|
+
const url = [(0, _localPreviews.localPreviewFor)(attachment.url), attachment.url].find(candidate => !!candidate && !failedUrls.includes(candidate));
|
|
1045
|
+
const isPhoto = (0, _pickers.isImageAttachment)(attachment.contentType) && !!url;
|
|
1046
|
+
// A large download waits its turn; small photos and the phone's own copy don't.
|
|
1047
|
+
const [turn, settled] = useImageTurn(attachment.id || attachment.url, loadPriority, isPhoto && isLargeDownload(url, attachment.size));
|
|
1048
|
+
return {
|
|
1049
|
+
url,
|
|
1050
|
+
isPhoto,
|
|
1051
|
+
turn,
|
|
1052
|
+
/** Waiting for its turn or still downloading; the next URL after a failure starts over. */
|
|
1053
|
+
loading: isPhoto && loadedUrl !== url,
|
|
1054
|
+
onLoad: () => {
|
|
1055
|
+
settled();
|
|
1056
|
+
if (url) setLoadedUrl(url);
|
|
1057
|
+
},
|
|
1058
|
+
onError: () => {
|
|
1059
|
+
settled();
|
|
1060
|
+
if (!url) return;
|
|
1061
|
+
setFailedUrls(current => [...current, url]);
|
|
1062
|
+
// The viewer and the message's other views stop offering it too.
|
|
1063
|
+
if (url !== attachment.url && url === (0, _localPreviews.localPreviewFor)(attachment.url)) (0, _localPreviews.forgetLocalPreview)(attachment.url);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Several photos in one message, the way WhatsApp shows them: one square,
|
|
1070
|
+
* two side by side, three as one tall beside two stacked, four in a grid, and
|
|
1071
|
+
* past four the last tile covered with how many more there are.
|
|
1072
|
+
*/
|
|
1073
|
+
function PhotoAlbum({
|
|
1074
|
+
photos,
|
|
1075
|
+
loadPriority,
|
|
1076
|
+
moreLabel,
|
|
1077
|
+
onOpen
|
|
1078
|
+
}) {
|
|
1079
|
+
const shown = photos.slice(0, 4);
|
|
1080
|
+
// The covered tile counts too: it's no more visible than the ones after it.
|
|
1081
|
+
const more = photos.length > 4 ? photos.length - 3 : 0;
|
|
1082
|
+
const tile = (attachment, index, size) => /*#__PURE__*/(0, _jsxRuntime.jsx)(PhotoTile, {
|
|
1083
|
+
attachment: attachment,
|
|
1084
|
+
loadPriority: loadPriority,
|
|
1085
|
+
style: size,
|
|
1086
|
+
more: index === 3 ? more : 0,
|
|
1087
|
+
moreLabel: moreLabel,
|
|
1088
|
+
onOpen: onOpen
|
|
1089
|
+
}, attachment.id);
|
|
1090
|
+
if (shown.length === 2) {
|
|
1091
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1092
|
+
style: [_ChatViewStyles.styles.album, _ChatViewStyles.styles.albumRow],
|
|
1093
|
+
children: shown.map((photo, i) => tile(photo, i, _ChatViewStyles.styles.albumHalf))
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
if (shown.length === 3) {
|
|
1097
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
1098
|
+
style: [_ChatViewStyles.styles.album, _ChatViewStyles.styles.albumRow],
|
|
1099
|
+
children: [tile(shown[0], 0, _ChatViewStyles.styles.albumHalf), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1100
|
+
style: _ChatViewStyles.styles.albumColumn,
|
|
1101
|
+
children: shown.slice(1).map((photo, i) => tile(photo, i + 1, _ChatViewStyles.styles.albumQuarter))
|
|
1102
|
+
})]
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1106
|
+
style: [_ChatViewStyles.styles.album, _ChatViewStyles.styles.albumGrid],
|
|
1107
|
+
children: shown.map((photo, i) => tile(photo, i, _ChatViewStyles.styles.albumQuarter))
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
function PhotoTile({
|
|
1111
|
+
attachment,
|
|
1112
|
+
loadPriority,
|
|
1113
|
+
style,
|
|
1114
|
+
more,
|
|
1115
|
+
moreLabel,
|
|
1116
|
+
onOpen
|
|
1117
|
+
}) {
|
|
1118
|
+
const photo = usePhotoSource(attachment, loadPriority);
|
|
1119
|
+
// One that can't load still opens, so the viewer can offer the browser.
|
|
1120
|
+
const openUrl = photo.url ?? ((0, _safeUrl.isHttpUrl)(attachment.url) ? attachment.url : undefined);
|
|
1121
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
1122
|
+
onPress: openUrl ? () => onOpen({
|
|
1123
|
+
id: attachment.id,
|
|
1124
|
+
url: openUrl,
|
|
1125
|
+
name: attachment.name
|
|
1126
|
+
}) : undefined,
|
|
1127
|
+
accessibilityRole: "imagebutton",
|
|
1128
|
+
accessibilityLabel: more > 0 ? `${attachment.name}, ${(0, _i18n.formatLabel)(moreLabel, {
|
|
1129
|
+
count: more
|
|
1130
|
+
})}` : attachment.name,
|
|
1131
|
+
style: ({
|
|
1132
|
+
pressed
|
|
1133
|
+
}) => [_ChatViewStyles.styles.albumTile, style, pressed && _global.globalStyles.pressed],
|
|
1134
|
+
children: [photo.isPhoto && photo.turn ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
1135
|
+
source: {
|
|
1136
|
+
uri: photo.url
|
|
1137
|
+
},
|
|
1138
|
+
style: _ChatViewStyles.styles.albumImage,
|
|
1139
|
+
resizeMode: "cover"
|
|
1140
|
+
// Decoded at the tile's size, like a single photo.
|
|
1141
|
+
,
|
|
1142
|
+
resizeMethod: "resize",
|
|
1143
|
+
accessibilityIgnoresInvertColors: true,
|
|
1144
|
+
onLoad: photo.onLoad,
|
|
1145
|
+
onError: photo.onError
|
|
1146
|
+
}) : null, !photo.isPhoto ?
|
|
1147
|
+
/*#__PURE__*/
|
|
1148
|
+
// Nothing left to load (an expired link): its type and name, not a blank square.
|
|
1149
|
+
(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
1150
|
+
style: _ChatViewStyles.styles.albumFallback,
|
|
1151
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
1152
|
+
style: _ChatViewStyles.styles.albumFallbackType,
|
|
1153
|
+
children: (0, _FileCard.fileTypeLabel)(attachment.name, attachment.contentType)
|
|
1154
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
1155
|
+
style: _ChatViewStyles.styles.albumFallbackName,
|
|
1156
|
+
numberOfLines: 2,
|
|
1157
|
+
children: attachment.name
|
|
1158
|
+
})]
|
|
1159
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(PhotoLoader, {
|
|
1160
|
+
loading: photo.loading && more === 0
|
|
1161
|
+
}), more > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1162
|
+
style: _ChatViewStyles.styles.albumMore,
|
|
1163
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
1164
|
+
style: _ChatViewStyles.styles.albumMoreText,
|
|
1165
|
+
children: ["+", more]
|
|
1166
|
+
})
|
|
1167
|
+
}) : null]
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Spins over a photo until it has loaded. Held back a moment, so a photo that
|
|
1173
|
+
* comes straight from the cache never flashes it.
|
|
1174
|
+
*/
|
|
1175
|
+
function PhotoLoader({
|
|
1176
|
+
loading
|
|
1177
|
+
}) {
|
|
1178
|
+
const [due, setDue] = (0, _react.useState)(false);
|
|
1179
|
+
(0, _react.useEffect)(() => {
|
|
1180
|
+
if (!loading) return undefined;
|
|
1181
|
+
const timer = setTimeout(() => setDue(true), PHOTO_LOADER_DELAY_MS);
|
|
1182
|
+
return () => {
|
|
1183
|
+
clearTimeout(timer);
|
|
1184
|
+
setDue(false);
|
|
1185
|
+
};
|
|
1186
|
+
}, [loading]);
|
|
1187
|
+
if (!loading || !due) return null;
|
|
1188
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1189
|
+
style: _ChatViewStyles.styles.photoLoader,
|
|
1190
|
+
pointerEvents: "none",
|
|
1191
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1192
|
+
style: _ChatViewStyles.styles.photoLoaderBadge,
|
|
1193
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
|
|
1194
|
+
size: "small",
|
|
1195
|
+
color: _theme.palette.white
|
|
1196
|
+
})
|
|
1197
|
+
})
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
720
1200
|
function AttachmentPreview({
|
|
721
1201
|
attachment,
|
|
1202
|
+
loadPriority,
|
|
722
1203
|
isVisitor,
|
|
723
1204
|
color,
|
|
724
1205
|
voiceLabels,
|
|
725
1206
|
openLabel,
|
|
726
1207
|
onOpenImage
|
|
727
1208
|
}) {
|
|
728
|
-
// `url` is empty when the server sent a non-http(s) URL (see resolveUrl).
|
|
729
|
-
// An unsent attachment shows its local file (or data URL): it renders, but
|
|
730
|
-
// there is nothing to open until the server copy arrives.
|
|
731
|
-
const [imageFailed, setImageFailed] = (0, _react.useState)(false);
|
|
732
1209
|
const [audioFailed, setAudioFailed] = (0, _react.useState)(false);
|
|
733
|
-
const
|
|
1210
|
+
const photo = usePhotoSource(attachment, loadPriority);
|
|
1211
|
+
const imageUrl = photo.url;
|
|
1212
|
+
const isImage = photo.isPhoto;
|
|
734
1213
|
const openable = (0, _safeUrl.isHttpUrl)(attachment.url);
|
|
735
|
-
// Images open in the in-app viewer
|
|
736
|
-
|
|
737
|
-
|
|
1214
|
+
// Images open in the in-app viewer, a local photo too since it already
|
|
1215
|
+
// renders here. Other files (PDF, sheets) need the OS, which can only open
|
|
1216
|
+
// the server copy.
|
|
1217
|
+
const open = isImage ? () => onOpenImage({
|
|
1218
|
+
id: attachment.id,
|
|
1219
|
+
url: imageUrl,
|
|
738
1220
|
name: attachment.name
|
|
739
|
-
}) : () => void (0, _safeUrl.openExternalUrl)(attachment.url);
|
|
1221
|
+
}) : openable ? () => void (0, _safeUrl.openExternalUrl)(attachment.url) : undefined;
|
|
740
1222
|
|
|
741
1223
|
// Voice notes from the team (and any other audio) play in place, like the
|
|
742
1224
|
// web Messenger — when the app has expo-audio; otherwise the file row below.
|
|
@@ -753,23 +1235,35 @@ function AttachmentPreview({
|
|
|
753
1235
|
});
|
|
754
1236
|
}
|
|
755
1237
|
if (isImage) {
|
|
756
|
-
return /*#__PURE__*/(0, _jsxRuntime.
|
|
1238
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
757
1239
|
onPress: open,
|
|
758
|
-
disabled: !openable,
|
|
759
1240
|
accessibilityRole: "imagebutton",
|
|
760
1241
|
accessibilityLabel: attachment.name,
|
|
761
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
1242
|
+
children: [photo.turn ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
762
1243
|
source: {
|
|
763
|
-
uri:
|
|
1244
|
+
uri: imageUrl
|
|
764
1245
|
},
|
|
765
1246
|
style: _ChatViewStyles.styles.attachmentImage,
|
|
766
|
-
resizeMode: "cover"
|
|
767
|
-
|
|
1247
|
+
resizeMode: "cover"
|
|
1248
|
+
// Android otherwise decodes a server image at full size: two 20 MP
|
|
1249
|
+
// photos fill its shared bitmap budget, and every later image in
|
|
1250
|
+
// the app fails to load. Decoded at the bubble's size instead.
|
|
1251
|
+
,
|
|
1252
|
+
resizeMethod: "resize",
|
|
1253
|
+
accessibilityIgnoresInvertColors: true,
|
|
1254
|
+
onLoad: photo.onLoad
|
|
768
1255
|
// An image that can't load (expired link, deleted file) falls back to
|
|
769
1256
|
// the file row below instead of an empty box.
|
|
770
1257
|
,
|
|
771
|
-
onError:
|
|
772
|
-
})
|
|
1258
|
+
onError: photo.onError
|
|
1259
|
+
}) :
|
|
1260
|
+
/*#__PURE__*/
|
|
1261
|
+
// Same box as the image, so nothing moves when its turn comes.
|
|
1262
|
+
(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1263
|
+
style: _ChatViewStyles.styles.attachmentImage
|
|
1264
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(PhotoLoader, {
|
|
1265
|
+
loading: photo.loading
|
|
1266
|
+
})]
|
|
773
1267
|
});
|
|
774
1268
|
}
|
|
775
1269
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_FileCard.FileCard, {
|
|
@@ -783,14 +1277,15 @@ function AttachmentPreview({
|
|
|
783
1277
|
});
|
|
784
1278
|
}
|
|
785
1279
|
function ProcedureFieldForm({
|
|
1280
|
+
messageId,
|
|
786
1281
|
form,
|
|
787
1282
|
color,
|
|
788
1283
|
labels,
|
|
789
1284
|
onSubmit
|
|
790
1285
|
}) {
|
|
791
|
-
const [values, setValues] = (0,
|
|
792
|
-
const [submitted, setSubmitted] = (0,
|
|
793
|
-
const [expanded, setExpanded] = (0,
|
|
1286
|
+
const [values, setValues] = (0, _useRetainedState.useRetainedState)(`${messageId}:values`, {});
|
|
1287
|
+
const [submitted, setSubmitted] = (0, _useRetainedState.useRetainedState)(`${messageId}:submitted`, false);
|
|
1288
|
+
const [expanded, setExpanded] = (0, _useRetainedState.useRetainedState)(`${messageId}:expanded`, false);
|
|
794
1289
|
const canSubmit = form.fields.every(field => !field.required || values[field.name]?.trim());
|
|
795
1290
|
const handleSubmit = () => {
|
|
796
1291
|
if (!canSubmit || submitted) return;
|