@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TeamAvatar } from '../../../types';
|
|
2
2
|
/** Props and label bundle for the chat thread. */
|
|
3
3
|
|
|
4
|
+
import type { UploadedFile } from '../../../api/client';
|
|
4
5
|
import type { DraftAttachment, PickResult } from '../../../attachments/pickers';
|
|
5
6
|
import type {
|
|
6
7
|
AgentUiAction,
|
|
@@ -20,6 +21,14 @@ export interface ChatLabels {
|
|
|
20
21
|
loadEarlier: string;
|
|
21
22
|
emptyConversation: string;
|
|
22
23
|
messagePlaceholder: string;
|
|
24
|
+
/** Screen-reader label of the send button. */
|
|
25
|
+
send: string;
|
|
26
|
+
/** The button back to the newest message. */
|
|
27
|
+
jumpToLatest: string;
|
|
28
|
+
/** The same with `{count}` teammate replies that arrived while scrolled up. */
|
|
29
|
+
jumpToLatestNew: string;
|
|
30
|
+
/** Last tile of a photo album, with `{count}` photos behind its "+N". */
|
|
31
|
+
morePhotos: string;
|
|
23
32
|
failedToSend: string;
|
|
24
33
|
/** After `failedToSend` on a bubble that can be re-sent. */
|
|
25
34
|
tapToRetry: string;
|
|
@@ -57,7 +66,10 @@ export interface ChatLabels {
|
|
|
57
66
|
openAttachment: string;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Composer attachment copy. Limits are already filled in; `remove`,
|
|
71
|
+
* `uploading` and `uploadFailed` keep `{name}`.
|
|
72
|
+
*/
|
|
61
73
|
export interface AttachLabels {
|
|
62
74
|
button: string;
|
|
63
75
|
photoLibrary: string;
|
|
@@ -70,6 +82,11 @@ export interface AttachLabels {
|
|
|
70
82
|
tooLargeTogether: string;
|
|
71
83
|
failed: string;
|
|
72
84
|
cameraDenied: string;
|
|
85
|
+
uploading: string;
|
|
86
|
+
uploadFailed: string;
|
|
87
|
+
uploadFailedNotice: string;
|
|
88
|
+
/** Under a file chip whose upload failed. */
|
|
89
|
+
retry: string;
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
export interface ChatViewProps {
|
|
@@ -100,6 +117,12 @@ export interface ChatViewProps {
|
|
|
100
117
|
attachmentSources?: AttachmentSource[];
|
|
101
118
|
/** Opens a picker and returns what fits beside `current`. Never rejects. */
|
|
102
119
|
onPickAttachments?: (source: AttachmentSource, current: DraftAttachment[]) => Promise<PickResult>;
|
|
120
|
+
/**
|
|
121
|
+
* Uploads a picked local file (`fileUri`) and resolves its upload token,
|
|
122
|
+
* with when it expires. Absent: such files go up when the message is sent
|
|
123
|
+
* instead.
|
|
124
|
+
*/
|
|
125
|
+
onUploadAttachment?: (draft: DraftAttachment, signal?: AbortSignal) => Promise<UploadedFile>;
|
|
103
126
|
/** Rejects when the send fails, so the composer can keep its attachments. */
|
|
104
127
|
onSendMessage: (
|
|
105
128
|
body: string,
|
|
@@ -117,6 +140,8 @@ export interface ChatViewProps {
|
|
|
117
140
|
) => Promise<ConversationRatingResult>;
|
|
118
141
|
/** The widget's default language — rating reasons fall back to it. */
|
|
119
142
|
defaultLocale?: string;
|
|
143
|
+
/** A message's photos share one album (default true; `boot({ photoAlbums })`). */
|
|
144
|
+
photoAlbums?: boolean;
|
|
120
145
|
onLoadMore: () => void;
|
|
121
146
|
onTyping: (isTyping: boolean) => void;
|
|
122
147
|
onBack: () => void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Props and label bundle for the Messages space. */
|
|
2
2
|
|
|
3
3
|
import type { Conversation } from '../../../types';
|
|
4
|
+
import type { LastMessageLabels } from '../../components/ConversationRow';
|
|
4
5
|
import type { RelativeTimeLabels } from '../../utils/conversations';
|
|
5
6
|
|
|
6
7
|
export interface ConversationsListLabels {
|
|
@@ -15,6 +16,7 @@ export interface ConversationsListLabels {
|
|
|
15
16
|
time: RelativeTimeLabels;
|
|
16
17
|
/** Template with a `{count}` token. */
|
|
17
18
|
unread: string;
|
|
19
|
+
lastMessage: LastMessageLabels;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface ConversationsListProps {
|
|
@@ -3,23 +3,23 @@
|
|
|
3
3
|
* the sheet so the component and the styles can never disagree about them.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { StyleSheet } from 'react-native';
|
|
7
7
|
|
|
8
|
-
import { rowDividerInset } from '../../components/ConversationRow';
|
|
9
8
|
import { fontSize, palette, radius, shadows, spacing } from '../../styles/theme';
|
|
10
9
|
import { START_ALIGN } from '../../utils/rtl';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Header geometry in dp, measured off the web widget so the two surfaces line
|
|
14
|
-
* up: a tall header with the greeting pinned low, a
|
|
13
|
+
* up: a tall header with the greeting pinned low, a first card that overhangs
|
|
15
14
|
* the seam by a fixed amount rather than half its height, and a fade that is a
|
|
16
15
|
* straight white ramp over the bottom of the header, starting just under the
|
|
17
16
|
* heading.
|
|
18
17
|
*/
|
|
19
18
|
export const HEADER_MIN_HEIGHT = 228;
|
|
20
|
-
/** Between the greeting and whatever comes next: the seam, or the
|
|
19
|
+
/** Between the greeting and whatever comes next: the seam, or the first card. */
|
|
21
20
|
export const HEADER_BOTTOM_PADDING = 36;
|
|
22
|
-
|
|
21
|
+
/** How far the first card (recent message, else the start card) rides up over the seam. */
|
|
22
|
+
export const LEAD_CARD_LIFT = 28;
|
|
23
23
|
export const START_CARD_MIN_HEIGHT = 76;
|
|
24
24
|
export const HEADER_FADE_HEIGHT = 90;
|
|
25
25
|
export const CLOSE_BUTTON_SIZE = 28;
|
|
@@ -38,7 +38,7 @@ export const styles = StyleSheet.create({
|
|
|
38
38
|
minHeight: HEADER_MIN_HEIGHT,
|
|
39
39
|
paddingHorizontal: spacing.xl,
|
|
40
40
|
paddingTop: spacing.xl,
|
|
41
|
-
// When
|
|
41
|
+
// When a card overhangs the seam the component grows this to clear it.
|
|
42
42
|
paddingBottom: HEADER_BOTTOM_PADDING,
|
|
43
43
|
overflow: 'hidden',
|
|
44
44
|
},
|
|
@@ -82,7 +82,7 @@ export const styles = StyleSheet.create({
|
|
|
82
82
|
lineHeight: fontSize.body * 1.4,
|
|
83
83
|
},
|
|
84
84
|
|
|
85
|
-
// Meets the header on a straight seam. No top padding here: the
|
|
85
|
+
// Meets the header on a straight seam. No top padding here: the first card
|
|
86
86
|
// is pulled up past this edge, and the rest of the content takes its spacing
|
|
87
87
|
// from `bodyContent`.
|
|
88
88
|
body: {
|
|
@@ -115,12 +115,36 @@ export const styles = StyleSheet.create({
|
|
|
115
115
|
color: palette.warningText,
|
|
116
116
|
},
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
marginTop: -
|
|
118
|
+
leadCards: {
|
|
119
|
+
marginTop: -LEAD_CARD_LIFT,
|
|
120
|
+
gap: spacing.md,
|
|
121
|
+
},
|
|
122
|
+
// White like the row inside it, so a press is the only thing that tints it,
|
|
123
|
+
// with the start card's border and shadow so the two read as a stack. No
|
|
124
|
+
// `overflow: hidden`: it would clip the shadow on iOS, and the row never
|
|
125
|
+
// reaches the rounded corners anyway.
|
|
126
|
+
recentCard: {
|
|
127
|
+
paddingTop: spacing.lg,
|
|
128
|
+
paddingBottom: spacing.sm,
|
|
129
|
+
borderRadius: radius.lg,
|
|
130
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
131
|
+
borderColor: palette.border,
|
|
132
|
+
backgroundColor: palette.white,
|
|
133
|
+
...shadows.card,
|
|
120
134
|
},
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
135
|
+
// Inset like the start card's content, so the title, the avatar and the
|
|
136
|
+
// start card's icon share one left edge.
|
|
137
|
+
recentTitle: {
|
|
138
|
+
textAlign: START_ALIGN,
|
|
139
|
+
paddingHorizontal: spacing.xl,
|
|
140
|
+
marginBottom: spacing.xs,
|
|
141
|
+
fontSize: fontSize.title,
|
|
142
|
+
fontWeight: '600',
|
|
143
|
+
color: palette.ink,
|
|
144
|
+
},
|
|
145
|
+
// Neutral and opaque, like the web card: as the first card it spans two
|
|
146
|
+
// backgrounds, and a translucent or brand-tinted fill would let the header
|
|
147
|
+
// bleed through its top or stain the seam. Shadowed because without it the half over the
|
|
124
148
|
// header reads as a flat cut-out.
|
|
125
149
|
startCard: {
|
|
126
150
|
flexDirection: 'row',
|
|
@@ -162,39 +186,6 @@ export const styles = StyleSheet.create({
|
|
|
162
186
|
marginTop: 2,
|
|
163
187
|
},
|
|
164
188
|
|
|
165
|
-
recentHeader: {
|
|
166
|
-
flexDirection: 'row',
|
|
167
|
-
alignItems: 'center',
|
|
168
|
-
justifyContent: 'space-between',
|
|
169
|
-
minHeight: 28,
|
|
170
|
-
marginBottom: spacing.sm,
|
|
171
|
-
},
|
|
172
|
-
recentTitle: {
|
|
173
|
-
textAlign: START_ALIGN,
|
|
174
|
-
fontSize: fontSize.caption,
|
|
175
|
-
fontWeight: '700',
|
|
176
|
-
color: palette.textMuted,
|
|
177
|
-
letterSpacing: I18nManager.isRTL ? 0 : 0.6,
|
|
178
|
-
textTransform: 'uppercase',
|
|
179
|
-
},
|
|
180
|
-
viewAllButton: {
|
|
181
|
-
minHeight: 32,
|
|
182
|
-
paddingHorizontal: spacing.xs,
|
|
183
|
-
justifyContent: 'center',
|
|
184
|
-
},
|
|
185
|
-
viewAllLabel: {
|
|
186
|
-
textAlign: START_ALIGN,
|
|
187
|
-
fontSize: fontSize.meta,
|
|
188
|
-
fontWeight: '600',
|
|
189
|
-
},
|
|
190
|
-
// Border rather than a shadow: `overflow: hidden` keeps the rows' ripple
|
|
191
|
-
// inside the rounded corners, and would clip a shadow on iOS anyway.
|
|
192
|
-
recentCard: {
|
|
193
|
-
overflow: 'hidden',
|
|
194
|
-
},
|
|
195
|
-
recentDivider: {
|
|
196
|
-
marginStart: rowDividerInset(spacing.lg),
|
|
197
|
-
},
|
|
198
189
|
// Absorbs the slack so the status strip stays pinned to the bottom, and
|
|
199
190
|
// centers the empty hint when there is nothing else to show.
|
|
200
191
|
spacer: {
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Home space — branded greeting header, the
|
|
3
|
-
*
|
|
2
|
+
* Home space — branded greeting header, the newest conversation in a "Recent
|
|
3
|
+
* message" card, the "start a conversation" card under it and a
|
|
4
|
+
* team/availability strip.
|
|
4
5
|
* Port of `client/src/widget/components/HomeView.tsx`.
|
|
5
6
|
*
|
|
6
7
|
* The header meets the white body on a straight seam, the same as the web
|
|
7
8
|
* widget. Whether that seam is a hard edge or the background washing out into
|
|
8
|
-
* white is the dashboard's `homeBackgroundFade` toggle, and the
|
|
9
|
-
*
|
|
9
|
+
* white is the dashboard's `homeBackgroundFade` toggle, and the first card (the
|
|
10
|
+
* recent message, or the start card when there is none) sits astride it either
|
|
11
|
+
* way: top over the brand background, bottom over white.
|
|
10
12
|
*
|
|
11
13
|
* Everything on this screen comes from the widget config or the conversations
|
|
12
14
|
* the visitor already has; nothing here assumes a field the backend does not
|
|
13
15
|
* send today.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
import {
|
|
18
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
17
19
|
import {
|
|
18
20
|
Animated,
|
|
19
21
|
Easing,
|
|
@@ -47,9 +49,9 @@ import {
|
|
|
47
49
|
HEADER_BOTTOM_PADDING,
|
|
48
50
|
HEADER_FADE_HEIGHT,
|
|
49
51
|
HEADER_MIN_HEIGHT,
|
|
52
|
+
LEAD_CARD_LIFT,
|
|
50
53
|
LOGO_HEIGHT,
|
|
51
54
|
LOGO_MAX_WIDTH,
|
|
52
|
-
START_CARD_LIFT,
|
|
53
55
|
styles,
|
|
54
56
|
} from './HomeView.styles';
|
|
55
57
|
import type { HomeViewProps } from './HomeView.types';
|
|
@@ -74,7 +76,7 @@ export function HomeView({
|
|
|
74
76
|
color,
|
|
75
77
|
visitorName,
|
|
76
78
|
conversationCount,
|
|
77
|
-
|
|
79
|
+
recentConversation,
|
|
78
80
|
unreadByConversation,
|
|
79
81
|
teamAgents,
|
|
80
82
|
isOnline,
|
|
@@ -86,7 +88,6 @@ export function HomeView({
|
|
|
86
88
|
labels,
|
|
87
89
|
onClose,
|
|
88
90
|
onStartConversation,
|
|
89
|
-
onViewConversations,
|
|
90
91
|
onOpenConversation,
|
|
91
92
|
}: HomeViewProps) {
|
|
92
93
|
const heading = personalizeGreeting(config?.welcomeHeading, visitorName, 'Hi there');
|
|
@@ -111,14 +112,14 @@ export function HomeView({
|
|
|
111
112
|
// colour over the bottom of the header, so the start card lands on near-white.
|
|
112
113
|
const fade = config?.homeBackgroundFade ? <FadeOverlay height={HEADER_FADE_HEIGHT} /> : null;
|
|
113
114
|
|
|
114
|
-
const
|
|
115
|
-
const
|
|
115
|
+
const recent = showPreviousConversations ? recentConversation : undefined;
|
|
116
|
+
const hasLeadCards = !!recent || allowNewConversations;
|
|
116
117
|
const showEmptyHint =
|
|
117
118
|
showPreviousConversations && conversationCount === 0 && allowNewConversations;
|
|
118
119
|
// One agent already has their face in the row above; repeating it reads as a bug.
|
|
119
120
|
const showAvatarStack = teamAgents.length >= 2;
|
|
120
121
|
|
|
121
|
-
// The
|
|
122
|
+
// The first card is pulled up over the seam by a fixed amount, so the header
|
|
122
123
|
// pads its bottom by that same amount and the greeting never slides under it.
|
|
123
124
|
// The header reaches up under the status bar (no white strip above it), so
|
|
124
125
|
// it grows by that inset and keeps its content where it was.
|
|
@@ -128,7 +129,7 @@ export function HomeView({
|
|
|
128
129
|
topInset > 0
|
|
129
130
|
? { paddingTop: spacing.xl + topInset, minHeight: HEADER_MIN_HEIGHT + topInset }
|
|
130
131
|
: null,
|
|
131
|
-
|
|
132
|
+
hasLeadCards ? { paddingBottom: HEADER_BOTTOM_PADDING + LEAD_CARD_LIFT } : null,
|
|
132
133
|
];
|
|
133
134
|
|
|
134
135
|
const headerContent = (
|
|
@@ -186,30 +187,53 @@ export function HomeView({
|
|
|
186
187
|
)}
|
|
187
188
|
|
|
188
189
|
<View style={styles.body}>
|
|
189
|
-
{
|
|
190
|
-
<View style={styles.
|
|
191
|
-
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
{hasLeadCards ? (
|
|
191
|
+
<View style={styles.leadCards}>
|
|
192
|
+
{recent ? (
|
|
193
|
+
<FadeSlideIn delay={40}>
|
|
194
|
+
<View style={styles.recentCard}>
|
|
195
|
+
<Text style={styles.recentTitle} accessibilityRole="header">
|
|
196
|
+
{labels.recentTitle}
|
|
197
|
+
</Text>
|
|
198
|
+
<ConversationRow
|
|
199
|
+
variant="card"
|
|
200
|
+
conversation={recent}
|
|
201
|
+
color={color}
|
|
202
|
+
fallbackName={labels.team}
|
|
203
|
+
timeLabels={labels.time}
|
|
204
|
+
unreadCount={unreadByConversation[recent.id] ?? 0}
|
|
205
|
+
unreadLabel={labels.unread}
|
|
206
|
+
lastMessageLabels={labels.lastMessage}
|
|
207
|
+
horizontalPadding={spacing.xl}
|
|
208
|
+
onPress={() => onOpenConversation(recent.id)}
|
|
209
|
+
/>
|
|
199
210
|
</View>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
</FadeSlideIn>
|
|
212
|
+
) : null}
|
|
213
|
+
{allowNewConversations ? (
|
|
214
|
+
<FadeSlideIn delay={80}>
|
|
215
|
+
<Pressable
|
|
216
|
+
onPress={onStartConversation}
|
|
217
|
+
accessibilityRole="button"
|
|
218
|
+
android_ripple={{ color: palette.borderSoft }}
|
|
219
|
+
style={({ pressed }) => [styles.startCard, pressed && styles.startCardPressed]}>
|
|
220
|
+
<View style={[styles.startIcon, { backgroundColor: color }]}>
|
|
221
|
+
<ChatIcon size={18} color={startIconColor} />
|
|
222
|
+
</View>
|
|
223
|
+
<View style={styles.startCopy}>
|
|
224
|
+
<Text style={styles.startTitle}>{startConversationLabel}</Text>
|
|
225
|
+
<Text style={styles.startSubtitle}>{replyTimeLabel}</Text>
|
|
226
|
+
</View>
|
|
227
|
+
<ArrowRightIcon size={18} color={color} style={flipForRTL()} />
|
|
228
|
+
</Pressable>
|
|
229
|
+
</FadeSlideIn>
|
|
230
|
+
) : null}
|
|
207
231
|
</View>
|
|
208
232
|
) : null}
|
|
209
233
|
|
|
210
234
|
<View style={styles.bodyContent}>
|
|
211
235
|
{offlineMessage ? (
|
|
212
|
-
<FadeSlideIn delay={
|
|
236
|
+
<FadeSlideIn delay={110}>
|
|
213
237
|
<View style={styles.offlineNotice}>
|
|
214
238
|
<AlertIcon size={18} />
|
|
215
239
|
<Text style={styles.offlineText}>{offlineMessage}</Text>
|
|
@@ -217,46 +241,6 @@ export function HomeView({
|
|
|
217
241
|
</FadeSlideIn>
|
|
218
242
|
) : null}
|
|
219
243
|
|
|
220
|
-
{showRecent ? (
|
|
221
|
-
<FadeSlideIn delay={140}>
|
|
222
|
-
<View style={styles.recentHeader}>
|
|
223
|
-
<Text style={styles.recentTitle}>{labels.recentTitle}</Text>
|
|
224
|
-
{showViewAll ? (
|
|
225
|
-
<Pressable
|
|
226
|
-
onPress={onViewConversations}
|
|
227
|
-
accessibilityRole="button"
|
|
228
|
-
accessibilityLabel={labels.viewPrevious}
|
|
229
|
-
hitSlop={10}
|
|
230
|
-
style={({ pressed }) => [
|
|
231
|
-
styles.viewAllButton,
|
|
232
|
-
pressed && globalStyles.pressed,
|
|
233
|
-
]}>
|
|
234
|
-
<Text style={[styles.viewAllLabel, { color }]}>{labels.viewAll}</Text>
|
|
235
|
-
</Pressable>
|
|
236
|
-
) : null}
|
|
237
|
-
</View>
|
|
238
|
-
<View style={[globalStyles.card, styles.recentCard]}>
|
|
239
|
-
{recentConversations.map((conversation, index) => (
|
|
240
|
-
<Fragment key={conversation.id}>
|
|
241
|
-
{index > 0 ? (
|
|
242
|
-
<View style={[globalStyles.divider, styles.recentDivider]} />
|
|
243
|
-
) : null}
|
|
244
|
-
<ConversationRow
|
|
245
|
-
conversation={conversation}
|
|
246
|
-
color={color}
|
|
247
|
-
fallbackName={labels.team}
|
|
248
|
-
timeLabels={labels.time}
|
|
249
|
-
unreadCount={unreadByConversation[conversation.id] ?? 0}
|
|
250
|
-
unreadLabel={labels.unread}
|
|
251
|
-
horizontalPadding={spacing.lg}
|
|
252
|
-
onPress={() => onOpenConversation(conversation.id)}
|
|
253
|
-
/>
|
|
254
|
-
</Fragment>
|
|
255
|
-
))}
|
|
256
|
-
</View>
|
|
257
|
-
</FadeSlideIn>
|
|
258
|
-
) : null}
|
|
259
|
-
|
|
260
244
|
<View style={styles.spacer}>
|
|
261
245
|
{showEmptyHint ? (
|
|
262
246
|
<FadeSlideIn delay={140}>
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/** Props and label bundle for the Home space. */
|
|
2
2
|
|
|
3
3
|
import type { Conversation, WidgetConfig } from '../../../types';
|
|
4
|
+
import type { LastMessageLabels } from '../../components/ConversationRow';
|
|
4
5
|
import type { RelativeTimeLabels, TeamAgent } from '../../utils/conversations';
|
|
5
6
|
|
|
6
7
|
export interface HomeViewLabels {
|
|
7
8
|
close: string;
|
|
8
|
-
/**
|
|
9
|
-
viewPrevious: string;
|
|
10
|
-
viewAll: string;
|
|
9
|
+
/** Heading inside the "Recent message" card. */
|
|
11
10
|
recentTitle: string;
|
|
12
11
|
noConversations: string;
|
|
13
12
|
statusOnline: string;
|
|
@@ -17,6 +16,7 @@ export interface HomeViewLabels {
|
|
|
17
16
|
time: RelativeTimeLabels;
|
|
18
17
|
/** Template with a `{count}` token. */
|
|
19
18
|
unread: string;
|
|
19
|
+
lastMessage: LastMessageLabels;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface HomeViewProps {
|
|
@@ -24,8 +24,8 @@ export interface HomeViewProps {
|
|
|
24
24
|
color: string;
|
|
25
25
|
visitorName?: string;
|
|
26
26
|
conversationCount: number;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/** The newest conversation, picked by the provider; none before the first one. */
|
|
28
|
+
recentConversation?: Conversation;
|
|
29
29
|
unreadByConversation: Record<string, number>;
|
|
30
30
|
teamAgents: TeamAgent[];
|
|
31
31
|
isOnline: boolean;
|
|
@@ -37,6 +37,5 @@ export interface HomeViewProps {
|
|
|
37
37
|
labels: HomeViewLabels;
|
|
38
38
|
onClose: () => void;
|
|
39
39
|
onStartConversation: () => void;
|
|
40
|
-
onViewConversations: () => void;
|
|
41
40
|
onOpenConversation: (conversationId: string) => void;
|
|
42
41
|
}
|
package/src/ui/utils/html.ts
CHANGED
|
@@ -42,6 +42,32 @@ export function stripHtml(html: string): string {
|
|
|
42
42
|
.trim();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Agent reply HTML → the Markdown subset `Markdown` draws: paragraphs, line
|
|
47
|
+
* breaks, lists, bold, italic, inline code and links. Other tags keep their text.
|
|
48
|
+
*/
|
|
49
|
+
export function htmlToMarkdown(html: string): string {
|
|
50
|
+
if (!html) return '';
|
|
51
|
+
return decodeEntities(
|
|
52
|
+
html
|
|
53
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
54
|
+
.replace(/<ol\b[^>]*>([\s\S]*?)<\/ol>/gi, (_list, items: string) => {
|
|
55
|
+
let number = 0;
|
|
56
|
+
return `\n\n${items.replace(/<li\b[^>]*>/gi, () => `\n${++number}. `)}\n\n`;
|
|
57
|
+
})
|
|
58
|
+
.replace(/<li\b[^>]*>/gi, '\n- ')
|
|
59
|
+
.replace(/<\/(p|div|h[1-6]|blockquote|ul|tr)>/gi, '\n\n')
|
|
60
|
+
.replace(/<(strong|b)\b[^>]*>([\s\S]+?)<\/\1>/gi, '**$2**')
|
|
61
|
+
.replace(/<(em|i)\b[^>]*>([\s\S]+?)<\/\1>/gi, '*$2*')
|
|
62
|
+
.replace(/<code\b[^>]*>([\s\S]+?)<\/code>/gi, '`$1`')
|
|
63
|
+
.replace(/<a\b[^>]*?\bhref=(["'])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi, '[$3]($2)')
|
|
64
|
+
.replace(/<[^>]+>/g, '')
|
|
65
|
+
)
|
|
66
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
67
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
68
|
+
.trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
45
71
|
/** Single-line flattening used for campaign cards and list snippets. */
|
|
46
72
|
export function htmlToText(html: string): string {
|
|
47
73
|
if (!html) return '';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lets images load a few at a time, highest priority first. The chat runs its
|
|
3
|
+
* large images through it: Android holds a whole download in a memory pool
|
|
4
|
+
* capped at three quarters of the app's heap (192 MB on a 256 MB phone), so
|
|
5
|
+
* several 30–50 MB photos at once overflow it and every one of them fails.
|
|
6
|
+
*
|
|
7
|
+
* No `react-native` import on purpose: it stays testable in plain Node.
|
|
8
|
+
*/
|
|
9
|
+
export class ImageLoadQueue {
|
|
10
|
+
// Key → priority, for both: an image paused off screen waits again at its own.
|
|
11
|
+
private readonly waiting = new Map<string, number>();
|
|
12
|
+
private readonly loading = new Map<string, number>();
|
|
13
|
+
private readonly done = new Set<string>();
|
|
14
|
+
/** The images on screen; null until the list says, so all of them count. */
|
|
15
|
+
private onScreen: ReadonlySet<string> | null = null;
|
|
16
|
+
private readonly listeners = new Set<() => void>();
|
|
17
|
+
private scheduled = false;
|
|
18
|
+
|
|
19
|
+
constructor(private readonly concurrency: number) {}
|
|
20
|
+
|
|
21
|
+
/** Queues a mounted image; the returned cleanup gives up its place. */
|
|
22
|
+
add(key: string, priority: number): () => void {
|
|
23
|
+
if (!this.done.has(key) && !this.loading.has(key)) {
|
|
24
|
+
this.waiting.set(key, Number.isFinite(priority) ? priority : 0);
|
|
25
|
+
this.schedule();
|
|
26
|
+
}
|
|
27
|
+
return () => this.remove(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Loaded or failed: it keeps showing, and the next image may start. */
|
|
31
|
+
finish(key: string): void {
|
|
32
|
+
this.waiting.delete(key);
|
|
33
|
+
this.done.add(key);
|
|
34
|
+
if (this.loading.delete(key)) this.advance();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
canLoad = (key: string): boolean => this.done.has(key) || this.loading.has(key);
|
|
38
|
+
|
|
39
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
40
|
+
this.listeners.add(listener);
|
|
41
|
+
return () => {
|
|
42
|
+
this.listeners.delete(listener);
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The images now on screen. One that scrolls off before it has loaded
|
|
48
|
+
* gives up its place and waits, so it can't hold up the ones in view:
|
|
49
|
+
* Android detaches a row scrolled out of view, and a detached image never
|
|
50
|
+
* finishes loading. Only images on screen are started.
|
|
51
|
+
*/
|
|
52
|
+
setOnScreen(keys: Iterable<string>): void {
|
|
53
|
+
this.onScreen = new Set(keys);
|
|
54
|
+
let paused = false;
|
|
55
|
+
this.loading.forEach((priority, key) => {
|
|
56
|
+
if (this.onScreen!.has(key)) return;
|
|
57
|
+
this.loading.delete(key);
|
|
58
|
+
this.waiting.set(key, priority);
|
|
59
|
+
paused = true;
|
|
60
|
+
});
|
|
61
|
+
if (!this.advance() && paused) this.notify();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Scrolled out of the list before it loaded: it queues again if it comes back. */
|
|
65
|
+
private remove(key: string): void {
|
|
66
|
+
this.waiting.delete(key);
|
|
67
|
+
if (this.loading.delete(key)) this.advance();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hands out places once the render that mounted an image is done, so all
|
|
72
|
+
* the images mounted together are queued first, whatever order they
|
|
73
|
+
* mounted in.
|
|
74
|
+
*/
|
|
75
|
+
private schedule(): void {
|
|
76
|
+
if (this.scheduled) return;
|
|
77
|
+
this.scheduled = true;
|
|
78
|
+
Promise.resolve().then(() => {
|
|
79
|
+
this.scheduled = false;
|
|
80
|
+
this.advance();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Starts what fits; true when anything started (listeners were told). */
|
|
85
|
+
private advance(): boolean {
|
|
86
|
+
let started = false;
|
|
87
|
+
while (this.loading.size < this.concurrency) {
|
|
88
|
+
let next: string | null = null;
|
|
89
|
+
let best = -Infinity;
|
|
90
|
+
this.waiting.forEach((priority, key) => {
|
|
91
|
+
if (priority > best && (!this.onScreen || this.onScreen.has(key))) {
|
|
92
|
+
best = priority;
|
|
93
|
+
next = key;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (next === null) break;
|
|
97
|
+
this.waiting.delete(next);
|
|
98
|
+
this.loading.set(next, best);
|
|
99
|
+
started = true;
|
|
100
|
+
}
|
|
101
|
+
if (started) this.notify();
|
|
102
|
+
return started;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private notify(): void {
|
|
106
|
+
this.listeners.forEach((listener) => listener());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent replies with images between their paragraphs. Workstation marks such
|
|
3
|
+
* images `inline` among the message's attachments and keeps an `<img>` for
|
|
4
|
+
* each in the HTML body, where the teammate put it. Same split as the web
|
|
5
|
+
* Messenger, so both show the text and images in the order they were written.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WidgetAttachment } from '../../types';
|
|
9
|
+
import { htmlToMarkdown } from './html';
|
|
10
|
+
|
|
11
|
+
/** Markdown for the bubble to draw, or one attachment. */
|
|
12
|
+
export type MessagePart = { text: string } | { attachment: WidgetAttachment };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The body and its inline images in order, plus the other attachments
|
|
16
|
+
* (`files`) that show after them. Null when nothing is inline: the bubble then
|
|
17
|
+
* shows the body, followed by every attachment.
|
|
18
|
+
*
|
|
19
|
+
* An `<img>` finds its attachment by the id in its URL. In a body whose
|
|
20
|
+
* images carry no ids, they take the attachments in order instead; in one
|
|
21
|
+
* that does, an image without an id (a logo, an emoji) isn't one of the files
|
|
22
|
+
* and takes nothing. One whose id names no remaining attachment (the same file
|
|
23
|
+
* twice) takes nothing either. Inline attachments with no `<img>` go at the end.
|
|
24
|
+
*/
|
|
25
|
+
export function splitInlineAttachments(
|
|
26
|
+
body: string | null | undefined,
|
|
27
|
+
attachments: WidgetAttachment[] | null | undefined
|
|
28
|
+
): { parts: MessagePart[]; files: WidgetAttachment[] } | null {
|
|
29
|
+
const inline = (attachments ?? []).filter(isInline);
|
|
30
|
+
if (!body || inline.length === 0) return null;
|
|
31
|
+
|
|
32
|
+
const remaining = [...inline];
|
|
33
|
+
const parts: MessagePart[] = [];
|
|
34
|
+
const pushText = (html: string) => {
|
|
35
|
+
const text = htmlToMarkdown(html);
|
|
36
|
+
if (text) parts.push({ text });
|
|
37
|
+
};
|
|
38
|
+
const image = /<img\b[^>]*?\bsrc=(["'])(.*?)\1[^>]*>/gi;
|
|
39
|
+
const images: Array<{ start: number; end: number; id: string | null }> = [];
|
|
40
|
+
for (let match = image.exec(body); match; match = image.exec(body)) {
|
|
41
|
+
images.push({ start: match.index, end: image.lastIndex, id: mediaId(match[2] ?? '') });
|
|
42
|
+
}
|
|
43
|
+
const byId = images.some((found) => found.id !== null);
|
|
44
|
+
let last = 0;
|
|
45
|
+
for (const { start, end, id } of images) {
|
|
46
|
+
// Left in the text, which drops it as it always has.
|
|
47
|
+
if (byId && !id) continue;
|
|
48
|
+
pushText(body.slice(last, start));
|
|
49
|
+
last = end;
|
|
50
|
+
const index = id ? remaining.findIndex((attachment) => attachment.id === id) : 0;
|
|
51
|
+
if (index < 0) continue;
|
|
52
|
+
const [attachment] = remaining.splice(index, 1);
|
|
53
|
+
if (attachment) parts.push({ attachment });
|
|
54
|
+
}
|
|
55
|
+
pushText(body.slice(last));
|
|
56
|
+
remaining.forEach((attachment) => parts.push({ attachment }));
|
|
57
|
+
return { parts, files: (attachments ?? []).filter((attachment) => !isInline(attachment)) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Workstation's media URLs carry the attachment's id, as
|
|
62
|
+
* `/api/widget/media/<id>?…` (what replies carry) or `/api/media/<id>/download`.
|
|
63
|
+
*/
|
|
64
|
+
function mediaId(src: string): string | null {
|
|
65
|
+
return /\/api\/(?:widget\/)?media\/([^/?#"'&]+)/.exec(src)?.[1] ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Only Workstation's agent replies set `inline`, so it isn't on `WidgetAttachment`. */
|
|
69
|
+
function isInline(attachment: WidgetAttachment): boolean {
|
|
70
|
+
return (attachment as { inline?: unknown }).inline === true;
|
|
71
|
+
}
|