@repzo/widget-react-native 1.3.1 → 1.5.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 +173 -5
- package/README.md +99 -13
- package/lib/commonjs/api/client.js +125 -28
- 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 +20 -29
- package/lib/commonjs/attachments/pickers.js.map +1 -1
- package/lib/commonjs/i18n.js +22 -3
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/locales.js +12 -3
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/user-update-queue.js +168 -0
- package/lib/commonjs/state/user-update-queue.js.map +1 -0
- package/lib/commonjs/state/widget-store.js +303 -33
- 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 +33 -13
- 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 +229 -24
- 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/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 +651 -165
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +138 -1
- 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/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 +123 -27
- 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 +15 -25
- package/lib/module/attachments/pickers.js.map +1 -1
- package/lib/module/i18n.js +22 -3
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/locales.js +12 -3
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/user-update-queue.js +163 -0
- package/lib/module/state/user-update-queue.js.map +1 -0
- package/lib/module/state/widget-store.js +305 -35
- 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 +33 -13
- 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 +231 -26
- 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/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 +655 -169
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +140 -2
- 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/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 +20 -6
- 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 +15 -2
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +12 -3
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +5 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/user-update-queue.d.ts +62 -0
- package/lib/typescript/commonjs/state/user-update-queue.d.ts.map +1 -0
- package/lib/typescript/commonjs/state/widget-store.d.ts +44 -1
- 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 +53 -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/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 +144 -0
- 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 +26 -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/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 +20 -6
- 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 +15 -2
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +12 -3
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +5 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/user-update-queue.d.ts +62 -0
- package/lib/typescript/module/state/user-update-queue.d.ts.map +1 -0
- package/lib/typescript/module/state/widget-store.d.ts +44 -1
- 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 +53 -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/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 +144 -0
- 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 +26 -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/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 +149 -38
- package/src/attachments/file-size.ts +42 -0
- package/src/attachments/local-previews.ts +32 -0
- package/src/attachments/pickers.ts +23 -25
- package/src/i18n.ts +22 -3
- package/src/index.tsx +5 -0
- package/src/locales.ts +12 -3
- package/src/state/user-update-queue.ts +178 -0
- package/src/state/widget-store.ts +346 -51
- package/src/storage.ts +2 -0
- package/src/types.ts +53 -0
- package/src/ui/WidgetProvider.tsx +35 -8
- package/src/ui/components/AttachmentChips.tsx +100 -29
- package/src/ui/components/ConversationRow.tsx +90 -9
- package/src/ui/components/ImageViewer.tsx +242 -24
- package/src/ui/components/RatingCard.tsx +5 -2
- package/src/ui/components/ZoomableImage.tsx +58 -5
- package/src/ui/hooks/use-retained-state.ts +35 -0
- package/src/ui/screens/chat/ChatView.styles.ts +139 -1
- package/src/ui/screens/chat/ChatView.tsx +735 -198
- package/src/ui/screens/chat/ChatView.types.ts +26 -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/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,11 +118,13 @@ function ChatView({
|
|
|
63
118
|
labels,
|
|
64
119
|
attachmentSources = [],
|
|
65
120
|
onPickAttachments,
|
|
121
|
+
onUploadAttachment,
|
|
66
122
|
onSendMessage,
|
|
67
123
|
onRetryMessage,
|
|
68
124
|
onIdentifyVisitor,
|
|
69
125
|
onRateConversation,
|
|
70
126
|
defaultLocale,
|
|
127
|
+
photoAlbums = true,
|
|
71
128
|
onLoadMore,
|
|
72
129
|
onTyping,
|
|
73
130
|
onBack,
|
|
@@ -77,30 +134,58 @@ function ChatView({
|
|
|
77
134
|
const [composerFocused, setComposerFocused] = (0, _react.useState)(false);
|
|
78
135
|
const [drafts, setDrafts] = (0, _react.useState)([]);
|
|
79
136
|
const [menuOpen, setMenuOpen] = (0, _react.useState)(false);
|
|
80
|
-
const [
|
|
137
|
+
const [viewerGallery, setViewerGallery] = (0, _react.useState)(null);
|
|
81
138
|
const [picking, setPicking] = (0, _react.useState)(false);
|
|
82
139
|
// Only attachment sends hold the composer; text sends stay fire-and-forget.
|
|
83
140
|
const [sending, setSending] = (0, _react.useState)(false);
|
|
84
141
|
const [notice, setNotice] = (0, _react.useState)(null);
|
|
85
142
|
const reduceMotion = (0, _useReduceMotion.useReduceMotion)();
|
|
86
|
-
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));
|
|
87
146
|
const typingStopTimer = (0, _react.useRef)(null);
|
|
88
147
|
const noticeTimer = (0, _react.useRef)(null);
|
|
89
148
|
const pickerTimer = (0, _react.useRef)(null);
|
|
90
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);
|
|
160
|
+
// Set by a tap on Send until the composer re-renders. Taps that pile up
|
|
161
|
+
// while the JS thread is busy (a slow phone, the host app at work) are all
|
|
162
|
+
// handled before React re-renders, so each still sees the typed text and
|
|
163
|
+
// would send it again. A ref changes at once: only the first goes out.
|
|
164
|
+
const sendTapped = (0, _react.useRef)(false);
|
|
165
|
+
|
|
166
|
+
// After every render, and after the send button has taken this render's
|
|
167
|
+
// handler (its own effect runs first): from here a tap sees the composer as
|
|
168
|
+
// the last send left it.
|
|
169
|
+
(0, _react.useEffect)(() => {
|
|
170
|
+
sendTapped.current = false;
|
|
171
|
+
});
|
|
91
172
|
(0, _react.useEffect)(() => {
|
|
92
173
|
if (prefillText) setInput(prefillText);
|
|
93
174
|
}, [prefillText]);
|
|
94
175
|
|
|
95
176
|
// The provider keys this screen by the store's thread generation. Pending
|
|
96
|
-
// picks and sends belong to this mount, even before a draft gets a
|
|
177
|
+
// picks, uploads and sends belong to this mount, even before a draft gets a
|
|
178
|
+
// server ID.
|
|
97
179
|
(0, _react.useEffect)(() => {
|
|
98
180
|
mounted.current = true;
|
|
181
|
+
const inFlight = uploads.current;
|
|
99
182
|
return () => {
|
|
100
183
|
mounted.current = false;
|
|
101
184
|
if (typingStopTimer.current) clearTimeout(typingStopTimer.current);
|
|
102
185
|
if (noticeTimer.current) clearTimeout(noticeTimer.current);
|
|
103
186
|
if (pickerTimer.current) clearTimeout(pickerTimer.current);
|
|
187
|
+
inFlight.forEach(upload => upload.cancel());
|
|
188
|
+
inFlight.clear();
|
|
104
189
|
};
|
|
105
190
|
}, []);
|
|
106
191
|
const showNotice = (0, _react.useCallback)(message => {
|
|
@@ -108,50 +193,66 @@ function ChatView({
|
|
|
108
193
|
setNotice(message);
|
|
109
194
|
if (message) noticeTimer.current = setTimeout(() => setNotice(null), 5000);
|
|
110
195
|
}, []);
|
|
196
|
+
|
|
197
|
+
// The list is inverted, so offset 0 is the newest message: the thread opens
|
|
198
|
+
// there without scrolling, stays there as the keyboard rises or replies
|
|
199
|
+
// arrive, and older pages land above it without moving the reader.
|
|
111
200
|
const scrollToBottom = (0, _react.useCallback)((animated = true) => {
|
|
112
|
-
|
|
201
|
+
listRef.current?.scrollToOffset({
|
|
202
|
+
offset: 0,
|
|
113
203
|
animated
|
|
114
204
|
});
|
|
115
205
|
}, []);
|
|
116
206
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// handler below covers later growth (loads, images, incoming messages).
|
|
120
|
-
(0, _react.useEffect)(() => {
|
|
121
|
-
const frame = requestAnimationFrame(() => scrollToBottom(false));
|
|
122
|
-
return () => cancelAnimationFrame(frame);
|
|
123
|
-
}, [scrollToBottom]);
|
|
124
|
-
|
|
125
|
-
// "Load earlier" PREPENDS history; skipping one auto-scroll keeps the reader
|
|
126
|
-
// anchored where they were instead of yanking them back to the bottom.
|
|
127
|
-
const skipNextAutoScroll = (0, _react.useRef)(false);
|
|
207
|
+
// Fires as the visitor nears the oldest message; the row up there is the
|
|
208
|
+
// fallback when a page fails to load.
|
|
128
209
|
const handleLoadEarlier = (0, _react.useCallback)(() => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}, [onLoadMore]);
|
|
132
|
-
const handleContentSizeChange = (0, _react.useCallback)(() => {
|
|
133
|
-
if (skipNextAutoScroll.current) {
|
|
134
|
-
skipNextAutoScroll.current = false;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
scrollToBottom(false);
|
|
138
|
-
}, [scrollToBottom]);
|
|
139
|
-
|
|
140
|
-
// The keyboard shortens the thread instead of scrolling it, so whatever was
|
|
141
|
-
// at the bottom — the newest message, an agent form — would slide in behind
|
|
142
|
-
// the composer. Re-pin on every shrink, unanimated, so the list tracks the
|
|
143
|
-
// keyboard as it rises rather than jumping once it lands.
|
|
144
|
-
const viewportHeight = (0, _react.useRef)(0);
|
|
145
|
-
const handleViewportLayout = (0, _react.useCallback)(event => {
|
|
146
|
-
const {
|
|
147
|
-
height
|
|
148
|
-
} = event.nativeEvent.layout;
|
|
149
|
-
const shrank = viewportHeight.current - height > 1;
|
|
150
|
-
viewportHeight.current = height;
|
|
151
|
-
if (shrank) scrollToBottom(false);
|
|
152
|
-
}, [scrollToBottom]);
|
|
210
|
+
if (hasMoreMessages && !loading) onLoadMore();
|
|
211
|
+
}, [hasMoreMessages, loading, onLoadMore]);
|
|
153
212
|
const showAttach = attachmentSources.length > 0 && !!onPickAttachments;
|
|
154
|
-
|
|
213
|
+
// Send waits until every picked file is up, so the message goes out at once.
|
|
214
|
+
const uploadsPending = drafts.some(draft => draft.uploadStatus === 'uploading' || draft.uploadStatus === 'failed');
|
|
215
|
+
const canSend = !sending && !uploadsPending && (input.trim().length > 0 || drafts.length > 0);
|
|
216
|
+
const updateDraft = (0, _react.useCallback)((id, patch) => setDrafts(current => current.map(draft => draft.id === id ? {
|
|
217
|
+
...draft,
|
|
218
|
+
...patch
|
|
219
|
+
} : draft)), []);
|
|
220
|
+
|
|
221
|
+
/** Puts a picked local file up while the visitor writes; inline files need nothing. */
|
|
222
|
+
const startUpload = draft => {
|
|
223
|
+
if (!onUploadAttachment || !draft.fileUri) return;
|
|
224
|
+
uploads.current.get(draft.id)?.cancel();
|
|
225
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
226
|
+
const attempt = {
|
|
227
|
+
cancel: () => controller?.abort()
|
|
228
|
+
};
|
|
229
|
+
uploads.current.set(draft.id, attempt);
|
|
230
|
+
updateDraft(draft.id, {
|
|
231
|
+
uploadStatus: 'uploading',
|
|
232
|
+
uploadToken: undefined,
|
|
233
|
+
uploadExpiresAt: undefined
|
|
234
|
+
});
|
|
235
|
+
onUploadAttachment(draft, controller?.signal).then(({
|
|
236
|
+
uploadToken,
|
|
237
|
+
expiresAt
|
|
238
|
+
}) => {
|
|
239
|
+
if (!mounted.current || uploads.current.get(draft.id) !== attempt) return;
|
|
240
|
+
uploads.current.delete(draft.id);
|
|
241
|
+
updateDraft(draft.id, {
|
|
242
|
+
uploadStatus: 'uploaded',
|
|
243
|
+
uploadToken,
|
|
244
|
+
uploadExpiresAt: expiresAt
|
|
245
|
+
});
|
|
246
|
+
}, error => {
|
|
247
|
+
if (!mounted.current || uploads.current.get(draft.id) !== attempt) return;
|
|
248
|
+
console.warn('[repzo-widget] attachment upload failed', error);
|
|
249
|
+
uploads.current.delete(draft.id);
|
|
250
|
+
updateDraft(draft.id, {
|
|
251
|
+
uploadStatus: 'failed'
|
|
252
|
+
});
|
|
253
|
+
showNotice(labels.attach.uploadFailedNotice);
|
|
254
|
+
});
|
|
255
|
+
};
|
|
155
256
|
const problemLabel = problem => {
|
|
156
257
|
switch (problem) {
|
|
157
258
|
case 'tooMany':
|
|
@@ -176,6 +277,8 @@ function ChatView({
|
|
|
176
277
|
if (!mounted.current) return;
|
|
177
278
|
if (result.added.length > 0) {
|
|
178
279
|
setDrafts(current => [...current, ...result.added].slice(0, _pickers.MAX_ATTACHMENTS));
|
|
280
|
+
// Opted out, the files go up when the visitor taps Send instead.
|
|
281
|
+
if ((0, _pickers.uploadsOnPick)()) result.added.forEach(startUpload);
|
|
179
282
|
}
|
|
180
283
|
showNotice(problemLabel(result.problem));
|
|
181
284
|
} finally {
|
|
@@ -211,23 +314,29 @@ function ChatView({
|
|
|
211
314
|
};
|
|
212
315
|
const handleSend = () => {
|
|
213
316
|
const trimmed = input.trim();
|
|
214
|
-
if (!canSend) return;
|
|
317
|
+
if (!canSend || sendTapped.current) return;
|
|
318
|
+
sendTapped.current = true;
|
|
215
319
|
setInput('');
|
|
216
320
|
if (typingStopTimer.current) clearTimeout(typingStopTimer.current);
|
|
217
321
|
onTyping(false);
|
|
218
|
-
|
|
322
|
+
followSend.current = true;
|
|
219
323
|
if (drafts.length === 0) {
|
|
220
324
|
// The failed bubble already surfaces a failure to the visitor.
|
|
221
325
|
onSendMessage(trimmed).catch(() => {});
|
|
222
326
|
return;
|
|
223
327
|
}
|
|
224
328
|
const sent = drafts;
|
|
329
|
+
// Named by its token while that lasts; otherwise the file goes up again
|
|
330
|
+
// as the message is sent.
|
|
331
|
+
const sendBy = Date.now() + UPLOAD_EXPIRY_MARGIN_MS;
|
|
332
|
+
const stillUploaded = draft => !!draft.uploadToken && (draft.uploadExpiresAt ?? Infinity) > sendBy;
|
|
225
333
|
setSending(true);
|
|
226
334
|
showNotice(null);
|
|
227
335
|
onSendMessage(trimmed, sent.map(draft => ({
|
|
228
336
|
name: draft.name,
|
|
229
337
|
url: draft.dataUrl,
|
|
230
338
|
fileUri: draft.fileUri,
|
|
339
|
+
uploadToken: stillUploaded(draft) ? draft.uploadToken : undefined,
|
|
231
340
|
contentType: draft.contentType,
|
|
232
341
|
size: draft.size,
|
|
233
342
|
previewUrl: draft.previewUri
|
|
@@ -243,6 +352,15 @@ function ChatView({
|
|
|
243
352
|
setInput(current => current || trimmed);
|
|
244
353
|
const status = error?.status;
|
|
245
354
|
showNotice(status === 413 ? labels.attach.tooLargeTogether : labels.failedToSend);
|
|
355
|
+
// An upload expired before this phone's clock said it would: put the
|
|
356
|
+
// files sent by token up again, once. After a lost connection the
|
|
357
|
+
// uploads still stand for the next try.
|
|
358
|
+
if (status === UPLOAD_EXPIRED_STATUS) {
|
|
359
|
+
sent.filter(draft => stillUploaded(draft) && !reuploaded.current.has(draft.id)).forEach(draft => {
|
|
360
|
+
reuploaded.current.add(draft.id);
|
|
361
|
+
startUpload(draft);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
246
364
|
}).finally(() => {
|
|
247
365
|
if (mounted.current) setSending(false);
|
|
248
366
|
});
|
|
@@ -262,17 +380,17 @@ function ChatView({
|
|
|
262
380
|
phone: stringPayload(action.payload.phone)
|
|
263
381
|
});
|
|
264
382
|
}
|
|
383
|
+
followSend.current = true;
|
|
265
384
|
onSendMessage((0, _agentUiBlocks.formatAgentUiActionMessage)(action), undefined, action).catch(() => {});
|
|
266
|
-
|
|
267
|
-
}, [onIdentifyVisitor, onSendMessage, scrollToBottom]);
|
|
385
|
+
}, [onIdentifyVisitor, onSendMessage]);
|
|
268
386
|
const handleSubmitProcedureForm = (0, _react.useCallback)(body => void onSendMessage(body).catch(() => {}), [onSendMessage]);
|
|
269
387
|
|
|
270
388
|
// The retried bubble moves to the end of the thread; follow it there.
|
|
271
389
|
// A second failure shows on the bubble again.
|
|
272
390
|
const handleRetryMessage = (0, _react.useCallback)(messageId => {
|
|
391
|
+
followSend.current = true;
|
|
273
392
|
onRetryMessage?.(messageId).catch(() => {});
|
|
274
|
-
|
|
275
|
-
}, [onRetryMessage, scrollToBottom]);
|
|
393
|
+
}, [onRetryMessage]);
|
|
276
394
|
|
|
277
395
|
// Recomputed only when the list changes — never on a keystroke.
|
|
278
396
|
const thread = (0, _react.useMemo)(() => deriveThread(messages), [messages]);
|
|
@@ -292,6 +410,143 @@ function ChatView({
|
|
|
292
410
|
const headerControl = {
|
|
293
411
|
backgroundColor: (0, _theme.withAlpha)(headerTextColor, 0.16)
|
|
294
412
|
};
|
|
413
|
+
|
|
414
|
+
// Newest first, for the inverted list; each row still compares itself with
|
|
415
|
+
// the message sent before it.
|
|
416
|
+
const rows = (0, _react.useMemo)(() => messages.map((message, index) => {
|
|
417
|
+
const previous = index > 0 ? messages[index - 1] : undefined;
|
|
418
|
+
return {
|
|
419
|
+
message,
|
|
420
|
+
startsDay: !previous || !(0, _conversations.isSameDay)(message.createdAt, previous.createdAt),
|
|
421
|
+
// Name only at the top of a run, the way every messenger does it.
|
|
422
|
+
startsRun: !previous || previous.direction === 'inbound' || previous.messageType === 'system' || previous.senderName !== message.senderName
|
|
423
|
+
};
|
|
424
|
+
}).reverse(), [messages]);
|
|
425
|
+
(0, _react.useEffect)(() => {
|
|
426
|
+
if (!followSend.current) return;
|
|
427
|
+
followSend.current = false;
|
|
428
|
+
scrollToBottom();
|
|
429
|
+
}, [rows, scrollToBottom]);
|
|
430
|
+
|
|
431
|
+
// The thread's message ids when the visitor scrolled away from the newest,
|
|
432
|
+
// null while it's in view. Teammate replies newer than all of them are
|
|
433
|
+
// counted on the button. A set, not the newest id alone: that may be the
|
|
434
|
+
// visitor's own bubble, whose temporary id changes once the server has it.
|
|
435
|
+
const [awayFrom, setAwayFrom] = (0, _react.useState)(null);
|
|
436
|
+
const rowIds = (0, _react.useRef)([]);
|
|
437
|
+
(0, _react.useEffect)(() => {
|
|
438
|
+
rowIds.current = rows.map(row => row.message.id);
|
|
439
|
+
}, [rows]);
|
|
440
|
+
const handleScroll = (0, _react.useCallback)(event => {
|
|
441
|
+
// Inverted: the offset is the distance up from the newest message.
|
|
442
|
+
const away = event.nativeEvent.contentOffset.y > JUMP_TO_LATEST_OFFSET;
|
|
443
|
+
setAwayFrom(current => away ? current ?? new Set(rowIds.current) : null);
|
|
444
|
+
}, []);
|
|
445
|
+
const unseenReplies = (0, _react.useMemo)(() => {
|
|
446
|
+
if (!awayFrom) return 0;
|
|
447
|
+
let count = 0;
|
|
448
|
+
for (const {
|
|
449
|
+
message
|
|
450
|
+
} of rows) {
|
|
451
|
+
if (awayFrom.has(message.id)) break;
|
|
452
|
+
if (message.direction !== 'inbound' && message.messageType !== 'system') count += 1;
|
|
453
|
+
}
|
|
454
|
+
return count;
|
|
455
|
+
}, [awayFrom, rows]);
|
|
456
|
+
const handleJumpToLatest = (0, _react.useCallback)(() => scrollToBottom(), [scrollToBottom]);
|
|
457
|
+
|
|
458
|
+
// Android only: it detaches rows scrolled out of view, where a large photo
|
|
459
|
+
// still downloading would never finish and keep its turn. Keyed as in usePhotoSource.
|
|
460
|
+
const handleViewableItemsChanged = (0, _react.useCallback)(({
|
|
461
|
+
viewableItems
|
|
462
|
+
}) => largeImageQueue.setOnScreen(viewableItems.flatMap(({
|
|
463
|
+
item
|
|
464
|
+
}) => (item.message.attachments ?? []).map(attachment => attachment.id || attachment.url))), [largeImageQueue]);
|
|
465
|
+
const renderRow = (0, _react.useCallback)(({
|
|
466
|
+
item: {
|
|
467
|
+
message,
|
|
468
|
+
startsDay,
|
|
469
|
+
startsRun
|
|
470
|
+
}
|
|
471
|
+
}) => {
|
|
472
|
+
const isAgentMessage = message.direction !== 'inbound' && message.messageType !== 'system';
|
|
473
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
474
|
+
style: _ChatViewStyles.styles.threadRow,
|
|
475
|
+
children: [startsDay ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
476
|
+
style: _ChatViewStyles.styles.daySeparator,
|
|
477
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
478
|
+
style: _ChatViewStyles.styles.dayLine
|
|
479
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
480
|
+
style: _ChatViewStyles.styles.dayLabel,
|
|
481
|
+
children: (0, _conversations.formatDayLabel)(message.createdAt, locale, labels)
|
|
482
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
483
|
+
style: _ChatViewStyles.styles.dayLine
|
|
484
|
+
})]
|
|
485
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_ErrorBoundary.WidgetErrorBoundary, {
|
|
486
|
+
fallback: null,
|
|
487
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(MessageBubble, {
|
|
488
|
+
message: message,
|
|
489
|
+
color: color,
|
|
490
|
+
labels: labels,
|
|
491
|
+
onOpenImage: setViewerGallery,
|
|
492
|
+
onRateConversation: onRateConversation,
|
|
493
|
+
locales: locales,
|
|
494
|
+
senderLabel: isAgentMessage && (startsRun || startsDay) ? message.senderName || agentName || labels.team : null,
|
|
495
|
+
timeLabel: (0, _conversations.formatClockTime)(message.createdAt, locale),
|
|
496
|
+
receiptLabel: message.id === thread.lastVisitorMessageId ? receiptLabel(message, labels) : undefined,
|
|
497
|
+
onSubmitProcedureForm: message.id === thread.procedureFormMessageId ? handleSubmitProcedureForm : undefined,
|
|
498
|
+
onAgentUiAction: thread.agentBlocksShown.has(message.id) ? handleAgentUiAction : undefined,
|
|
499
|
+
onRetry: onRetryMessage && (0, _conversations.canRetryMessage)(message) ? handleRetryMessage : undefined,
|
|
500
|
+
photoAlbums: photoAlbums
|
|
501
|
+
})
|
|
502
|
+
})]
|
|
503
|
+
});
|
|
504
|
+
}, [agentName, color, handleAgentUiAction, handleRetryMessage, handleSubmitProcedureForm, labels, locale, locales, onRateConversation, onRetryMessage, photoAlbums, thread]);
|
|
505
|
+
const loadEarlierRow = hasMoreMessages ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
506
|
+
onPress: handleLoadEarlier,
|
|
507
|
+
accessibilityRole: "button",
|
|
508
|
+
style: _ChatViewStyles.styles.loadMore,
|
|
509
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
510
|
+
style: [_ChatViewStyles.styles.loadMoreLabel, {
|
|
511
|
+
color
|
|
512
|
+
}],
|
|
513
|
+
children: loading ? labels.loading : labels.loadEarlier
|
|
514
|
+
})
|
|
515
|
+
}) : null;
|
|
516
|
+
|
|
517
|
+
// Under the newest message.
|
|
518
|
+
const threadTail = awaitingTeammate || agentTyping ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
519
|
+
style: _ChatViewStyles.styles.threadTail,
|
|
520
|
+
children: [awaitingTeammate ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
521
|
+
style: _ChatViewStyles.styles.waitingRow,
|
|
522
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TeamAvatars.TeamAvatars, {
|
|
523
|
+
avatars: teamAvatars,
|
|
524
|
+
size: 20,
|
|
525
|
+
color: color
|
|
526
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
527
|
+
style: _ChatViewStyles.styles.waitingText,
|
|
528
|
+
children: labels.waitingForTeammate
|
|
529
|
+
})]
|
|
530
|
+
}) : null, agentTyping ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
531
|
+
style: _ChatViewStyles.styles.typingRow,
|
|
532
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
533
|
+
style: _ChatViewStyles.styles.typingBubble,
|
|
534
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
535
|
+
index: 0,
|
|
536
|
+
reduceMotion: reduceMotion
|
|
537
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
538
|
+
index: 1,
|
|
539
|
+
reduceMotion: reduceMotion
|
|
540
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
541
|
+
index: 2,
|
|
542
|
+
reduceMotion: reduceMotion
|
|
543
|
+
})]
|
|
544
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
545
|
+
style: _ChatViewStyles.styles.typingLabel,
|
|
546
|
+
children: labels.thinking
|
|
547
|
+
})]
|
|
548
|
+
}) : null]
|
|
549
|
+
}) : null;
|
|
295
550
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
296
551
|
style: _global.globalStyles.screen,
|
|
297
552
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_Gradient.GradientBackground, {
|
|
@@ -396,29 +651,76 @@ function ChatView({
|
|
|
396
651
|
color: headerTextColor
|
|
397
652
|
})
|
|
398
653
|
})]
|
|
399
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.
|
|
400
|
-
ref: scrollRef,
|
|
654
|
+
}), messages.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
401
655
|
style: _global.globalStyles.fill,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
656
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(ImageQueueContext.Provider, {
|
|
657
|
+
value: largeImageQueue,
|
|
658
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
659
|
+
ref: listRef,
|
|
660
|
+
data: rows,
|
|
661
|
+
inverted: true,
|
|
662
|
+
keyExtractor: rowKey,
|
|
663
|
+
renderItem: renderRow,
|
|
664
|
+
style: _global.globalStyles.fill,
|
|
665
|
+
contentContainerStyle: _ChatViewStyles.styles.threadContent,
|
|
666
|
+
ItemSeparatorComponent: ThreadGap
|
|
667
|
+
// Inverted, the header sits under the newest message and the footer
|
|
668
|
+
// above the oldest.
|
|
669
|
+
,
|
|
670
|
+
ListHeaderComponent: threadTail,
|
|
671
|
+
ListHeaderComponentStyle: _ChatViewStyles.styles.threadTailInList,
|
|
672
|
+
ListFooterComponent: loadEarlierRow,
|
|
673
|
+
onEndReached: handleLoadEarlier,
|
|
674
|
+
onEndReachedThreshold: 0.5,
|
|
675
|
+
onScroll: handleScroll,
|
|
676
|
+
scrollEventThrottle: 100
|
|
677
|
+
// A reply arriving below doesn't move what a visitor reads further
|
|
678
|
+
// up; near the newest message the list follows it into view.
|
|
679
|
+
,
|
|
680
|
+
maintainVisibleContentPosition: {
|
|
681
|
+
minIndexForVisible: 0,
|
|
682
|
+
autoscrollToTopThreshold: JUMP_TO_LATEST_OFFSET
|
|
683
|
+
},
|
|
684
|
+
onViewableItemsChanged: _reactNative.Platform.OS === 'android' ? handleViewableItemsChanged : undefined,
|
|
685
|
+
keyboardShouldPersistTaps: "handled"
|
|
686
|
+
// Interactive dismissal drags the keyboard down under the finger while
|
|
687
|
+
// the composer stays where the last keyboard event left it, which opens
|
|
688
|
+
// a gap under the thread; on-drag hands us a normal hide event instead.
|
|
689
|
+
,
|
|
690
|
+
keyboardDismissMode: "on-drag"
|
|
691
|
+
})
|
|
692
|
+
}), awayFrom ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
693
|
+
onPress: handleJumpToLatest,
|
|
413
694
|
accessibilityRole: "button",
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
695
|
+
accessibilityLabel: unseenReplies ? (0, _i18n.formatLabel)(labels.jumpToLatestNew, {
|
|
696
|
+
count: unseenReplies
|
|
697
|
+
}) : labels.jumpToLatest,
|
|
698
|
+
hitSlop: 8,
|
|
699
|
+
style: ({
|
|
700
|
+
pressed
|
|
701
|
+
}) => [_ChatViewStyles.styles.jumpToLatest, pressed && _global.globalStyles.pressed],
|
|
702
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.ChevronIcon, {
|
|
703
|
+
size: 18,
|
|
704
|
+
direction: "down",
|
|
705
|
+
color: _theme.palette.text
|
|
706
|
+
}), unseenReplies ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
707
|
+
style: [_ChatViewStyles.styles.jumpBadge, {
|
|
708
|
+
backgroundColor: color
|
|
418
709
|
}],
|
|
419
|
-
children:
|
|
420
|
-
|
|
421
|
-
|
|
710
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
711
|
+
style: [_ChatViewStyles.styles.jumpBadgeText, {
|
|
712
|
+
color: (0, _theme.contrastingTextColor)(color)
|
|
713
|
+
}],
|
|
714
|
+
children: unseenReplies > 9 ? '9+' : unseenReplies
|
|
715
|
+
})
|
|
716
|
+
}) : null]
|
|
717
|
+
}) : null]
|
|
718
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
|
|
719
|
+
style: _global.globalStyles.fill,
|
|
720
|
+
contentContainerStyle: [_ChatViewStyles.styles.messagesContent, isEmptyThread && _ChatViewStyles.styles.messagesContentEmpty, isLoadingThread && _ChatViewStyles.styles.messagesContentLoading],
|
|
721
|
+
keyboardShouldPersistTaps: "handled",
|
|
722
|
+
keyboardDismissMode: "on-drag",
|
|
723
|
+
children: [loadEarlierRow, isLoadingThread ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_MessagesSkeleton.MessagesSkeleton, {
|
|
422
724
|
color: color,
|
|
423
725
|
label: labels.loading
|
|
424
726
|
}) : null, isEmptyThread ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
@@ -435,70 +737,7 @@ function ChatView({
|
|
|
435
737
|
style: _ChatViewStyles.styles.emptyText,
|
|
436
738
|
children: labels.emptyConversation
|
|
437
739
|
})]
|
|
438
|
-
}) : null,
|
|
439
|
-
const previous = index > 0 ? messages[index - 1] : undefined;
|
|
440
|
-
const startsDay = !previous || !(0, _conversations.isSameDay)(message.createdAt, previous.createdAt);
|
|
441
|
-
const isAgentMessage = message.direction !== 'inbound' && message.messageType !== 'system';
|
|
442
|
-
// Name only at the top of a run, the way every messenger does it.
|
|
443
|
-
const startsRun = !previous || previous.direction === 'inbound' || previous.messageType === 'system' || previous.senderName !== message.senderName;
|
|
444
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_react.Fragment, {
|
|
445
|
-
children: [startsDay ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
446
|
-
style: _ChatViewStyles.styles.daySeparator,
|
|
447
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
448
|
-
style: _ChatViewStyles.styles.dayLine
|
|
449
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
450
|
-
style: _ChatViewStyles.styles.dayLabel,
|
|
451
|
-
children: (0, _conversations.formatDayLabel)(message.createdAt, locale, labels)
|
|
452
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
453
|
-
style: _ChatViewStyles.styles.dayLine
|
|
454
|
-
})]
|
|
455
|
-
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_ErrorBoundary.WidgetErrorBoundary, {
|
|
456
|
-
fallback: null,
|
|
457
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(MessageBubble, {
|
|
458
|
-
message: message,
|
|
459
|
-
color: color,
|
|
460
|
-
labels: labels,
|
|
461
|
-
onOpenImage: setViewerImage,
|
|
462
|
-
onRateConversation: onRateConversation,
|
|
463
|
-
locales: locales,
|
|
464
|
-
senderLabel: isAgentMessage && (startsRun || startsDay) ? message.senderName || agentName || labels.team : null,
|
|
465
|
-
timeLabel: (0, _conversations.formatClockTime)(message.createdAt, locale),
|
|
466
|
-
receiptLabel: message.id === thread.lastVisitorMessageId ? receiptLabel(message, labels) : undefined,
|
|
467
|
-
onSubmitProcedureForm: message.id === thread.procedureFormMessageId ? handleSubmitProcedureForm : undefined,
|
|
468
|
-
onAgentUiAction: thread.agentBlocksShown.has(message.id) ? handleAgentUiAction : undefined,
|
|
469
|
-
onRetry: onRetryMessage && (0, _conversations.canRetryMessage)(message) ? handleRetryMessage : undefined
|
|
470
|
-
})
|
|
471
|
-
})]
|
|
472
|
-
}, message.id);
|
|
473
|
-
}), awaitingTeammate ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
474
|
-
style: _ChatViewStyles.styles.waitingRow,
|
|
475
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TeamAvatars.TeamAvatars, {
|
|
476
|
-
avatars: teamAvatars,
|
|
477
|
-
size: 20,
|
|
478
|
-
color: color
|
|
479
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
480
|
-
style: _ChatViewStyles.styles.waitingText,
|
|
481
|
-
children: labels.waitingForTeammate
|
|
482
|
-
})]
|
|
483
|
-
}) : null, agentTyping ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
484
|
-
style: _ChatViewStyles.styles.typingRow,
|
|
485
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
486
|
-
style: _ChatViewStyles.styles.typingBubble,
|
|
487
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
488
|
-
index: 0,
|
|
489
|
-
reduceMotion: reduceMotion
|
|
490
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
491
|
-
index: 1,
|
|
492
|
-
reduceMotion: reduceMotion
|
|
493
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(TypingDot, {
|
|
494
|
-
index: 2,
|
|
495
|
-
reduceMotion: reduceMotion
|
|
496
|
-
})]
|
|
497
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
498
|
-
style: _ChatViewStyles.styles.typingLabel,
|
|
499
|
-
children: labels.thinking
|
|
500
|
-
})]
|
|
501
|
-
}) : null]
|
|
740
|
+
}) : null, threadTail]
|
|
502
741
|
}), offlineMessage ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
503
742
|
style: _ChatViewStyles.styles.offlineBanner,
|
|
504
743
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -515,10 +754,18 @@ function ChatView({
|
|
|
515
754
|
}) : null, drafts.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AttachmentChips.AttachmentChips, {
|
|
516
755
|
attachments: drafts,
|
|
517
756
|
disabled: sending,
|
|
518
|
-
|
|
757
|
+
labels: labels.attach,
|
|
519
758
|
onRemove: id => {
|
|
759
|
+
uploads.current.get(id)?.cancel();
|
|
760
|
+
uploads.current.delete(id);
|
|
520
761
|
setDrafts(current => current.filter(draft => draft.id !== id));
|
|
521
762
|
showNotice(null);
|
|
763
|
+
},
|
|
764
|
+
onRetry: id => {
|
|
765
|
+
const draft = drafts.find(item => item.id === id);
|
|
766
|
+
if (!draft) return;
|
|
767
|
+
showNotice(null);
|
|
768
|
+
startUpload(draft);
|
|
522
769
|
}
|
|
523
770
|
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
524
771
|
style: [_ChatViewStyles.styles.composer, showAttach && _ChatViewStyles.styles.composerWithAttach, composerFocused && _ChatViewStyles.styles.composerFocused, composerFocused && {
|
|
@@ -556,6 +803,7 @@ function ChatView({
|
|
|
556
803
|
onPress: handleSend,
|
|
557
804
|
disabled: !canSend,
|
|
558
805
|
accessibilityRole: "button",
|
|
806
|
+
accessibilityLabel: labels.send,
|
|
559
807
|
accessibilityState: {
|
|
560
808
|
disabled: !canSend
|
|
561
809
|
},
|
|
@@ -578,11 +826,11 @@ function ChatView({
|
|
|
578
826
|
onSelect: handleMenuSelect,
|
|
579
827
|
onClose: () => setMenuOpen(false)
|
|
580
828
|
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImageViewer.ImageViewer, {
|
|
581
|
-
|
|
829
|
+
gallery: viewerGallery,
|
|
582
830
|
labels: labels.viewer,
|
|
583
|
-
onClose: () =>
|
|
831
|
+
onClose: () => setViewerGallery(null),
|
|
584
832
|
onOpenExternally: url => {
|
|
585
|
-
|
|
833
|
+
setViewerGallery(null);
|
|
586
834
|
void (0, _safeUrl.openExternalUrl)(url);
|
|
587
835
|
}
|
|
588
836
|
})]
|
|
@@ -605,7 +853,8 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
605
853
|
onOpenImage,
|
|
606
854
|
onRateConversation,
|
|
607
855
|
locales,
|
|
608
|
-
onRetry
|
|
856
|
+
onRetry,
|
|
857
|
+
photoAlbums
|
|
609
858
|
}) {
|
|
610
859
|
const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
|
|
611
860
|
if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
|
|
@@ -668,6 +917,56 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
668
917
|
const procedureForm = !isVisitor ? (0, _agentUiBlocks.getProcedureForm)(message.metadata) : null;
|
|
669
918
|
const agentUiBlocks = !isVisitor ? (0, _agentUiBlocks.getAgentUiBlocks)(message.metadata) : [];
|
|
670
919
|
const textColor = isVisitor ? _theme.palette.white : _theme.palette.ink;
|
|
920
|
+
// A reply with images between its paragraphs shows them where the teammate put them.
|
|
921
|
+
const inlineParts = isVisitor ? null : (0, _inlineAttachments.splitInlineAttachments)(message.body, message.attachments);
|
|
922
|
+
const files = inlineParts ? inlineParts.files : message.attachments ?? [];
|
|
923
|
+
// Two or more photos share one album; any other files follow it.
|
|
924
|
+
const photos = files.filter(attachment => (0, _pickers.isImageAttachment)(attachment.contentType));
|
|
925
|
+
const album = photoAlbums && photos.length > 1 ? photos : null;
|
|
926
|
+
const sentAt = Date.parse(message.createdAt) || 0;
|
|
927
|
+
// A tap on one of the message's photos opens them all, at that one, in the
|
|
928
|
+
// order the bubble shows them.
|
|
929
|
+
const openPhotos = tapped => {
|
|
930
|
+
const photos = [...(inlineParts?.parts.flatMap(part => 'attachment' in part ? [part.attachment] : []) ?? []), ...files].filter(attachment => (0, _pickers.isImageAttachment)(attachment.contentType)).map(attachment => {
|
|
931
|
+
const url = attachment.id === tapped.id ? tapped.url : (0, _localPreviews.localPreviewFor)(attachment.url) || attachment.url;
|
|
932
|
+
// The phone's copy may be gone by now; the stored one stands in.
|
|
933
|
+
const fallbackUrl = url !== attachment.url && (0, _safeUrl.isHttpUrl)(attachment.url) ? attachment.url : undefined;
|
|
934
|
+
return {
|
|
935
|
+
id: attachment.id,
|
|
936
|
+
url,
|
|
937
|
+
name: attachment.name,
|
|
938
|
+
large: isLargeDownload(url, attachment.size),
|
|
939
|
+
fallbackUrl
|
|
940
|
+
};
|
|
941
|
+
}).filter(photo => !!photo.url);
|
|
942
|
+
const index = photos.findIndex(photo => photo.id === tapped.id);
|
|
943
|
+
onOpenImage(index < 0 ? {
|
|
944
|
+
images: [tapped],
|
|
945
|
+
index: 0
|
|
946
|
+
} : {
|
|
947
|
+
images: photos.map(({
|
|
948
|
+
url,
|
|
949
|
+
name,
|
|
950
|
+
large,
|
|
951
|
+
fallbackUrl
|
|
952
|
+
}) => ({
|
|
953
|
+
url,
|
|
954
|
+
name,
|
|
955
|
+
large,
|
|
956
|
+
fallbackUrl
|
|
957
|
+
})),
|
|
958
|
+
index
|
|
959
|
+
});
|
|
960
|
+
};
|
|
961
|
+
const renderAttachment = attachment => /*#__PURE__*/(0, _jsxRuntime.jsx)(AttachmentPreview, {
|
|
962
|
+
loadPriority: sentAt,
|
|
963
|
+
attachment: attachment,
|
|
964
|
+
isVisitor: isVisitor,
|
|
965
|
+
color: color,
|
|
966
|
+
voiceLabels: labels.voice,
|
|
967
|
+
openLabel: labels.openAttachment,
|
|
968
|
+
onOpenImage: openPhotos
|
|
969
|
+
}, attachment.id);
|
|
671
970
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
672
971
|
style: [_ChatViewStyles.styles.messageRow, isVisitor ? _ChatViewStyles.styles.messageRowRight : _ChatViewStyles.styles.messageRowLeft],
|
|
673
972
|
children: [senderLabel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -677,7 +976,13 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
677
976
|
style: [_ChatViewStyles.styles.bubble, isVisitor ? [_ChatViewStyles.styles.bubbleVisitor, {
|
|
678
977
|
backgroundColor: color
|
|
679
978
|
}] : _ChatViewStyles.styles.bubbleAgent, rtl && (isVisitor ? _ChatViewStyles.styles.bubbleVisitorRTL : _ChatViewStyles.styles.bubbleAgentRTL), message.status === 'pending' && _ChatViewStyles.styles.bubblePending],
|
|
680
|
-
children: [
|
|
979
|
+
children: [inlineParts ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
980
|
+
style: _ChatViewStyles.styles.inlineParts,
|
|
981
|
+
children: inlineParts.parts.map((part, index) => 'text' in part ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Markdown.Markdown, {
|
|
982
|
+
content: part.text,
|
|
983
|
+
color: textColor
|
|
984
|
+
}, `text-${index}`) : renderAttachment(part.attachment))
|
|
985
|
+
}) : hasBody ? isVisitor ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
681
986
|
style: [_ChatViewStyles.styles.bubbleText, {
|
|
682
987
|
color: textColor
|
|
683
988
|
}],
|
|
@@ -685,16 +990,14 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
685
990
|
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_Markdown.Markdown, {
|
|
686
991
|
content: messageText,
|
|
687
992
|
color: textColor
|
|
688
|
-
}) : null,
|
|
689
|
-
style: [_ChatViewStyles.styles.attachments, hasBody && _ChatViewStyles.styles.attachmentsSpaced],
|
|
690
|
-
children:
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
onOpenImage: onOpenImage
|
|
697
|
-
}, attachment.id))
|
|
993
|
+
}) : null, files.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
994
|
+
style: [_ChatViewStyles.styles.attachments, (hasBody || !!inlineParts) && _ChatViewStyles.styles.attachmentsSpaced],
|
|
995
|
+
children: [album ? /*#__PURE__*/(0, _jsxRuntime.jsx)(PhotoAlbum, {
|
|
996
|
+
photos: album,
|
|
997
|
+
loadPriority: sentAt,
|
|
998
|
+
moreLabel: labels.morePhotos,
|
|
999
|
+
onOpen: openPhotos
|
|
1000
|
+
}) : null, (album ? files.filter(attachment => !album.includes(attachment)) : files).map(renderAttachment)]
|
|
698
1001
|
}) : null, timeLabel ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
699
1002
|
style: [_ChatViewStyles.styles.messageTime, {
|
|
700
1003
|
color: isVisitor ? (0, _theme.withAlpha)(_theme.palette.white, 0.7) : _theme.palette.textSubtle
|
|
@@ -725,6 +1028,7 @@ const MessageBubble = /*#__PURE__*/(0, _react.memo)(function MessageBubble({
|
|
|
725
1028
|
labels: labels.blocks,
|
|
726
1029
|
onAction: onAgentUiAction
|
|
727
1030
|
}) : null, agentUiBlocks.length === 0 && procedureForm && onSubmitProcedureForm ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ProcedureFieldForm, {
|
|
1031
|
+
messageId: message.id,
|
|
728
1032
|
form: procedureForm,
|
|
729
1033
|
color: color,
|
|
730
1034
|
labels: labels,
|
|
@@ -738,26 +1042,194 @@ function receiptLabel(message, labels) {
|
|
|
738
1042
|
if (message.deletedAt || message.status === 'pending' || message.status === 'failed') return undefined;
|
|
739
1043
|
return message.status === 'read' ? labels.messageSeen : labels.messageSent;
|
|
740
1044
|
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Where a photo shows from, and whether it may load yet. `url` is empty when
|
|
1048
|
+
* the server sent a non-http(s) URL (see resolveUrl). The visitor's own
|
|
1049
|
+
* attachment points at the picked file until the server copy replaces it (or
|
|
1050
|
+
* is empty without one). A photo they sent this session keeps showing from
|
|
1051
|
+
* the phone after that: the stored copy may not load yet. Whichever fails to
|
|
1052
|
+
* load gives way to the next.
|
|
1053
|
+
*/
|
|
1054
|
+
function usePhotoSource(attachment, loadPriority) {
|
|
1055
|
+
const [failedUrls, setFailedUrls] = (0, _react.useState)([]);
|
|
1056
|
+
const [loadedUrl, setLoadedUrl] = (0, _react.useState)(null);
|
|
1057
|
+
const url = [(0, _localPreviews.localPreviewFor)(attachment.url), attachment.url].find(candidate => !!candidate && !failedUrls.includes(candidate));
|
|
1058
|
+
const isPhoto = (0, _pickers.isImageAttachment)(attachment.contentType) && !!url;
|
|
1059
|
+
// A large download waits its turn; small photos and the phone's own copy don't.
|
|
1060
|
+
const [turn, settled] = useImageTurn(attachment.id || attachment.url, loadPriority, isPhoto && isLargeDownload(url, attachment.size));
|
|
1061
|
+
return {
|
|
1062
|
+
url,
|
|
1063
|
+
isPhoto,
|
|
1064
|
+
turn,
|
|
1065
|
+
/** Waiting for its turn or still downloading; the next URL after a failure starts over. */
|
|
1066
|
+
loading: isPhoto && loadedUrl !== url,
|
|
1067
|
+
onLoad: () => {
|
|
1068
|
+
settled();
|
|
1069
|
+
if (url) setLoadedUrl(url);
|
|
1070
|
+
},
|
|
1071
|
+
onError: () => {
|
|
1072
|
+
settled();
|
|
1073
|
+
if (!url) return;
|
|
1074
|
+
setFailedUrls(current => [...current, url]);
|
|
1075
|
+
// The viewer and the message's other views stop offering it too.
|
|
1076
|
+
if (url !== attachment.url && url === (0, _localPreviews.localPreviewFor)(attachment.url)) (0, _localPreviews.forgetLocalPreview)(attachment.url);
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Several photos in one message, the way WhatsApp shows them: one square,
|
|
1083
|
+
* two side by side, three as one tall beside two stacked, four in a grid, and
|
|
1084
|
+
* past four the last tile covered with how many more there are.
|
|
1085
|
+
*/
|
|
1086
|
+
function PhotoAlbum({
|
|
1087
|
+
photos,
|
|
1088
|
+
loadPriority,
|
|
1089
|
+
moreLabel,
|
|
1090
|
+
onOpen
|
|
1091
|
+
}) {
|
|
1092
|
+
const shown = photos.slice(0, 4);
|
|
1093
|
+
// The covered tile counts too: it's no more visible than the ones after it.
|
|
1094
|
+
const more = photos.length > 4 ? photos.length - 3 : 0;
|
|
1095
|
+
const tile = (attachment, index, size) => /*#__PURE__*/(0, _jsxRuntime.jsx)(PhotoTile, {
|
|
1096
|
+
attachment: attachment,
|
|
1097
|
+
loadPriority: loadPriority,
|
|
1098
|
+
style: size,
|
|
1099
|
+
more: index === 3 ? more : 0,
|
|
1100
|
+
moreLabel: moreLabel,
|
|
1101
|
+
onOpen: onOpen
|
|
1102
|
+
}, attachment.id);
|
|
1103
|
+
if (shown.length === 2) {
|
|
1104
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1105
|
+
style: [_ChatViewStyles.styles.album, _ChatViewStyles.styles.albumRow],
|
|
1106
|
+
children: shown.map((photo, i) => tile(photo, i, _ChatViewStyles.styles.albumHalf))
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
if (shown.length === 3) {
|
|
1110
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
1111
|
+
style: [_ChatViewStyles.styles.album, _ChatViewStyles.styles.albumRow],
|
|
1112
|
+
children: [tile(shown[0], 0, _ChatViewStyles.styles.albumHalf), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1113
|
+
style: _ChatViewStyles.styles.albumColumn,
|
|
1114
|
+
children: shown.slice(1).map((photo, i) => tile(photo, i + 1, _ChatViewStyles.styles.albumQuarter))
|
|
1115
|
+
})]
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1119
|
+
style: [_ChatViewStyles.styles.album, _ChatViewStyles.styles.albumGrid],
|
|
1120
|
+
children: shown.map((photo, i) => tile(photo, i, _ChatViewStyles.styles.albumQuarter))
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
function PhotoTile({
|
|
1124
|
+
attachment,
|
|
1125
|
+
loadPriority,
|
|
1126
|
+
style,
|
|
1127
|
+
more,
|
|
1128
|
+
moreLabel,
|
|
1129
|
+
onOpen
|
|
1130
|
+
}) {
|
|
1131
|
+
const photo = usePhotoSource(attachment, loadPriority);
|
|
1132
|
+
// One that can't load still opens, so the viewer can offer the browser.
|
|
1133
|
+
const openUrl = photo.url ?? ((0, _safeUrl.isHttpUrl)(attachment.url) ? attachment.url : undefined);
|
|
1134
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
1135
|
+
onPress: openUrl ? () => onOpen({
|
|
1136
|
+
id: attachment.id,
|
|
1137
|
+
url: openUrl,
|
|
1138
|
+
name: attachment.name
|
|
1139
|
+
}) : undefined,
|
|
1140
|
+
accessibilityRole: "imagebutton",
|
|
1141
|
+
accessibilityLabel: more > 0 ? `${attachment.name}, ${(0, _i18n.formatLabel)(moreLabel, {
|
|
1142
|
+
count: more
|
|
1143
|
+
})}` : attachment.name,
|
|
1144
|
+
style: ({
|
|
1145
|
+
pressed
|
|
1146
|
+
}) => [_ChatViewStyles.styles.albumTile, style, pressed && _global.globalStyles.pressed],
|
|
1147
|
+
children: [photo.isPhoto && photo.turn ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
1148
|
+
source: {
|
|
1149
|
+
uri: photo.url
|
|
1150
|
+
},
|
|
1151
|
+
style: _ChatViewStyles.styles.albumImage,
|
|
1152
|
+
resizeMode: "cover"
|
|
1153
|
+
// Decoded at the tile's size, like a single photo.
|
|
1154
|
+
,
|
|
1155
|
+
resizeMethod: "resize",
|
|
1156
|
+
accessibilityIgnoresInvertColors: true,
|
|
1157
|
+
onLoad: photo.onLoad,
|
|
1158
|
+
onError: photo.onError
|
|
1159
|
+
}) : null, !photo.isPhoto ?
|
|
1160
|
+
/*#__PURE__*/
|
|
1161
|
+
// Nothing left to load (an expired link): its type and name, not a blank square.
|
|
1162
|
+
(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
1163
|
+
style: _ChatViewStyles.styles.albumFallback,
|
|
1164
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
1165
|
+
style: _ChatViewStyles.styles.albumFallbackType,
|
|
1166
|
+
children: (0, _FileCard.fileTypeLabel)(attachment.name, attachment.contentType)
|
|
1167
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
1168
|
+
style: _ChatViewStyles.styles.albumFallbackName,
|
|
1169
|
+
numberOfLines: 2,
|
|
1170
|
+
children: attachment.name
|
|
1171
|
+
})]
|
|
1172
|
+
}) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(PhotoLoader, {
|
|
1173
|
+
loading: photo.loading && more === 0
|
|
1174
|
+
}), more > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1175
|
+
style: _ChatViewStyles.styles.albumMore,
|
|
1176
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
1177
|
+
style: _ChatViewStyles.styles.albumMoreText,
|
|
1178
|
+
children: ["+", more]
|
|
1179
|
+
})
|
|
1180
|
+
}) : null]
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Spins over a photo until it has loaded. Held back a moment, so a photo that
|
|
1186
|
+
* comes straight from the cache never flashes it.
|
|
1187
|
+
*/
|
|
1188
|
+
function PhotoLoader({
|
|
1189
|
+
loading
|
|
1190
|
+
}) {
|
|
1191
|
+
const [due, setDue] = (0, _react.useState)(false);
|
|
1192
|
+
(0, _react.useEffect)(() => {
|
|
1193
|
+
if (!loading) return undefined;
|
|
1194
|
+
const timer = setTimeout(() => setDue(true), PHOTO_LOADER_DELAY_MS);
|
|
1195
|
+
return () => {
|
|
1196
|
+
clearTimeout(timer);
|
|
1197
|
+
setDue(false);
|
|
1198
|
+
};
|
|
1199
|
+
}, [loading]);
|
|
1200
|
+
if (!loading || !due) return null;
|
|
1201
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1202
|
+
style: _ChatViewStyles.styles.photoLoader,
|
|
1203
|
+
pointerEvents: "none",
|
|
1204
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1205
|
+
style: _ChatViewStyles.styles.photoLoaderBadge,
|
|
1206
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
|
|
1207
|
+
size: "small",
|
|
1208
|
+
color: _theme.palette.white
|
|
1209
|
+
})
|
|
1210
|
+
})
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
741
1213
|
function AttachmentPreview({
|
|
742
1214
|
attachment,
|
|
1215
|
+
loadPriority,
|
|
743
1216
|
isVisitor,
|
|
744
1217
|
color,
|
|
745
1218
|
voiceLabels,
|
|
746
1219
|
openLabel,
|
|
747
1220
|
onOpenImage
|
|
748
1221
|
}) {
|
|
749
|
-
// `url` is empty when the server sent a non-http(s) URL (see resolveUrl).
|
|
750
|
-
// The visitor's own attachment points at the picked file until the server
|
|
751
|
-
// copy replaces it (or is empty without one).
|
|
752
|
-
const [imageFailed, setImageFailed] = (0, _react.useState)(false);
|
|
753
1222
|
const [audioFailed, setAudioFailed] = (0, _react.useState)(false);
|
|
754
|
-
const
|
|
1223
|
+
const photo = usePhotoSource(attachment, loadPriority);
|
|
1224
|
+
const imageUrl = photo.url;
|
|
1225
|
+
const isImage = photo.isPhoto;
|
|
755
1226
|
const openable = (0, _safeUrl.isHttpUrl)(attachment.url);
|
|
756
1227
|
// Images open in the in-app viewer, a local photo too since it already
|
|
757
1228
|
// renders here. Other files (PDF, sheets) need the OS, which can only open
|
|
758
1229
|
// the server copy.
|
|
759
1230
|
const open = isImage ? () => onOpenImage({
|
|
760
|
-
|
|
1231
|
+
id: attachment.id,
|
|
1232
|
+
url: imageUrl,
|
|
761
1233
|
name: attachment.name
|
|
762
1234
|
}) : openable ? () => void (0, _safeUrl.openExternalUrl)(attachment.url) : undefined;
|
|
763
1235
|
|
|
@@ -776,22 +1248,35 @@ function AttachmentPreview({
|
|
|
776
1248
|
});
|
|
777
1249
|
}
|
|
778
1250
|
if (isImage) {
|
|
779
|
-
return /*#__PURE__*/(0, _jsxRuntime.
|
|
1251
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
|
|
780
1252
|
onPress: open,
|
|
781
1253
|
accessibilityRole: "imagebutton",
|
|
782
1254
|
accessibilityLabel: attachment.name,
|
|
783
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
1255
|
+
children: [photo.turn ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
784
1256
|
source: {
|
|
785
|
-
uri:
|
|
1257
|
+
uri: imageUrl
|
|
786
1258
|
},
|
|
787
1259
|
style: _ChatViewStyles.styles.attachmentImage,
|
|
788
|
-
resizeMode: "cover"
|
|
789
|
-
|
|
1260
|
+
resizeMode: "cover"
|
|
1261
|
+
// Android otherwise decodes a server image at full size: two 20 MP
|
|
1262
|
+
// photos fill its shared bitmap budget, and every later image in
|
|
1263
|
+
// the app fails to load. Decoded at the bubble's size instead.
|
|
1264
|
+
,
|
|
1265
|
+
resizeMethod: "resize",
|
|
1266
|
+
accessibilityIgnoresInvertColors: true,
|
|
1267
|
+
onLoad: photo.onLoad
|
|
790
1268
|
// An image that can't load (expired link, deleted file) falls back to
|
|
791
1269
|
// the file row below instead of an empty box.
|
|
792
1270
|
,
|
|
793
|
-
onError:
|
|
794
|
-
})
|
|
1271
|
+
onError: photo.onError
|
|
1272
|
+
}) :
|
|
1273
|
+
/*#__PURE__*/
|
|
1274
|
+
// Same box as the image, so nothing moves when its turn comes.
|
|
1275
|
+
(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
1276
|
+
style: _ChatViewStyles.styles.attachmentImage
|
|
1277
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(PhotoLoader, {
|
|
1278
|
+
loading: photo.loading
|
|
1279
|
+
})]
|
|
795
1280
|
});
|
|
796
1281
|
}
|
|
797
1282
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_FileCard.FileCard, {
|
|
@@ -805,14 +1290,15 @@ function AttachmentPreview({
|
|
|
805
1290
|
});
|
|
806
1291
|
}
|
|
807
1292
|
function ProcedureFieldForm({
|
|
1293
|
+
messageId,
|
|
808
1294
|
form,
|
|
809
1295
|
color,
|
|
810
1296
|
labels,
|
|
811
1297
|
onSubmit
|
|
812
1298
|
}) {
|
|
813
|
-
const [values, setValues] = (0,
|
|
814
|
-
const [submitted, setSubmitted] = (0,
|
|
815
|
-
const [expanded, setExpanded] = (0,
|
|
1299
|
+
const [values, setValues] = (0, _useRetainedState.useRetainedState)(`${messageId}:values`, {});
|
|
1300
|
+
const [submitted, setSubmitted] = (0, _useRetainedState.useRetainedState)(`${messageId}:submitted`, false);
|
|
1301
|
+
const [expanded, setExpanded] = (0, _useRetainedState.useRetainedState)(`${messageId}:expanded`, false);
|
|
816
1302
|
const canSubmit = form.fields.every(field => !field.required || values[field.name]?.trim());
|
|
817
1303
|
const handleSubmit = () => {
|
|
818
1304
|
if (!canSubmit || submitted) return;
|