@repzo/widget-react-native 1.0.1 → 1.0.4
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/lib/commonjs/api/client.js +3 -1
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/api/realtime.js +2 -0
- package/lib/commonjs/api/realtime.js.map +1 -1
- package/lib/commonjs/i18n.js +26 -0
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +5 -0
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +111 -7
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +17 -2
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/TeamAvatars.js +63 -0
- package/lib/commonjs/ui/components/TeamAvatars.js.map +1 -0
- package/lib/commonjs/ui/components/icons.js +50 -0
- package/lib/commonjs/ui/components/icons.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.js +105 -6
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +51 -0
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/api/client.js +3 -1
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/api/realtime.js +2 -0
- package/lib/module/api/realtime.js.map +1 -1
- package/lib/module/i18n.js +25 -0
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +5 -0
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/widget-store.js +111 -7
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +18 -3
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/TeamAvatars.js +59 -0
- package/lib/module/ui/components/TeamAvatars.js.map +1 -0
- package/lib/module/ui/components/icons.js +49 -0
- package/lib/module/ui/components/icons.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.js +106 -7
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +51 -0
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/api/realtime.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +7 -0
- 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 +17 -0
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +14 -1
- 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/TeamAvatars.d.ts +17 -0
- package/lib/typescript/commonjs/ui/components/TeamAvatars.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -0
- package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +51 -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 +13 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/api/realtime.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +7 -0
- 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 +17 -0
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +14 -1
- 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/TeamAvatars.d.ts +17 -0
- package/lib/typescript/module/ui/components/TeamAvatars.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/icons.d.ts +2 -0
- package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +51 -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 +13 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/client.ts +3 -1
- package/src/api/realtime.ts +2 -0
- package/src/i18n.ts +28 -0
- package/src/locales.ts +5 -0
- package/src/state/widget-store.ts +106 -5
- package/src/types.ts +16 -0
- package/src/ui/WidgetProvider.tsx +25 -3
- package/src/ui/components/TeamAvatars.tsx +58 -0
- package/src/ui/components/icons.tsx +37 -0
- package/src/ui/screens/chat/ChatView.styles.ts +51 -0
- package/src/ui/screens/chat/ChatView.tsx +111 -20
- package/src/ui/screens/chat/ChatView.types.ts +13 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Up to three overlapping teammate avatars — the Messenger header when no
|
|
3
|
+
* teammate has joined yet, and the "Waiting for a teammate" row (Intercom-style).
|
|
4
|
+
* Falls back to a single placeholder avatar when the workspace sent none.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { StyleSheet, View } from 'react-native';
|
|
8
|
+
|
|
9
|
+
import type { TeamAvatar } from '../../types';
|
|
10
|
+
import { palette } from '../styles/theme';
|
|
11
|
+
import { Avatar } from './Avatar';
|
|
12
|
+
|
|
13
|
+
interface TeamAvatarsProps {
|
|
14
|
+
avatars: TeamAvatar[];
|
|
15
|
+
size?: number;
|
|
16
|
+
color?: string;
|
|
17
|
+
/** Ring between overlapping avatars; matches the surface behind them. */
|
|
18
|
+
ringColor?: string;
|
|
19
|
+
translucent?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function TeamAvatars({
|
|
23
|
+
avatars,
|
|
24
|
+
size = 32,
|
|
25
|
+
color = palette.ink,
|
|
26
|
+
ringColor = palette.white,
|
|
27
|
+
translucent = false,
|
|
28
|
+
}: TeamAvatarsProps) {
|
|
29
|
+
if (avatars.length === 0) {
|
|
30
|
+
return <Avatar size={size} color={color} translucent={translucent} />;
|
|
31
|
+
}
|
|
32
|
+
const ring = Math.max(1.5, size * 0.06);
|
|
33
|
+
return (
|
|
34
|
+
<View style={styles.row} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
|
|
35
|
+
{avatars.slice(0, 3).map((avatar, index) => (
|
|
36
|
+
<View
|
|
37
|
+
key={`${avatar.initials}-${index}`}
|
|
38
|
+
style={[
|
|
39
|
+
styles.slot,
|
|
40
|
+
{
|
|
41
|
+
marginStart: index === 0 ? 0 : -size * 0.3,
|
|
42
|
+
borderRadius: (size + ring * 2) / 2,
|
|
43
|
+
borderWidth: ring,
|
|
44
|
+
borderColor: ringColor,
|
|
45
|
+
zIndex: 3 - index,
|
|
46
|
+
},
|
|
47
|
+
]}>
|
|
48
|
+
<Avatar avatar={avatar.avatarUrl} name={avatar.initials} size={size} color={color} translucent={translucent} />
|
|
49
|
+
</View>
|
|
50
|
+
))}
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const styles = StyleSheet.create({
|
|
56
|
+
row: { flexDirection: 'row', alignItems: 'center' },
|
|
57
|
+
slot: { overflow: 'hidden' },
|
|
58
|
+
});
|
|
@@ -313,3 +313,40 @@ const styles = StyleSheet.create({
|
|
|
313
313
|
justifyContent: 'center',
|
|
314
314
|
},
|
|
315
315
|
});
|
|
316
|
+
|
|
317
|
+
/** A ring with two hands — the reply-time hint beside the team name. */
|
|
318
|
+
export function ClockIcon({ size = 14, color = palette.textMuted, style }: IconProps) {
|
|
319
|
+
const thickness = Math.max(1.25, size * 0.1);
|
|
320
|
+
const hand: ViewStyle = {
|
|
321
|
+
position: 'absolute',
|
|
322
|
+
width: thickness,
|
|
323
|
+
borderRadius: thickness,
|
|
324
|
+
backgroundColor: color,
|
|
325
|
+
bottom: size / 2 - thickness / 2,
|
|
326
|
+
};
|
|
327
|
+
return (
|
|
328
|
+
<View style={[{ width: size, height: size }, styles.center, style]}>
|
|
329
|
+
<View
|
|
330
|
+
style={{
|
|
331
|
+
position: 'absolute',
|
|
332
|
+
width: size,
|
|
333
|
+
height: size,
|
|
334
|
+
borderRadius: size / 2,
|
|
335
|
+
borderWidth: thickness,
|
|
336
|
+
borderColor: color,
|
|
337
|
+
}}
|
|
338
|
+
/>
|
|
339
|
+
<View style={[hand, { height: size * 0.32, left: size / 2 - thickness / 2 }]} />
|
|
340
|
+
<View
|
|
341
|
+
style={[
|
|
342
|
+
hand,
|
|
343
|
+
{
|
|
344
|
+
height: size * 0.24,
|
|
345
|
+
left: size / 2 - thickness / 2,
|
|
346
|
+
transform: [{ translateX: size * 0.1 }, { translateY: size * 0.04 }, { rotate: '-90deg' }],
|
|
347
|
+
},
|
|
348
|
+
]}
|
|
349
|
+
/>
|
|
350
|
+
</View>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
@@ -50,6 +50,36 @@ export const styles = StyleSheet.create({
|
|
|
50
50
|
fontSize: fontSize.caption,
|
|
51
51
|
marginTop: 1,
|
|
52
52
|
},
|
|
53
|
+
headerReplyTime: {
|
|
54
|
+
flexDirection: 'row',
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
gap: 4,
|
|
57
|
+
marginTop: 1,
|
|
58
|
+
},
|
|
59
|
+
headerReplyTimeText: {
|
|
60
|
+
flexShrink: 1,
|
|
61
|
+
marginTop: 0,
|
|
62
|
+
},
|
|
63
|
+
waitingRow: {
|
|
64
|
+
flexDirection: 'row',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
gap: spacing.sm,
|
|
68
|
+
paddingVertical: spacing.sm,
|
|
69
|
+
},
|
|
70
|
+
waitingText: {
|
|
71
|
+
color: palette.textMuted,
|
|
72
|
+
fontSize: fontSize.caption,
|
|
73
|
+
},
|
|
74
|
+
joinedRow: {
|
|
75
|
+
flexDirection: 'row',
|
|
76
|
+
justifyContent: 'center',
|
|
77
|
+
gap: spacing.xs,
|
|
78
|
+
},
|
|
79
|
+
joinedName: {
|
|
80
|
+
fontWeight: '700',
|
|
81
|
+
color: palette.ink,
|
|
82
|
+
},
|
|
53
83
|
messagesContentEmpty: {
|
|
54
84
|
flexGrow: 1,
|
|
55
85
|
justifyContent: 'center',
|
|
@@ -108,6 +138,13 @@ export const styles = StyleSheet.create({
|
|
|
108
138
|
marginBottom: 3,
|
|
109
139
|
marginStart: spacing.xs,
|
|
110
140
|
},
|
|
141
|
+
receiptText: {
|
|
142
|
+
alignSelf: 'flex-end',
|
|
143
|
+
marginTop: 3,
|
|
144
|
+
marginEnd: spacing.xs,
|
|
145
|
+
fontSize: 11,
|
|
146
|
+
color: palette.textSubtle,
|
|
147
|
+
},
|
|
111
148
|
messageTime: {
|
|
112
149
|
fontSize: 11,
|
|
113
150
|
alignSelf: 'flex-end',
|
|
@@ -190,6 +227,20 @@ export const styles = StyleSheet.create({
|
|
|
190
227
|
fontSize: fontSize.caption,
|
|
191
228
|
marginTop: spacing.xs,
|
|
192
229
|
},
|
|
230
|
+
deletedBubble: {
|
|
231
|
+
maxWidth: '80%',
|
|
232
|
+
paddingHorizontal: spacing.md,
|
|
233
|
+
paddingVertical: spacing.sm,
|
|
234
|
+
borderRadius: radius.lg + 4,
|
|
235
|
+
borderWidth: 1,
|
|
236
|
+
borderStyle: 'dashed',
|
|
237
|
+
borderColor: palette.border,
|
|
238
|
+
},
|
|
239
|
+
deletedText: {
|
|
240
|
+
color: palette.textMuted,
|
|
241
|
+
fontSize: fontSize.caption,
|
|
242
|
+
fontStyle: 'italic',
|
|
243
|
+
},
|
|
193
244
|
systemRow: {
|
|
194
245
|
alignItems: 'center',
|
|
195
246
|
paddingVertical: spacing.sm,
|
|
@@ -47,7 +47,8 @@ import { Avatar } from '../../components/Avatar';
|
|
|
47
47
|
import { ImageViewer } from '../../components/ImageViewer';
|
|
48
48
|
import { WidgetErrorBoundary } from '../../components/ErrorBoundary';
|
|
49
49
|
import { GradientBackground } from '../../components/Gradient';
|
|
50
|
-
import { ArrowUpIcon, ChatOutlineIcon, ChevronIcon, PaperclipIcon } from '../../components/icons';
|
|
50
|
+
import { ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon, PaperclipIcon } from '../../components/icons';
|
|
51
|
+
import { TeamAvatars } from '../../components/TeamAvatars';
|
|
51
52
|
import { Markdown } from '../../components/Markdown';
|
|
52
53
|
import { useReduceMotion } from '../../hooks/use-reduce-motion';
|
|
53
54
|
import { globalStyles } from '../../styles/global';
|
|
@@ -67,7 +68,11 @@ export function ChatView({
|
|
|
67
68
|
color,
|
|
68
69
|
agentName,
|
|
69
70
|
agentAvatar,
|
|
71
|
+
agentOnline,
|
|
70
72
|
isOnline,
|
|
73
|
+
teamName,
|
|
74
|
+
teamAvatars = [],
|
|
75
|
+
replyTimeLabel,
|
|
71
76
|
locale,
|
|
72
77
|
prefillText,
|
|
73
78
|
offlineMessage,
|
|
@@ -309,6 +314,17 @@ export function ChatView({
|
|
|
309
314
|
const headerTextColor = contrastingTextColor(color);
|
|
310
315
|
const onDarkHeader = headerTextColor === palette.white;
|
|
311
316
|
const isEmptyThread = !loading && messages.length === 0;
|
|
317
|
+
const agentIsActive = agentOnline ?? isOnline;
|
|
318
|
+
// "Waiting for a teammate" until someone replies: no teammate in the header
|
|
319
|
+
// and the visitor spoke last (bot replies before that still count as waiting).
|
|
320
|
+
const lastChatMessage = [...messages]
|
|
321
|
+
.reverse()
|
|
322
|
+
.find((message) => (message.messageType ?? 'message') === 'message' && !message.deletedAt);
|
|
323
|
+
const awaitingTeammate = !agentName && !agentTyping && lastChatMessage?.direction === 'inbound';
|
|
324
|
+
// "Sent" / "Seen" goes under the visitor's latest message only.
|
|
325
|
+
const lastVisitorMessageId = [...messages]
|
|
326
|
+
.reverse()
|
|
327
|
+
.find((message) => message.direction === 'inbound' && (message.messageType ?? 'message') === 'message')?.id;
|
|
312
328
|
// Two stops off the brand color: flat fill looked like an unstyled block.
|
|
313
329
|
// A non-hex brand color falls through mixHex unchanged and renders solid.
|
|
314
330
|
const headerGradient = [color, mixHex(color, '#000000', 0.16)];
|
|
@@ -334,28 +350,57 @@ export function ChatView({
|
|
|
334
350
|
<ChevronIcon size={18} direction={backDirection()} color={headerTextColor} />
|
|
335
351
|
</Pressable>
|
|
336
352
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
353
|
+
{agentName ? (
|
|
354
|
+
// A teammate joined: their photo, name and own presence.
|
|
355
|
+
<View style={styles.headerIdentity}>
|
|
356
|
+
<View style={styles.avatarWrapper}>
|
|
357
|
+
<Avatar
|
|
358
|
+
avatar={agentAvatar}
|
|
359
|
+
name={agentName}
|
|
360
|
+
size={36}
|
|
361
|
+
translucent={onDarkHeader}
|
|
362
|
+
color={palette.ink}
|
|
363
|
+
/>
|
|
364
|
+
{agentIsActive ? <View style={styles.onlineDot} /> : null}
|
|
365
|
+
</View>
|
|
366
|
+
<View style={styles.headerText}>
|
|
367
|
+
<Text style={[styles.headerName, { color: headerTextColor }]} numberOfLines={1}>
|
|
368
|
+
{agentName}
|
|
369
|
+
</Text>
|
|
370
|
+
<Text
|
|
371
|
+
style={[styles.headerStatus, { color: withAlpha(headerTextColor, 0.75) }]}
|
|
372
|
+
numberOfLines={1}>
|
|
373
|
+
{agentIsActive ? labels.statusOnline : labels.statusAway}
|
|
374
|
+
</Text>
|
|
375
|
+
</View>
|
|
376
|
+
</View>
|
|
377
|
+
) : (
|
|
378
|
+
// No teammate yet, or the conversation closed: the team, Intercom-style.
|
|
379
|
+
<View style={styles.headerIdentity}>
|
|
380
|
+
<TeamAvatars
|
|
381
|
+
avatars={teamAvatars}
|
|
382
|
+
size={30}
|
|
344
383
|
color={palette.ink}
|
|
384
|
+
ringColor={color}
|
|
385
|
+
translucent={onDarkHeader}
|
|
345
386
|
/>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
387
|
+
<View style={styles.headerText}>
|
|
388
|
+
<Text style={[styles.headerName, { color: headerTextColor }]} numberOfLines={1}>
|
|
389
|
+
{teamName || labels.team}
|
|
390
|
+
</Text>
|
|
391
|
+
{replyTimeLabel ? (
|
|
392
|
+
<View style={styles.headerReplyTime}>
|
|
393
|
+
<ClockIcon size={12} color={withAlpha(headerTextColor, 0.75)} />
|
|
394
|
+
<Text
|
|
395
|
+
style={[styles.headerStatus, styles.headerReplyTimeText, { color: withAlpha(headerTextColor, 0.75) }]}
|
|
396
|
+
numberOfLines={1}>
|
|
397
|
+
{replyTimeLabel}
|
|
398
|
+
</Text>
|
|
399
|
+
</View>
|
|
400
|
+
) : null}
|
|
401
|
+
</View>
|
|
357
402
|
</View>
|
|
358
|
-
|
|
403
|
+
)}
|
|
359
404
|
|
|
360
405
|
<Pressable
|
|
361
406
|
onPress={onClose}
|
|
@@ -443,6 +488,7 @@ export function ChatView({
|
|
|
443
488
|
: null
|
|
444
489
|
}
|
|
445
490
|
timeLabel={formatClockTime(message.createdAt, locale)}
|
|
491
|
+
receiptLabel={message.id === lastVisitorMessageId ? receiptLabel(message, labels) : undefined}
|
|
446
492
|
onSubmitProcedureForm={
|
|
447
493
|
shouldShowProcedureForm(messages, message)
|
|
448
494
|
? (body) => void onSendMessage(body).catch(() => {})
|
|
@@ -457,6 +503,13 @@ export function ChatView({
|
|
|
457
503
|
);
|
|
458
504
|
})}
|
|
459
505
|
|
|
506
|
+
{awaitingTeammate ? (
|
|
507
|
+
<View style={styles.waitingRow}>
|
|
508
|
+
<TeamAvatars avatars={teamAvatars} size={20} color={color} />
|
|
509
|
+
<Text style={styles.waitingText}>{labels.waitingForTeammate}</Text>
|
|
510
|
+
</View>
|
|
511
|
+
) : null}
|
|
512
|
+
|
|
460
513
|
{agentTyping ? (
|
|
461
514
|
<View style={styles.typingRow}>
|
|
462
515
|
<View style={styles.typingBubble}>
|
|
@@ -576,6 +629,7 @@ function MessageBubble({
|
|
|
576
629
|
labels,
|
|
577
630
|
senderLabel,
|
|
578
631
|
timeLabel,
|
|
632
|
+
receiptLabel,
|
|
579
633
|
onSubmitProcedureForm,
|
|
580
634
|
onAgentUiAction,
|
|
581
635
|
onOpenImage,
|
|
@@ -585,10 +639,28 @@ function MessageBubble({
|
|
|
585
639
|
labels: ChatLabels;
|
|
586
640
|
senderLabel?: string | null;
|
|
587
641
|
timeLabel: string;
|
|
642
|
+
receiptLabel?: string;
|
|
588
643
|
onSubmitProcedureForm?: (body: string) => void;
|
|
589
644
|
onAgentUiAction?: (action: AgentUiAction) => void | Promise<void>;
|
|
590
645
|
onOpenImage: (image: { url: string; name: string }) => void;
|
|
591
646
|
}) {
|
|
647
|
+
const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
|
|
648
|
+
if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
|
|
649
|
+
return (
|
|
650
|
+
<View style={[styles.systemRow, styles.joinedRow]}>
|
|
651
|
+
<Avatar
|
|
652
|
+
avatar={typeof message.metadata?.agentAvatar === 'string' ? message.metadata.agentAvatar : null}
|
|
653
|
+
name={joinedName}
|
|
654
|
+
size={20}
|
|
655
|
+
color={palette.ink}
|
|
656
|
+
/>
|
|
657
|
+
<Text style={styles.systemText}>
|
|
658
|
+
<Text style={styles.joinedName}>{joinedName}</Text> {labels.joinedConversation}
|
|
659
|
+
</Text>
|
|
660
|
+
</View>
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
592
664
|
if (message.messageType === 'system') {
|
|
593
665
|
const text = message.bodyPlain || stripHtml(message.body || '') || labels.conversationUpdated;
|
|
594
666
|
return (
|
|
@@ -599,6 +671,17 @@ function MessageBubble({
|
|
|
599
671
|
}
|
|
600
672
|
|
|
601
673
|
const isVisitor = message.direction === 'inbound';
|
|
674
|
+
|
|
675
|
+
if (message.deletedAt) {
|
|
676
|
+
return (
|
|
677
|
+
<View style={[styles.messageRow, isVisitor ? styles.messageRowRight : styles.messageRowLeft]}>
|
|
678
|
+
<View style={styles.deletedBubble}>
|
|
679
|
+
<Text style={styles.deletedText}>{labels.messageDeleted}</Text>
|
|
680
|
+
</View>
|
|
681
|
+
</View>
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
602
685
|
const rtl = isRTL();
|
|
603
686
|
const messageText = message.bodyPlain || stripHtml(message.body || '');
|
|
604
687
|
const hasBody = !!messageText;
|
|
@@ -658,6 +741,8 @@ function MessageBubble({
|
|
|
658
741
|
) : null}
|
|
659
742
|
</View>
|
|
660
743
|
|
|
744
|
+
{receiptLabel ? <Text style={styles.receiptText}>{receiptLabel}</Text> : null}
|
|
745
|
+
|
|
661
746
|
{agentUiBlocks.length > 0 && onAgentUiAction ? (
|
|
662
747
|
<AgentUiBlocks
|
|
663
748
|
blocks={agentUiBlocks}
|
|
@@ -679,6 +764,12 @@ function MessageBubble({
|
|
|
679
764
|
);
|
|
680
765
|
}
|
|
681
766
|
|
|
767
|
+
/** Sent once the server has it, Seen once a teammate opened the conversation. */
|
|
768
|
+
function receiptLabel(message: Message, labels: ChatLabels): string | undefined {
|
|
769
|
+
if (message.deletedAt || message.status === 'pending' || message.status === 'failed') return undefined;
|
|
770
|
+
return message.status === 'read' ? labels.messageSeen : labels.messageSent;
|
|
771
|
+
}
|
|
772
|
+
|
|
682
773
|
function AttachmentPreview({
|
|
683
774
|
attachment,
|
|
684
775
|
isVisitor,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TeamAvatar } from '../../../types';
|
|
1
2
|
/** Props and label bundle for the chat thread. */
|
|
2
3
|
|
|
3
4
|
import type { DraftAttachment, PickResult } from '../../../attachments/pickers';
|
|
@@ -25,6 +26,11 @@ export interface ChatLabels {
|
|
|
25
26
|
today: string;
|
|
26
27
|
yesterday: string;
|
|
27
28
|
conversationUpdated: string;
|
|
29
|
+
messageDeleted: string;
|
|
30
|
+
messageSent: string;
|
|
31
|
+
messageSeen: string;
|
|
32
|
+
waitingForTeammate: string;
|
|
33
|
+
joinedConversation: string;
|
|
28
34
|
procedureSend: string;
|
|
29
35
|
procedureSent: string;
|
|
30
36
|
blocks: AgentUiBlockLabels;
|
|
@@ -56,7 +62,14 @@ export interface ChatViewProps {
|
|
|
56
62
|
/** Whoever is handling this thread, once the backend assigns someone. */
|
|
57
63
|
agentName?: string | null;
|
|
58
64
|
agentAvatar?: string | null;
|
|
65
|
+
/** The assigned teammate's own presence; falls back to `isOnline` when absent. */
|
|
66
|
+
agentOnline?: boolean | null;
|
|
59
67
|
isOnline: boolean;
|
|
68
|
+
/** Header title with no teammate yet (or after close); falls back to the Team label. */
|
|
69
|
+
teamName?: string | null;
|
|
70
|
+
teamAvatars?: TeamAvatar[];
|
|
71
|
+
/** e.g. "We usually reply within a few minutes", under the team name. */
|
|
72
|
+
replyTimeLabel?: string;
|
|
60
73
|
prefillText?: string;
|
|
61
74
|
offlineMessage?: string;
|
|
62
75
|
locale: string;
|