@repzo/widget-react-native 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +179 -0
- package/README.md +72 -16
- package/lib/commonjs/api/client.js +113 -0
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/attachments/file-size.js +52 -0
- package/lib/commonjs/attachments/file-size.js.map +1 -0
- package/lib/commonjs/attachments/local-previews.js +40 -0
- package/lib/commonjs/attachments/local-previews.js.map +1 -0
- package/lib/commonjs/attachments/pickers.js +76 -28
- package/lib/commonjs/attachments/pickers.js.map +1 -1
- package/lib/commonjs/i18n.js +24 -3
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +13 -3
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +486 -63
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/storage.js +3 -1
- package/lib/commonjs/storage.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +40 -16
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/AttachmentChips.js +109 -40
- package/lib/commonjs/ui/components/AttachmentChips.js.map +1 -1
- package/lib/commonjs/ui/components/ConversationRow.js +65 -10
- package/lib/commonjs/ui/components/ConversationRow.js.map +1 -1
- package/lib/commonjs/ui/components/ImageViewer.js +231 -25
- package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
- package/lib/commonjs/ui/components/RatingCard.js +5 -2
- package/lib/commonjs/ui/components/RatingCard.js.map +1 -1
- package/lib/commonjs/ui/components/ZoomableImage.js +58 -10
- package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -1
- package/lib/commonjs/ui/components/icons.js +22 -16
- package/lib/commonjs/ui/components/icons.js.map +1 -1
- package/lib/commonjs/ui/hooks/use-retained-state.js +39 -0
- package/lib/commonjs/ui/hooks/use-retained-state.js.map +1 -0
- package/lib/commonjs/ui/screens/chat/ChatView.js +666 -171
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +150 -3
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/commonjs/ui/screens/conversations/ConversationsList.js +1 -0
- package/lib/commonjs/ui/screens/conversations/ConversationsList.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.js +38 -55
- package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.styles.js +36 -45
- package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/commonjs/ui/styles/theme.js +1 -0
- package/lib/commonjs/ui/styles/theme.js.map +1 -1
- package/lib/commonjs/ui/utils/conversations.js +17 -1
- package/lib/commonjs/ui/utils/conversations.js.map +1 -1
- package/lib/commonjs/ui/utils/html.js +13 -0
- package/lib/commonjs/ui/utils/html.js.map +1 -1
- package/lib/commonjs/ui/utils/image-load-queue.js +114 -0
- package/lib/commonjs/ui/utils/image-load-queue.js.map +1 -0
- package/lib/commonjs/ui/utils/inline-attachments.js +88 -0
- package/lib/commonjs/ui/utils/inline-attachments.js.map +1 -0
- package/lib/module/api/client.js +113 -0
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/attachments/file-size.js +47 -0
- package/lib/module/attachments/file-size.js.map +1 -0
- package/lib/module/attachments/local-previews.js +33 -0
- package/lib/module/attachments/local-previews.js.map +1 -0
- package/lib/module/attachments/pickers.js +74 -27
- package/lib/module/attachments/pickers.js.map +1 -1
- package/lib/module/i18n.js +24 -3
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +13 -3
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/widget-store.js +488 -65
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/storage.js +2 -0
- package/lib/module/storage.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +40 -16
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/AttachmentChips.js +111 -42
- package/lib/module/ui/components/AttachmentChips.js.map +1 -1
- package/lib/module/ui/components/ConversationRow.js +65 -10
- package/lib/module/ui/components/ConversationRow.js.map +1 -1
- package/lib/module/ui/components/ImageViewer.js +233 -27
- package/lib/module/ui/components/ImageViewer.js.map +1 -1
- package/lib/module/ui/components/RatingCard.js +5 -2
- package/lib/module/ui/components/RatingCard.js.map +1 -1
- package/lib/module/ui/components/ZoomableImage.js +58 -10
- package/lib/module/ui/components/ZoomableImage.js.map +1 -1
- package/lib/module/ui/components/icons.js +21 -15
- package/lib/module/ui/components/icons.js.map +1 -1
- package/lib/module/ui/hooks/use-retained-state.js +34 -0
- package/lib/module/ui/hooks/use-retained-state.js.map +1 -0
- package/lib/module/ui/screens/chat/ChatView.js +672 -177
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +152 -4
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/ui/screens/conversations/ConversationsList.js +1 -0
- package/lib/module/ui/screens/conversations/ConversationsList.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.js +40 -57
- package/lib/module/ui/screens/home/HomeView.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.styles.js +36 -45
- package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/module/ui/styles/theme.js +1 -0
- package/lib/module/ui/styles/theme.js.map +1 -1
- package/lib/module/ui/utils/conversations.js +15 -1
- package/lib/module/ui/utils/conversations.js.map +1 -1
- package/lib/module/ui/utils/html.js +12 -0
- package/lib/module/ui/utils/html.js.map +1 -1
- package/lib/module/ui/utils/image-load-queue.js +109 -0
- package/lib/module/ui/utils/image-load-queue.js.map +1 -0
- package/lib/module/ui/utils/inline-attachments.js +85 -0
- package/lib/module/ui/utils/inline-attachments.js.map +1 -0
- package/lib/typescript/commonjs/api/client.d.ts +35 -1
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/attachments/file-size.d.ts +15 -0
- package/lib/typescript/commonjs/attachments/file-size.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachments/local-previews.d.ts +14 -0
- package/lib/typescript/commonjs/attachments/local-previews.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachments/pickers.d.ts +35 -10
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +13 -3
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +67 -4
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage.d.ts +2 -0
- package/lib/typescript/commonjs/storage.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +58 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts +13 -3
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts +15 -1
- package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +26 -6
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +10 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts +11 -0
- package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +4 -4
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +154 -2
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +30 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -5
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +30 -35
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts +5 -6
- package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/styles/theme.d.ts +2 -0
- package/lib/typescript/commonjs/ui/styles/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/conversations.d.ts +10 -2
- package/lib/typescript/commonjs/ui/utils/conversations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/html.d.ts +5 -0
- package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts +44 -0
- package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts +29 -0
- package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts.map +1 -0
- package/lib/typescript/module/api/client.d.ts +35 -1
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/attachments/file-size.d.ts +15 -0
- package/lib/typescript/module/attachments/file-size.d.ts.map +1 -0
- package/lib/typescript/module/attachments/local-previews.d.ts +14 -0
- package/lib/typescript/module/attachments/local-previews.d.ts.map +1 -0
- package/lib/typescript/module/attachments/pickers.d.ts +35 -10
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +13 -3
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +67 -4
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/storage.d.ts +2 -0
- package/lib/typescript/module/storage.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +58 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts +13 -3
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ConversationRow.d.ts +15 -1
- package/lib/typescript/module/ui/components/ConversationRow.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ImageViewer.d.ts +26 -6
- package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts +10 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/icons.d.ts +2 -1
- package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/use-retained-state.d.ts +11 -0
- package/lib/typescript/module/ui/hooks/use-retained-state.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +4 -4
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +154 -2
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +30 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -5
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +30 -35
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts +5 -6
- package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/styles/theme.d.ts +2 -0
- package/lib/typescript/module/ui/styles/theme.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/conversations.d.ts +10 -2
- package/lib/typescript/module/ui/utils/conversations.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/html.d.ts +5 -0
- package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/image-load-queue.d.ts +44 -0
- package/lib/typescript/module/ui/utils/image-load-queue.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/inline-attachments.d.ts +29 -0
- package/lib/typescript/module/ui/utils/inline-attachments.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/api/client.ts +148 -2
- package/src/attachments/file-size.ts +42 -0
- package/src/attachments/local-previews.ts +32 -0
- package/src/attachments/pickers.ts +90 -30
- package/src/i18n.ts +24 -3
- package/src/locales.ts +13 -3
- package/src/state/widget-store.ts +519 -70
- package/src/storage.ts +2 -0
- package/src/types.ts +58 -0
- package/src/ui/WidgetProvider.tsx +50 -12
- package/src/ui/components/AttachmentChips.tsx +100 -29
- package/src/ui/components/ConversationRow.tsx +90 -9
- package/src/ui/components/ImageViewer.tsx +250 -28
- package/src/ui/components/RatingCard.tsx +5 -2
- package/src/ui/components/ZoomableImage.tsx +58 -5
- package/src/ui/components/icons.tsx +10 -12
- package/src/ui/hooks/use-retained-state.ts +35 -0
- package/src/ui/screens/chat/ChatView.styles.ts +151 -3
- package/src/ui/screens/chat/ChatView.tsx +760 -213
- package/src/ui/screens/chat/ChatView.types.ts +30 -1
- package/src/ui/screens/conversations/ConversationsList.tsx +1 -0
- package/src/ui/screens/conversations/ConversationsList.types.ts +2 -0
- package/src/ui/screens/home/HomeView.styles.ts +36 -45
- package/src/ui/screens/home/HomeView.tsx +54 -70
- package/src/ui/screens/home/HomeView.types.ts +5 -6
- package/src/ui/styles/theme.ts +6 -0
- package/src/ui/utils/conversations.ts +16 -2
- package/src/ui/utils/html.ts +26 -0
- package/src/ui/utils/image-load-queue.ts +108 -0
- package/src/ui/utils/inline-attachments.ts +71 -0
package/src/storage.ts
CHANGED
|
@@ -20,6 +20,8 @@ export const TOKEN_KEY = 'repzo_widget_token';
|
|
|
20
20
|
export const USER_KEY = 'repzo_widget_user_key';
|
|
21
21
|
export const DEVICE_ID_KEY = 'repzo_widget_device_id';
|
|
22
22
|
export const UNREAD_KEY = 'repzo_widget_unread';
|
|
23
|
+
/** Who sent each conversation's newest message; Workstation's list doesn't say. */
|
|
24
|
+
export const LAST_MESSAGE_KEY = 'repzo_widget_last_message';
|
|
23
25
|
/** Last config received — repaints the real theme instantly on relaunch. */
|
|
24
26
|
/**
|
|
25
27
|
* v2 wraps the config with the channel/baseUrl/platform it came from. v1 held a
|
package/src/types.ts
CHANGED
|
@@ -64,6 +64,12 @@ export interface WidgetConfig {
|
|
|
64
64
|
proactiveFrequency: 'session' | 'visitor';
|
|
65
65
|
proactiveOpenMode: 'home' | 'new_message';
|
|
66
66
|
attachmentsEnabled: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Largest file a visitor may attach. Set by servers that take uploads
|
|
69
|
+
* (`/api/widget/uploads`); absent on older ones, which only take files
|
|
70
|
+
* inline in the message.
|
|
71
|
+
*/
|
|
72
|
+
maxAttachmentBytes?: number;
|
|
67
73
|
analyticsEnabled: boolean;
|
|
68
74
|
toursEnabled: boolean;
|
|
69
75
|
defaultLocale: string;
|
|
@@ -94,7 +100,12 @@ export interface WidgetAttachment {
|
|
|
94
100
|
*/
|
|
95
101
|
export interface OutgoingAttachment {
|
|
96
102
|
name: string;
|
|
103
|
+
/** A base64 data URL; empty when `fileUri` is set. */
|
|
97
104
|
url: string;
|
|
105
|
+
/** A local file uploaded before the message is sent (servers with `maxAttachmentBytes`). */
|
|
106
|
+
fileUri?: string;
|
|
107
|
+
/** `fileUri` is already uploaded under this token, so the send skips uploading it. */
|
|
108
|
+
uploadToken?: string;
|
|
98
109
|
contentType: string;
|
|
99
110
|
/** Decoded bytes. */
|
|
100
111
|
size: number;
|
|
@@ -107,6 +118,21 @@ export interface Conversation {
|
|
|
107
118
|
subject: string | null;
|
|
108
119
|
status: string;
|
|
109
120
|
snippet: string | null;
|
|
121
|
+
/**
|
|
122
|
+
* Set when the newest message had no words, only photos (`'photo'`) or
|
|
123
|
+
* other files (`'file'`); the lists then say "Photo" or "File", while
|
|
124
|
+
* `snippet` keeps Workstation's wording ("2 attachments"). `null` when it
|
|
125
|
+
* had words. Filled in from what this device sent, received or loaded.
|
|
126
|
+
* @since 1.4.0
|
|
127
|
+
*/
|
|
128
|
+
lastMessageAttachment?: 'photo' | 'file' | null;
|
|
129
|
+
/**
|
|
130
|
+
* The newest message is the visitor's own; the lists put "You:" before it.
|
|
131
|
+
* A Workstation list that includes it is used as is; otherwise the SDK fills
|
|
132
|
+
* it in from what this device sent, received or loaded.
|
|
133
|
+
* @since 1.4.0
|
|
134
|
+
*/
|
|
135
|
+
lastMessageFromVisitor?: boolean | null;
|
|
110
136
|
messageCount: number;
|
|
111
137
|
lastMessageAt: string | null;
|
|
112
138
|
createdAt: string;
|
|
@@ -136,6 +162,14 @@ export interface Message {
|
|
|
136
162
|
createdAt: string;
|
|
137
163
|
/** Set when a teammate deleted the message; the content fields are then empty. */
|
|
138
164
|
deletedAt?: string | null;
|
|
165
|
+
/**
|
|
166
|
+
* On the visitor's own messages: the id this device sent it under, which
|
|
167
|
+
* stays the same when a failed message is retried. Lets a stored message
|
|
168
|
+
* replace its pending or failed bubble. Only newer Workstation deployments
|
|
169
|
+
* send it.
|
|
170
|
+
* @since 1.3.1
|
|
171
|
+
*/
|
|
172
|
+
clientMessageId?: string | null;
|
|
139
173
|
}
|
|
140
174
|
|
|
141
175
|
export interface OutboundMessage {
|
|
@@ -250,6 +284,22 @@ export interface BootOptions {
|
|
|
250
284
|
* @since 1.2.0
|
|
251
285
|
*/
|
|
252
286
|
posts?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* Show two or more photos in one message together as an album: one square
|
|
289
|
+
* holding them all, with "+N" on the last tile past four (default true).
|
|
290
|
+
* False shows each photo on its own, full width, as before 1.4.0. Either
|
|
291
|
+
* way, tapping a photo opens the message's photos in the viewer.
|
|
292
|
+
* @since 1.4.0
|
|
293
|
+
*/
|
|
294
|
+
photoAlbums?: boolean;
|
|
295
|
+
/**
|
|
296
|
+
* Put "You: " before a conversation's newest message on Home and in
|
|
297
|
+
* Messages when the visitor sent it (default true; Arabic "أنت: ", the
|
|
298
|
+
* `lastMessageYou` translation key). False shows the message alone, as
|
|
299
|
+
* before 1.4.0. `Conversation.lastMessageFromVisitor` is filled in either way.
|
|
300
|
+
* @since 1.4.0
|
|
301
|
+
*/
|
|
302
|
+
youPrefix?: boolean;
|
|
253
303
|
}
|
|
254
304
|
|
|
255
305
|
/** An option in the composer's attach menu. */
|
|
@@ -286,6 +336,14 @@ export interface RepzoWidgetAttachmentOptions {
|
|
|
286
336
|
pick?: (source: AttachmentSource) => Promise<RepzoPickedFile[] | null>;
|
|
287
337
|
/** Menu options offered with `pick`. Default all three. Ignored without `pick`. */
|
|
288
338
|
sources?: AttachmentSource[];
|
|
339
|
+
/**
|
|
340
|
+
* Default true: with a Workstation that takes uploads, a picked file starts
|
|
341
|
+
* uploading right away, while the visitor writes, so the message goes out
|
|
342
|
+
* the moment they tap Send. False uploads the files only when they tap
|
|
343
|
+
* Send, so nothing leaves the phone for a file they remove first.
|
|
344
|
+
* @since 1.4.0
|
|
345
|
+
*/
|
|
346
|
+
uploadOnPick?: boolean;
|
|
289
347
|
}
|
|
290
348
|
|
|
291
349
|
// ─── Agent UI blocks ────────────────────────────────────────────────────────
|
|
@@ -25,11 +25,12 @@ import {
|
|
|
25
25
|
MAX_ATTACHMENT_BYTES,
|
|
26
26
|
MAX_ATTACHMENTS,
|
|
27
27
|
pickAttachments,
|
|
28
|
+
type DraftAttachment,
|
|
28
29
|
} from '../attachments/pickers';
|
|
29
30
|
import { controller } from '../controller';
|
|
30
31
|
import { buildLabels, createTranslator, formatLabel, localizeWelcomeText } from '../i18n';
|
|
31
32
|
import { useWidgetState } from '../state/use-widget-state';
|
|
32
|
-
import type { AgentUiAction, OutgoingAttachment, PostInteractionKind, WidgetSpaceType } from '../types';
|
|
33
|
+
import type { AgentUiAction, AttachmentSource, OutgoingAttachment, PostInteractionKind, WidgetSpaceType } from '../types';
|
|
33
34
|
import { WidgetErrorBoundary } from './components/ErrorBoundary';
|
|
34
35
|
import { AlertIcon } from './components/icons';
|
|
35
36
|
import { InAppNotification } from './components/InAppNotification';
|
|
@@ -103,9 +104,9 @@ export function RepzoWidgetProvider({
|
|
|
103
104
|
[state.config, state.locale]
|
|
104
105
|
);
|
|
105
106
|
|
|
106
|
-
// Home previews the
|
|
107
|
-
const
|
|
108
|
-
() => pickRecentConversations(state.conversations,
|
|
107
|
+
// Home previews the newest thread and credits the agents behind the recent ones.
|
|
108
|
+
const recentConversation = useMemo(
|
|
109
|
+
() => pickRecentConversations(state.conversations, 1)[0],
|
|
109
110
|
[state.conversations]
|
|
110
111
|
);
|
|
111
112
|
const teamAgents = useMemo(() => pickTeamAgents(state.conversations, 3), [state.conversations]);
|
|
@@ -131,6 +132,15 @@ export function RepzoWidgetProvider({
|
|
|
131
132
|
}),
|
|
132
133
|
[labels]
|
|
133
134
|
);
|
|
135
|
+
const youPrefix = store.showsYouPrefix();
|
|
136
|
+
const lastMessageLabels = useMemo(
|
|
137
|
+
() => ({
|
|
138
|
+
photo: labels.lastMessagePhoto,
|
|
139
|
+
file: labels.lastMessageFile,
|
|
140
|
+
you: youPrefix ? labels.lastMessageYou : '',
|
|
141
|
+
}),
|
|
142
|
+
[labels, youPrefix]
|
|
143
|
+
);
|
|
134
144
|
|
|
135
145
|
const blockLabels = useMemo(
|
|
136
146
|
() => ({
|
|
@@ -163,13 +173,19 @@ export function RepzoWidgetProvider({
|
|
|
163
173
|
file: labels.attachChooseFile,
|
|
164
174
|
cancel: labels.cancel,
|
|
165
175
|
remove: labels.removeAttachment,
|
|
166
|
-
tooLarge: formatLabel(labels.attachmentTooLarge, {
|
|
176
|
+
tooLarge: formatLabel(labels.attachmentTooLarge, {
|
|
177
|
+
max: formatBytes(theme.maxAttachmentBytes ?? MAX_ATTACHMENT_BYTES),
|
|
178
|
+
}),
|
|
167
179
|
tooMany: formatLabel(labels.attachmentTooMany, { count: MAX_ATTACHMENTS }),
|
|
168
180
|
tooLargeTogether: labels.attachmentsTooLargeTogether,
|
|
169
181
|
failed: labels.attachmentFailed,
|
|
170
182
|
cameraDenied: labels.cameraPermissionDenied,
|
|
183
|
+
uploading: labels.attachmentUploading,
|
|
184
|
+
uploadFailed: labels.attachmentUploadFailed,
|
|
185
|
+
uploadFailedNotice: labels.attachmentsUploadFailed,
|
|
186
|
+
retry: labels.tapToRetry,
|
|
171
187
|
}),
|
|
172
|
-
[labels]
|
|
188
|
+
[labels, theme.maxAttachmentBytes]
|
|
173
189
|
);
|
|
174
190
|
|
|
175
191
|
// Stable across renders so each message bubble only re-renders when its
|
|
@@ -181,7 +197,12 @@ export function RepzoWidgetProvider({
|
|
|
181
197
|
loadEarlier: labels.loadEarlier,
|
|
182
198
|
emptyConversation: labels.emptyConversation,
|
|
183
199
|
messagePlaceholder: labels.messagePlaceholder,
|
|
200
|
+
send: labels.sendMessage,
|
|
201
|
+
jumpToLatest: labels.jumpToLatest,
|
|
202
|
+
jumpToLatestNew: labels.jumpToLatestNew,
|
|
203
|
+
morePhotos: labels.morePhotos,
|
|
184
204
|
failedToSend: labels.failedToSend,
|
|
205
|
+
tapToRetry: labels.tapToRetry,
|
|
185
206
|
openAttachment: labels.openAttachment,
|
|
186
207
|
thinking: labels.thinking,
|
|
187
208
|
team: labels.team,
|
|
@@ -224,6 +245,7 @@ export function RepzoWidgetProvider({
|
|
|
224
245
|
close: labels.close,
|
|
225
246
|
loadFailed: labels.imageLoadFailed,
|
|
226
247
|
openExternally: labels.openImageExternally,
|
|
248
|
+
position: labels.imagePosition,
|
|
227
249
|
},
|
|
228
250
|
voice: {
|
|
229
251
|
play: labels.voicePlay,
|
|
@@ -238,6 +260,19 @@ export function RepzoWidgetProvider({
|
|
|
238
260
|
const attachmentSources = theme.attachmentsEnabled ? getAttachmentSources() : [];
|
|
239
261
|
|
|
240
262
|
const handleClose = useCallback(() => store.hide(), [store]);
|
|
263
|
+
const handlePickAttachments = useCallback(
|
|
264
|
+
(source: AttachmentSource, current: DraftAttachment[]) =>
|
|
265
|
+
pickAttachments(source, current, theme.maxAttachmentBytes),
|
|
266
|
+
[theme.maxAttachmentBytes]
|
|
267
|
+
);
|
|
268
|
+
const handleUploadAttachment = useCallback(
|
|
269
|
+
(draft: DraftAttachment, signal?: AbortSignal) =>
|
|
270
|
+
store.uploadAttachment(
|
|
271
|
+
{ uri: draft.fileUri!, name: draft.name, contentType: draft.contentType, size: draft.size },
|
|
272
|
+
signal
|
|
273
|
+
),
|
|
274
|
+
[store]
|
|
275
|
+
);
|
|
241
276
|
|
|
242
277
|
const handleOpenNotification = useCallback(
|
|
243
278
|
(conversationId: string) => void store.openConversation(conversationId),
|
|
@@ -304,6 +339,7 @@ export function RepzoWidgetProvider({
|
|
|
304
339
|
store.sendMessage(body, attachments ?? [], action),
|
|
305
340
|
[store]
|
|
306
341
|
);
|
|
342
|
+
const handleRetryMessage = useCallback((messageId: string) => store.retryMessage(messageId), [store]);
|
|
307
343
|
|
|
308
344
|
const handleIdentifyVisitor = useCallback(
|
|
309
345
|
(data: { email?: string; name?: string; phone?: string }) => store.identifyVisitor(data),
|
|
@@ -442,8 +478,11 @@ export function RepzoWidgetProvider({
|
|
|
442
478
|
offlineMessage={theme.offlineMessage}
|
|
443
479
|
labels={chatLabels}
|
|
444
480
|
attachmentSources={attachmentSources}
|
|
445
|
-
onPickAttachments={
|
|
481
|
+
onPickAttachments={handlePickAttachments}
|
|
482
|
+
onUploadAttachment={handleUploadAttachment}
|
|
446
483
|
onSendMessage={handleSendMessage}
|
|
484
|
+
onRetryMessage={handleRetryMessage}
|
|
485
|
+
photoAlbums={store.showsPhotoAlbums()}
|
|
447
486
|
onIdentifyVisitor={handleIdentifyVisitor}
|
|
448
487
|
onRateConversation={handleRateConversation}
|
|
449
488
|
onLoadMore={handleLoadMore}
|
|
@@ -461,7 +500,7 @@ export function RepzoWidgetProvider({
|
|
|
461
500
|
color={theme.primaryColor}
|
|
462
501
|
visitorName={state.visitorProfile.name}
|
|
463
502
|
conversationCount={state.conversations.length}
|
|
464
|
-
|
|
503
|
+
recentConversation={recentConversation}
|
|
465
504
|
unreadByConversation={state.unreadByConversation}
|
|
466
505
|
teamAgents={teamAgents}
|
|
467
506
|
isOnline={theme.isOnline}
|
|
@@ -472,19 +511,17 @@ export function RepzoWidgetProvider({
|
|
|
472
511
|
showPreviousConversations={theme.showPreviousConversations}
|
|
473
512
|
labels={{
|
|
474
513
|
close: labels.close,
|
|
475
|
-
|
|
476
|
-
viewAll: formatLabel(labels.homeViewAllCount, { count: state.conversations.length }),
|
|
477
|
-
recentTitle: labels.homeRecentTitle,
|
|
514
|
+
recentTitle: labels.homeRecentMessageTitle,
|
|
478
515
|
noConversations: labels.homeNoConversations,
|
|
479
516
|
statusOnline: labels.homeStatusOnline,
|
|
480
517
|
statusAway: labels.homeStatusAway,
|
|
481
518
|
team: labels.team,
|
|
482
519
|
time: timeLabels,
|
|
483
520
|
unread: labels.unreadBadge,
|
|
521
|
+
lastMessage: lastMessageLabels,
|
|
484
522
|
}}
|
|
485
523
|
onClose={handleClose}
|
|
486
524
|
onStartConversation={handleStartConversation}
|
|
487
|
-
onViewConversations={() => handleTabChange('messages')}
|
|
488
525
|
onOpenConversation={(conversationId) => store.openConversation(conversationId)}
|
|
489
526
|
/>
|
|
490
527
|
);
|
|
@@ -510,6 +547,7 @@ export function RepzoWidgetProvider({
|
|
|
510
547
|
statusAway: labels.homeStatusAway,
|
|
511
548
|
time: timeLabels,
|
|
512
549
|
unread: labels.unreadBadge,
|
|
550
|
+
lastMessage: lastMessageLabels,
|
|
513
551
|
}}
|
|
514
552
|
onSelect={(conversationId) => store.openConversation(conversationId)}
|
|
515
553
|
onNewConversation={handleStartConversation}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Picked-but-unsent attachments above the composer: a thumbnail for images, a
|
|
3
3
|
* name + size card for everything else, each with its own remove button.
|
|
4
|
+
* A file that uploads ahead of the send shows a spinner until it's up, and
|
|
5
|
+
* turns into a retry button when the upload fails.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
import { Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
8
|
+
import { ActivityIndicator, Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
7
9
|
|
|
8
10
|
import {
|
|
9
11
|
formatBytes,
|
|
@@ -11,24 +13,29 @@ import {
|
|
|
11
13
|
type DraftAttachment,
|
|
12
14
|
} from '../../attachments/pickers';
|
|
13
15
|
import { formatLabel } from '../../i18n';
|
|
16
|
+
import { globalStyles } from '../styles/global';
|
|
14
17
|
import { fontSize, palette, radius, spacing, withAlpha } from '../styles/theme';
|
|
15
18
|
import { START_ALIGN } from '../utils/rtl';
|
|
16
|
-
import { CloseIcon } from './icons';
|
|
19
|
+
import { AlertIcon, CloseIcon } from './icons';
|
|
17
20
|
|
|
18
21
|
interface AttachmentChipsProps {
|
|
19
22
|
attachments: DraftAttachment[];
|
|
20
23
|
/** While a send is in flight the chips are dimmed and can't be removed. */
|
|
21
24
|
disabled?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
|
|
25
|
+
/** `remove`, `uploading` and `uploadFailed` keep their `{name}` placeholder; filled per chip. */
|
|
26
|
+
labels: { remove: string; uploading: string; uploadFailed: string; retry: string };
|
|
27
|
+
/** Removing a chip mid-upload cancels the upload. */
|
|
24
28
|
onRemove: (id: string) => void;
|
|
29
|
+
/** Starts a failed upload again. */
|
|
30
|
+
onRetry: (id: string) => void;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export function AttachmentChips({
|
|
28
34
|
attachments,
|
|
29
35
|
disabled,
|
|
30
|
-
|
|
36
|
+
labels,
|
|
31
37
|
onRemove,
|
|
38
|
+
onRetry,
|
|
32
39
|
}: AttachmentChipsProps) {
|
|
33
40
|
return (
|
|
34
41
|
<ScrollView
|
|
@@ -37,38 +44,82 @@ export function AttachmentChips({
|
|
|
37
44
|
keyboardShouldPersistTaps="handled"
|
|
38
45
|
style={styles.scroller}
|
|
39
46
|
contentContainerStyle={styles.row}>
|
|
40
|
-
{attachments.map((attachment) =>
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
{attachments.map((attachment) => {
|
|
48
|
+
const uploading = attachment.uploadStatus === 'uploading';
|
|
49
|
+
const failed = attachment.uploadStatus === 'failed';
|
|
50
|
+
const preview = isImageAttachment(attachment.contentType) ? (
|
|
51
|
+
<View>
|
|
43
52
|
<Image
|
|
44
53
|
source={{ uri: attachment.previewUri }}
|
|
45
54
|
style={styles.thumbnail}
|
|
46
55
|
resizeMode="cover"
|
|
47
|
-
accessible
|
|
56
|
+
accessible={!uploading && !failed}
|
|
48
57
|
accessibilityLabel={attachment.name}
|
|
49
58
|
accessibilityIgnoresInvertColors
|
|
50
59
|
/>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
{uploading || failed ? (
|
|
61
|
+
<View style={[styles.scrim, failed && styles.scrimFailed]}>
|
|
62
|
+
{uploading ? (
|
|
63
|
+
<ActivityIndicator color={palette.white} />
|
|
64
|
+
) : (
|
|
65
|
+
<AlertIcon size={20} color={palette.white} />
|
|
66
|
+
)}
|
|
67
|
+
</View>
|
|
68
|
+
) : null}
|
|
69
|
+
</View>
|
|
70
|
+
) : (
|
|
71
|
+
<View style={styles.file}>
|
|
72
|
+
<Text style={styles.fileName} numberOfLines={1}>
|
|
73
|
+
{attachment.name}
|
|
74
|
+
</Text>
|
|
75
|
+
<View style={styles.fileStatus}>
|
|
76
|
+
{uploading ? (
|
|
77
|
+
<ActivityIndicator size="small" color={palette.textMuted} />
|
|
78
|
+
) : failed ? (
|
|
79
|
+
<AlertIcon size={12} color={palette.danger} />
|
|
80
|
+
) : null}
|
|
81
|
+
<Text style={[styles.fileSize, failed && styles.fileFailed]} numberOfLines={1}>
|
|
82
|
+
{failed ? labels.retry : formatBytes(attachment.size)}
|
|
55
83
|
</Text>
|
|
56
|
-
<Text style={styles.fileSize}>{formatBytes(attachment.size)}</Text>
|
|
57
84
|
</View>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<View key={attachment.id} style={[styles.chip, disabled && styles.chipDisabled]}>
|
|
90
|
+
{failed ? (
|
|
91
|
+
<Pressable
|
|
92
|
+
onPress={() => onRetry(attachment.id)}
|
|
93
|
+
disabled={disabled}
|
|
94
|
+
accessibilityRole="button"
|
|
95
|
+
accessibilityLabel={formatLabel(labels.uploadFailed, { name: attachment.name })}
|
|
96
|
+
style={({ pressed }) => pressed && globalStyles.pressed}>
|
|
97
|
+
{preview}
|
|
98
|
+
</Pressable>
|
|
99
|
+
) : uploading ? (
|
|
100
|
+
<View
|
|
101
|
+
accessible
|
|
102
|
+
accessibilityLabel={formatLabel(labels.uploading, { name: attachment.name })}
|
|
103
|
+
accessibilityState={{ busy: true }}>
|
|
104
|
+
{preview}
|
|
105
|
+
</View>
|
|
106
|
+
) : (
|
|
107
|
+
preview
|
|
108
|
+
)}
|
|
109
|
+
<Pressable
|
|
110
|
+
onPress={() => onRemove(attachment.id)}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
accessibilityRole="button"
|
|
113
|
+
accessibilityLabel={formatLabel(labels.remove, { name: attachment.name })}
|
|
114
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
115
|
+
// 22pt badge + 11pt on every side = a 44pt target.
|
|
116
|
+
hitSlop={11}
|
|
117
|
+
style={({ pressed }) => [styles.remove, pressed && styles.removePressed]}>
|
|
118
|
+
<CloseIcon size={10} color={palette.white} />
|
|
119
|
+
</Pressable>
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
72
123
|
</ScrollView>
|
|
73
124
|
);
|
|
74
125
|
}
|
|
@@ -98,6 +149,16 @@ const styles = StyleSheet.create({
|
|
|
98
149
|
width: CHIP_HEIGHT,
|
|
99
150
|
height: CHIP_HEIGHT,
|
|
100
151
|
},
|
|
152
|
+
// Over the thumbnail while it uploads; darker once it failed, so the alert reads.
|
|
153
|
+
scrim: {
|
|
154
|
+
...StyleSheet.absoluteFillObject,
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
justifyContent: 'center',
|
|
157
|
+
backgroundColor: withAlpha(palette.ink, 0.4),
|
|
158
|
+
},
|
|
159
|
+
scrimFailed: {
|
|
160
|
+
backgroundColor: withAlpha(palette.ink, 0.6),
|
|
161
|
+
},
|
|
101
162
|
file: {
|
|
102
163
|
width: 156,
|
|
103
164
|
height: CHIP_HEIGHT,
|
|
@@ -112,11 +173,21 @@ const styles = StyleSheet.create({
|
|
|
112
173
|
fontWeight: '600',
|
|
113
174
|
color: palette.ink,
|
|
114
175
|
},
|
|
176
|
+
fileStatus: {
|
|
177
|
+
flexDirection: 'row',
|
|
178
|
+
alignItems: 'center',
|
|
179
|
+
gap: spacing.xs,
|
|
180
|
+
marginTop: 2,
|
|
181
|
+
},
|
|
115
182
|
fileSize: {
|
|
183
|
+
flexShrink: 1,
|
|
116
184
|
textAlign: START_ALIGN,
|
|
117
185
|
fontSize: fontSize.caption,
|
|
118
186
|
color: palette.textMuted,
|
|
119
|
-
|
|
187
|
+
},
|
|
188
|
+
fileFailed: {
|
|
189
|
+
color: palette.danger,
|
|
190
|
+
fontWeight: '600',
|
|
120
191
|
},
|
|
121
192
|
remove: {
|
|
122
193
|
position: 'absolute',
|
|
@@ -15,6 +15,15 @@ import { forwardDirection, START_ALIGN } from '../utils/rtl';
|
|
|
15
15
|
import { fontSize, palette, spacing } from '../styles/theme';
|
|
16
16
|
|
|
17
17
|
const AVATAR_SIZE = 40;
|
|
18
|
+
const CARD_AVATAR_SIZE = 32;
|
|
19
|
+
|
|
20
|
+
export interface LastMessageLabels {
|
|
21
|
+
/** Stand in for a newest message that had no words, only photos or other files. */
|
|
22
|
+
photo: string;
|
|
23
|
+
file: string;
|
|
24
|
+
/** Before the visitor's own newest message; keeps `{message}`. Empty: no prefix (`boot({ youPrefix: false })`). */
|
|
25
|
+
you: string;
|
|
26
|
+
}
|
|
18
27
|
|
|
19
28
|
interface ConversationRowProps {
|
|
20
29
|
conversation: Conversation;
|
|
@@ -26,8 +35,15 @@ interface ConversationRowProps {
|
|
|
26
35
|
unreadCount?: number;
|
|
27
36
|
/** Template with a `{count}` token, for screen readers. */
|
|
28
37
|
unreadLabel?: string;
|
|
38
|
+
lastMessageLabels?: LastMessageLabels;
|
|
29
39
|
/** Messages runs full-bleed; Home insets it inside a card. */
|
|
30
40
|
horizontalPadding?: number;
|
|
41
|
+
/**
|
|
42
|
+
* `card` is Home's "Recent message" card: a smaller avatar, a regular-weight
|
|
43
|
+
* name with the time right after it, and no chevron. The words, unread state
|
|
44
|
+
* and screen-reader label are the list's.
|
|
45
|
+
*/
|
|
46
|
+
variant?: 'list' | 'card';
|
|
31
47
|
onPress: () => void;
|
|
32
48
|
}
|
|
33
49
|
|
|
@@ -38,47 +54,70 @@ export function ConversationRow({
|
|
|
38
54
|
timeLabels,
|
|
39
55
|
unreadCount = 0,
|
|
40
56
|
unreadLabel,
|
|
57
|
+
lastMessageLabels,
|
|
41
58
|
horizontalPadding = spacing.xl,
|
|
59
|
+
variant = 'list',
|
|
42
60
|
onPress,
|
|
43
61
|
}: ConversationRowProps) {
|
|
62
|
+
const card = variant === 'card';
|
|
44
63
|
const displayName = conversation.agentName || fallbackName;
|
|
45
64
|
const hasUnread = unreadCount > 0;
|
|
46
65
|
const unreadSuffix =
|
|
47
66
|
hasUnread && unreadLabel ? `. ${formatLabel(unreadLabel, { count: unreadCount })}` : '';
|
|
67
|
+
const attachment = conversation.lastMessageAttachment;
|
|
68
|
+
// "Photo" or "File" over Workstation's "2 attachments".
|
|
69
|
+
const snippet = (attachment && lastMessageLabels?.[attachment]) || conversation.snippet || '';
|
|
70
|
+
// "You: " is drawn a shade stronger than the message, so it reads as a label.
|
|
71
|
+
const [youBefore, youAfter] =
|
|
72
|
+
snippet && conversation.lastMessageFromVisitor && lastMessageLabels?.you
|
|
73
|
+
? splitAround(lastMessageLabels.you, '{message}')
|
|
74
|
+
: ['', ''];
|
|
75
|
+
const snippetLabel = `${youBefore}${snippet}${youAfter}`;
|
|
48
76
|
|
|
49
77
|
return (
|
|
50
78
|
<Pressable
|
|
51
79
|
onPress={onPress}
|
|
52
80
|
accessibilityRole="button"
|
|
53
81
|
accessibilityLabel={`${
|
|
54
|
-
|
|
82
|
+
snippet ? `${displayName}. ${snippetLabel}` : displayName
|
|
55
83
|
}${unreadSuffix}`}
|
|
56
84
|
android_ripple={{ color: palette.borderSoft }}
|
|
57
85
|
style={({ pressed }) => [
|
|
58
86
|
styles.row,
|
|
87
|
+
card && cardStyles.row,
|
|
59
88
|
{ paddingHorizontal: horizontalPadding },
|
|
60
89
|
pressed && styles.rowPressed,
|
|
61
90
|
]}>
|
|
62
91
|
<Avatar
|
|
63
92
|
avatar={conversation.agentAvatar}
|
|
64
93
|
name={conversation.agentName}
|
|
65
|
-
size={AVATAR_SIZE}
|
|
94
|
+
size={card ? CARD_AVATAR_SIZE : AVATAR_SIZE}
|
|
66
95
|
color={color}
|
|
67
96
|
/>
|
|
68
97
|
<View style={styles.content}>
|
|
69
|
-
<View style={styles.topLine}>
|
|
70
|
-
<Text
|
|
98
|
+
<View style={[styles.topLine, card && cardStyles.topLine]}>
|
|
99
|
+
<Text
|
|
100
|
+
style={
|
|
101
|
+
card
|
|
102
|
+
? [cardStyles.name, hasUnread && cardStyles.nameUnread]
|
|
103
|
+
: [styles.name, hasUnread && styles.nameUnread]
|
|
104
|
+
}
|
|
105
|
+
numberOfLines={1}>
|
|
71
106
|
{displayName}
|
|
72
107
|
</Text>
|
|
73
|
-
<Text style={styles.time}>
|
|
108
|
+
<Text style={[styles.time, card && cardStyles.time]}>
|
|
74
109
|
{conversation.lastMessageAt
|
|
75
110
|
? formatRelativeTime(conversation.lastMessageAt, timeLabels)
|
|
76
111
|
: ''}
|
|
77
112
|
</Text>
|
|
78
113
|
</View>
|
|
79
|
-
{
|
|
80
|
-
<Text
|
|
81
|
-
{
|
|
114
|
+
{snippet ? (
|
|
115
|
+
<Text
|
|
116
|
+
style={[styles.snippet, card && cardStyles.snippet, hasUnread && styles.snippetUnread]}
|
|
117
|
+
numberOfLines={2}>
|
|
118
|
+
{youBefore ? <Text style={styles.snippetYou}>{youBefore}</Text> : null}
|
|
119
|
+
{snippet}
|
|
120
|
+
{youAfter}
|
|
82
121
|
</Text>
|
|
83
122
|
) : null}
|
|
84
123
|
</View>
|
|
@@ -86,13 +125,20 @@ export function ConversationRow({
|
|
|
86
125
|
<View style={[styles.badge, { backgroundColor: color }]}>
|
|
87
126
|
<Text style={styles.badgeLabel}>{unreadCount > 9 ? '9+' : unreadCount}</Text>
|
|
88
127
|
</View>
|
|
89
|
-
) : (
|
|
128
|
+
) : card ? null : (
|
|
90
129
|
<ChevronIcon size={16} direction={forwardDirection()} color={palette.textSubtle} />
|
|
91
130
|
)}
|
|
92
131
|
</Pressable>
|
|
93
132
|
);
|
|
94
133
|
}
|
|
95
134
|
|
|
135
|
+
/** `'You: {message}'` → `['You: ', '']`; a template without the token goes in front. */
|
|
136
|
+
function splitAround(template: string, token: string): [string, string] {
|
|
137
|
+
const index = template.indexOf(token);
|
|
138
|
+
if (index < 0) return [`${template} `, ''];
|
|
139
|
+
return [template.slice(0, index), template.slice(index + token.length)];
|
|
140
|
+
}
|
|
141
|
+
|
|
96
142
|
/** Where a divider should start so it sits under the text, not the avatar. */
|
|
97
143
|
export function rowDividerInset(horizontalPadding: number = spacing.xl): number {
|
|
98
144
|
return horizontalPadding + AVATAR_SIZE + spacing.md;
|
|
@@ -145,6 +191,10 @@ const styles = StyleSheet.create({
|
|
|
145
191
|
color: palette.text,
|
|
146
192
|
fontWeight: '500',
|
|
147
193
|
},
|
|
194
|
+
snippetYou: {
|
|
195
|
+
color: palette.text,
|
|
196
|
+
fontWeight: '600',
|
|
197
|
+
},
|
|
148
198
|
// Replaces the chevron rather than crowding beside it, so the row keeps
|
|
149
199
|
// the same width whether or not there is anything unread.
|
|
150
200
|
badge: {
|
|
@@ -161,3 +211,34 @@ const styles = StyleSheet.create({
|
|
|
161
211
|
fontWeight: '700',
|
|
162
212
|
},
|
|
163
213
|
});
|
|
214
|
+
|
|
215
|
+
// Overrides for the `card` variant. It sits under the card's title, so it
|
|
216
|
+
// drops the list's minimum height, and its name, time and last message share
|
|
217
|
+
// one size, the way the Home design draws them.
|
|
218
|
+
const cardStyles = StyleSheet.create({
|
|
219
|
+
row: {
|
|
220
|
+
minHeight: 0,
|
|
221
|
+
paddingVertical: spacing.sm,
|
|
222
|
+
},
|
|
223
|
+
// The time follows the name instead of being pushed to the far edge.
|
|
224
|
+
topLine: {
|
|
225
|
+
justifyContent: 'flex-start',
|
|
226
|
+
},
|
|
227
|
+
name: {
|
|
228
|
+
textAlign: START_ALIGN,
|
|
229
|
+
flexShrink: 1,
|
|
230
|
+
fontSize: fontSize.body,
|
|
231
|
+
color: palette.ink,
|
|
232
|
+
},
|
|
233
|
+
nameUnread: {
|
|
234
|
+
fontWeight: '600',
|
|
235
|
+
},
|
|
236
|
+
time: {
|
|
237
|
+
fontSize: fontSize.body,
|
|
238
|
+
},
|
|
239
|
+
snippet: {
|
|
240
|
+
fontSize: fontSize.body,
|
|
241
|
+
lineHeight: fontSize.body * 1.35,
|
|
242
|
+
color: palette.textSubtle,
|
|
243
|
+
},
|
|
244
|
+
});
|