@repzo/widget-react-native 1.0.1 → 1.0.5

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.
Files changed (131) hide show
  1. package/lib/commonjs/api/client.js +3 -1
  2. package/lib/commonjs/api/client.js.map +1 -1
  3. package/lib/commonjs/api/realtime.js +2 -0
  4. package/lib/commonjs/api/realtime.js.map +1 -1
  5. package/lib/commonjs/i18n.js +26 -0
  6. package/lib/commonjs/i18n.js.map +1 -1
  7. package/lib/commonjs/locales.js +5 -0
  8. package/lib/commonjs/locales.js.map +1 -1
  9. package/lib/commonjs/state/widget-store.js +129 -10
  10. package/lib/commonjs/state/widget-store.js.map +1 -1
  11. package/lib/commonjs/types.js +2 -0
  12. package/lib/commonjs/types.js.map +1 -1
  13. package/lib/commonjs/ui/WidgetProvider.js +97 -66
  14. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  15. package/lib/commonjs/ui/components/Gradient.js +18 -2
  16. package/lib/commonjs/ui/components/Gradient.js.map +1 -1
  17. package/lib/commonjs/ui/components/TeamAvatars.js +63 -0
  18. package/lib/commonjs/ui/components/TeamAvatars.js.map +1 -0
  19. package/lib/commonjs/ui/components/icons.js +50 -0
  20. package/lib/commonjs/ui/components/icons.js.map +1 -1
  21. package/lib/commonjs/ui/hooks/use-top-inset.js +22 -0
  22. package/lib/commonjs/ui/hooks/use-top-inset.js.map +1 -0
  23. package/lib/commonjs/ui/screens/chat/ChatView.js +113 -7
  24. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  25. package/lib/commonjs/ui/screens/chat/ChatView.styles.js +51 -0
  26. package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
  27. package/lib/commonjs/ui/screens/home/HomeView.js +42 -9
  28. package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
  29. package/lib/commonjs/ui/screens/home/HomeView.styles.js +5 -3
  30. package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
  31. package/lib/module/api/client.js +3 -1
  32. package/lib/module/api/client.js.map +1 -1
  33. package/lib/module/api/realtime.js +2 -0
  34. package/lib/module/api/realtime.js.map +1 -1
  35. package/lib/module/i18n.js +25 -0
  36. package/lib/module/i18n.js.map +1 -1
  37. package/lib/module/locales.js +5 -0
  38. package/lib/module/locales.js.map +1 -1
  39. package/lib/module/state/widget-store.js +129 -10
  40. package/lib/module/state/widget-store.js.map +1 -1
  41. package/lib/module/types.js +2 -0
  42. package/lib/module/types.js.map +1 -1
  43. package/lib/module/ui/WidgetProvider.js +101 -70
  44. package/lib/module/ui/WidgetProvider.js.map +1 -1
  45. package/lib/module/ui/components/Gradient.js +18 -2
  46. package/lib/module/ui/components/Gradient.js.map +1 -1
  47. package/lib/module/ui/components/TeamAvatars.js +59 -0
  48. package/lib/module/ui/components/TeamAvatars.js.map +1 -0
  49. package/lib/module/ui/components/icons.js +49 -0
  50. package/lib/module/ui/components/icons.js.map +1 -1
  51. package/lib/module/ui/hooks/use-top-inset.js +17 -0
  52. package/lib/module/ui/hooks/use-top-inset.js.map +1 -0
  53. package/lib/module/ui/screens/chat/ChatView.js +115 -9
  54. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  55. package/lib/module/ui/screens/chat/ChatView.styles.js +51 -0
  56. package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
  57. package/lib/module/ui/screens/home/HomeView.js +44 -12
  58. package/lib/module/ui/screens/home/HomeView.js.map +1 -1
  59. package/lib/module/ui/screens/home/HomeView.styles.js +4 -2
  60. package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
  61. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/api/realtime.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/i18n.d.ts +7 -0
  64. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/state/widget-store.d.ts +24 -0
  67. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/types.d.ts +14 -1
  69. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/components/Gradient.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/components/TeamAvatars.d.ts +17 -0
  73. package/lib/typescript/commonjs/ui/components/TeamAvatars.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -0
  75. package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/hooks/use-top-inset.d.ts +11 -0
  77. package/lib/typescript/commonjs/ui/hooks/use-top-inset.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +1 -1
  79. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +51 -0
  81. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +13 -0
  83. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -0
  85. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +3 -1
  87. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
  88. package/lib/typescript/module/api/client.d.ts.map +1 -1
  89. package/lib/typescript/module/api/realtime.d.ts.map +1 -1
  90. package/lib/typescript/module/i18n.d.ts +7 -0
  91. package/lib/typescript/module/i18n.d.ts.map +1 -1
  92. package/lib/typescript/module/locales.d.ts.map +1 -1
  93. package/lib/typescript/module/state/widget-store.d.ts +24 -0
  94. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  95. package/lib/typescript/module/types.d.ts +14 -1
  96. package/lib/typescript/module/types.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/components/Gradient.d.ts.map +1 -1
  99. package/lib/typescript/module/ui/components/TeamAvatars.d.ts +17 -0
  100. package/lib/typescript/module/ui/components/TeamAvatars.d.ts.map +1 -0
  101. package/lib/typescript/module/ui/components/icons.d.ts +2 -0
  102. package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
  103. package/lib/typescript/module/ui/hooks/use-top-inset.d.ts +11 -0
  104. package/lib/typescript/module/ui/hooks/use-top-inset.d.ts.map +1 -0
  105. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +1 -1
  106. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  107. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +51 -0
  108. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  109. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +13 -0
  110. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  111. package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -0
  112. package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
  113. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +3 -1
  114. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
  115. package/package.json +1 -1
  116. package/src/api/client.ts +3 -1
  117. package/src/api/realtime.ts +2 -0
  118. package/src/i18n.ts +28 -0
  119. package/src/locales.ts +5 -0
  120. package/src/state/widget-store.ts +124 -8
  121. package/src/types.ts +16 -0
  122. package/src/ui/WidgetProvider.tsx +66 -7
  123. package/src/ui/components/Gradient.tsx +19 -2
  124. package/src/ui/components/TeamAvatars.tsx +58 -0
  125. package/src/ui/components/icons.tsx +37 -0
  126. package/src/ui/hooks/use-top-inset.ts +16 -0
  127. package/src/ui/screens/chat/ChatView.styles.ts +51 -0
  128. package/src/ui/screens/chat/ChatView.tsx +120 -22
  129. package/src/ui/screens/chat/ChatView.types.ts +13 -0
  130. package/src/ui/screens/home/HomeView.styles.ts +4 -2
  131. package/src/ui/screens/home/HomeView.tsx +36 -8
@@ -47,11 +47,13 @@ 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';
54
+ import { useTopInset } from '../../hooks/use-top-inset';
53
55
  import { globalStyles } from '../../styles/global';
54
- import { contrastingTextColor, mixHex, palette, withAlpha } from '../../styles/theme';
56
+ import { contrastingTextColor, mixHex, palette, spacing, withAlpha } from '../../styles/theme';
55
57
  import { formatClockTime, formatDayLabel, isSameDay } from '../../utils/conversations';
56
58
  import { stripHtml } from '../../utils/html';
57
59
  import { isHttpUrl, openExternalUrl } from '../../utils/safe-url';
@@ -67,7 +69,11 @@ export function ChatView({
67
69
  color,
68
70
  agentName,
69
71
  agentAvatar,
72
+ agentOnline,
70
73
  isOnline,
74
+ teamName,
75
+ teamAvatars = [],
76
+ replyTimeLabel,
71
77
  locale,
72
78
  prefillText,
73
79
  offlineMessage,
@@ -309,8 +315,20 @@ export function ChatView({
309
315
  const headerTextColor = contrastingTextColor(color);
310
316
  const onDarkHeader = headerTextColor === palette.white;
311
317
  const isEmptyThread = !loading && messages.length === 0;
318
+ const agentIsActive = agentOnline ?? isOnline;
319
+ // "Waiting for a teammate" until someone replies: no teammate in the header
320
+ // and the visitor spoke last (bot replies before that still count as waiting).
321
+ const lastChatMessage = [...messages]
322
+ .reverse()
323
+ .find((message) => (message.messageType ?? 'message') === 'message' && !message.deletedAt);
324
+ const awaitingTeammate = !agentName && !agentTyping && lastChatMessage?.direction === 'inbound';
325
+ // "Sent" / "Seen" goes under the visitor's latest message only.
326
+ const lastVisitorMessageId = [...messages]
327
+ .reverse()
328
+ .find((message) => message.direction === 'inbound' && (message.messageType ?? 'message') === 'message')?.id;
312
329
  // Two stops off the brand color: flat fill looked like an unstyled block.
313
330
  // A non-hex brand color falls through mixHex unchanged and renders solid.
331
+ const topInset = useTopInset();
314
332
  const headerGradient = [color, mixHex(color, '#000000', 0.16)];
315
333
  const headerControl = { backgroundColor: withAlpha(headerTextColor, 0.16) };
316
334
 
@@ -320,7 +338,12 @@ export function ChatView({
320
338
  was empty whenever no agent was assigned yet. */}
321
339
  <GradientBackground
322
340
  colors={headerGradient}
323
- style={[styles.header, { borderBottomColor: mixHex(color, '#000000', 0.26) }]}>
341
+ style={[
342
+ styles.header,
343
+ { borderBottomColor: mixHex(color, '#000000', 0.26) },
344
+ // Reaches under the status bar so the brand colour runs to the top.
345
+ topInset > 0 ? { paddingTop: spacing.md + topInset, minHeight: 72 + topInset } : null,
346
+ ]}>
324
347
  <Pressable
325
348
  onPress={onBack}
326
349
  accessibilityRole="button"
@@ -334,28 +357,57 @@ export function ChatView({
334
357
  <ChevronIcon size={18} direction={backDirection()} color={headerTextColor} />
335
358
  </Pressable>
336
359
 
337
- <View style={styles.headerIdentity}>
338
- <View style={styles.avatarWrapper}>
339
- <Avatar
340
- avatar={agentAvatar}
341
- name={agentName}
342
- size={36}
343
- translucent={onDarkHeader}
360
+ {agentName ? (
361
+ // A teammate joined: their photo, name and own presence.
362
+ <View style={styles.headerIdentity}>
363
+ <View style={styles.avatarWrapper}>
364
+ <Avatar
365
+ avatar={agentAvatar}
366
+ name={agentName}
367
+ size={36}
368
+ translucent={onDarkHeader}
369
+ color={palette.ink}
370
+ />
371
+ {agentIsActive ? <View style={styles.onlineDot} /> : null}
372
+ </View>
373
+ <View style={styles.headerText}>
374
+ <Text style={[styles.headerName, { color: headerTextColor }]} numberOfLines={1}>
375
+ {agentName}
376
+ </Text>
377
+ <Text
378
+ style={[styles.headerStatus, { color: withAlpha(headerTextColor, 0.75) }]}
379
+ numberOfLines={1}>
380
+ {agentIsActive ? labels.statusOnline : labels.statusAway}
381
+ </Text>
382
+ </View>
383
+ </View>
384
+ ) : (
385
+ // No teammate yet, or the conversation closed: the team, Intercom-style.
386
+ <View style={styles.headerIdentity}>
387
+ <TeamAvatars
388
+ avatars={teamAvatars}
389
+ size={30}
344
390
  color={palette.ink}
391
+ ringColor={color}
392
+ translucent={onDarkHeader}
345
393
  />
346
- {isOnline ? <View style={styles.onlineDot} /> : null}
347
- </View>
348
- <View style={styles.headerText}>
349
- <Text style={[styles.headerName, { color: headerTextColor }]} numberOfLines={1}>
350
- {agentName || labels.team}
351
- </Text>
352
- <Text
353
- style={[styles.headerStatus, { color: withAlpha(headerTextColor, 0.75) }]}
354
- numberOfLines={1}>
355
- {isOnline ? labels.statusOnline : labels.statusAway}
356
- </Text>
394
+ <View style={styles.headerText}>
395
+ <Text style={[styles.headerName, { color: headerTextColor }]} numberOfLines={1}>
396
+ {teamName || labels.team}
397
+ </Text>
398
+ {replyTimeLabel ? (
399
+ <View style={styles.headerReplyTime}>
400
+ <ClockIcon size={12} color={withAlpha(headerTextColor, 0.75)} />
401
+ <Text
402
+ style={[styles.headerStatus, styles.headerReplyTimeText, { color: withAlpha(headerTextColor, 0.75) }]}
403
+ numberOfLines={1}>
404
+ {replyTimeLabel}
405
+ </Text>
406
+ </View>
407
+ ) : null}
408
+ </View>
357
409
  </View>
358
- </View>
410
+ )}
359
411
 
360
412
  <Pressable
361
413
  onPress={onClose}
@@ -443,6 +495,7 @@ export function ChatView({
443
495
  : null
444
496
  }
445
497
  timeLabel={formatClockTime(message.createdAt, locale)}
498
+ receiptLabel={message.id === lastVisitorMessageId ? receiptLabel(message, labels) : undefined}
446
499
  onSubmitProcedureForm={
447
500
  shouldShowProcedureForm(messages, message)
448
501
  ? (body) => void onSendMessage(body).catch(() => {})
@@ -457,6 +510,13 @@ export function ChatView({
457
510
  );
458
511
  })}
459
512
 
513
+ {awaitingTeammate ? (
514
+ <View style={styles.waitingRow}>
515
+ <TeamAvatars avatars={teamAvatars} size={20} color={color} />
516
+ <Text style={styles.waitingText}>{labels.waitingForTeammate}</Text>
517
+ </View>
518
+ ) : null}
519
+
460
520
  {agentTyping ? (
461
521
  <View style={styles.typingRow}>
462
522
  <View style={styles.typingBubble}>
@@ -576,6 +636,7 @@ function MessageBubble({
576
636
  labels,
577
637
  senderLabel,
578
638
  timeLabel,
639
+ receiptLabel,
579
640
  onSubmitProcedureForm,
580
641
  onAgentUiAction,
581
642
  onOpenImage,
@@ -585,10 +646,28 @@ function MessageBubble({
585
646
  labels: ChatLabels;
586
647
  senderLabel?: string | null;
587
648
  timeLabel: string;
649
+ receiptLabel?: string;
588
650
  onSubmitProcedureForm?: (body: string) => void;
589
651
  onAgentUiAction?: (action: AgentUiAction) => void | Promise<void>;
590
652
  onOpenImage: (image: { url: string; name: string }) => void;
591
653
  }) {
654
+ const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
655
+ if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
656
+ return (
657
+ <View style={[styles.systemRow, styles.joinedRow]}>
658
+ <Avatar
659
+ avatar={typeof message.metadata?.agentAvatar === 'string' ? message.metadata.agentAvatar : null}
660
+ name={joinedName}
661
+ size={20}
662
+ color={palette.ink}
663
+ />
664
+ <Text style={styles.systemText}>
665
+ <Text style={styles.joinedName}>{joinedName}</Text> {labels.joinedConversation}
666
+ </Text>
667
+ </View>
668
+ );
669
+ }
670
+
592
671
  if (message.messageType === 'system') {
593
672
  const text = message.bodyPlain || stripHtml(message.body || '') || labels.conversationUpdated;
594
673
  return (
@@ -599,6 +678,17 @@ function MessageBubble({
599
678
  }
600
679
 
601
680
  const isVisitor = message.direction === 'inbound';
681
+
682
+ if (message.deletedAt) {
683
+ return (
684
+ <View style={[styles.messageRow, isVisitor ? styles.messageRowRight : styles.messageRowLeft]}>
685
+ <View style={styles.deletedBubble}>
686
+ <Text style={styles.deletedText}>{labels.messageDeleted}</Text>
687
+ </View>
688
+ </View>
689
+ );
690
+ }
691
+
602
692
  const rtl = isRTL();
603
693
  const messageText = message.bodyPlain || stripHtml(message.body || '');
604
694
  const hasBody = !!messageText;
@@ -658,6 +748,8 @@ function MessageBubble({
658
748
  ) : null}
659
749
  </View>
660
750
 
751
+ {receiptLabel ? <Text style={styles.receiptText}>{receiptLabel}</Text> : null}
752
+
661
753
  {agentUiBlocks.length > 0 && onAgentUiAction ? (
662
754
  <AgentUiBlocks
663
755
  blocks={agentUiBlocks}
@@ -679,6 +771,12 @@ function MessageBubble({
679
771
  );
680
772
  }
681
773
 
774
+ /** Sent once the server has it, Seen once a teammate opened the conversation. */
775
+ function receiptLabel(message: Message, labels: ChatLabels): string | undefined {
776
+ if (message.deletedAt || message.status === 'pending' || message.status === 'failed') return undefined;
777
+ return message.status === 'read' ? labels.messageSeen : labels.messageSent;
778
+ }
779
+
682
780
  function AttachmentPreview({
683
781
  attachment,
684
782
  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;
@@ -23,6 +23,9 @@ export const START_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;
26
+ /** The logo keeps its own shape at this height, up to the web widget's max width. */
27
+ export const LOGO_HEIGHT = 40;
28
+ export const LOGO_MAX_WIDTH = 152;
26
29
  export const CLOSE_ICON_SIZE = 14;
27
30
 
28
31
  export const styles = StyleSheet.create({
@@ -58,8 +61,7 @@ export const styles = StyleSheet.create({
58
61
  minHeight: 40,
59
62
  },
60
63
  logo: {
61
- width: 132,
62
- height: 40,
64
+ height: LOGO_HEIGHT,
63
65
  },
64
66
  closeButton: {
65
67
  marginStart: 'auto',
@@ -13,7 +13,7 @@
13
13
  * send today.
14
14
  */
15
15
 
16
- import { Fragment, useEffect, useRef, type ReactNode } from 'react';
16
+ import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react';
17
17
  import {
18
18
  Animated,
19
19
  Easing,
@@ -37,6 +37,7 @@ import {
37
37
  CloseIcon,
38
38
  } from '../../components/icons';
39
39
  import { useReduceMotion } from '../../hooks/use-reduce-motion';
40
+ import { useTopInset } from '../../hooks/use-top-inset';
40
41
  import { globalStyles } from '../../styles/global';
41
42
  import { contrastingTextColor, palette, spacing, withAlpha } from '../../styles/theme';
42
43
  import { flipForRTL } from '../../utils/rtl';
@@ -45,6 +46,9 @@ import {
45
46
  CLOSE_ICON_SIZE,
46
47
  HEADER_BOTTOM_PADDING,
47
48
  HEADER_FADE_HEIGHT,
49
+ HEADER_MIN_HEIGHT,
50
+ LOGO_HEIGHT,
51
+ LOGO_MAX_WIDTH,
48
52
  START_CARD_LIFT,
49
53
  styles,
50
54
  } from './HomeView.styles';
@@ -55,7 +59,7 @@ import type { HomeViewProps } from './HomeView.types';
55
59
  * it, and defaulting to dark text over a dark brand header is unreadable, so
56
60
  * derive something legible from the background instead.
57
61
  */
58
- function resolveHeaderTextColor(config: WidgetConfig | null, fallbackColor: string): string {
62
+ export function resolveHeaderTextColor(config: WidgetConfig | null, fallbackColor: string): string {
59
63
  if (config?.homeHeaderTextColor === 'white') return palette.white;
60
64
  if (config?.homeHeaderTextColor === 'black') return palette.ink;
61
65
  if (config?.homeBackgroundType === 'image') return palette.white;
@@ -116,8 +120,14 @@ export function HomeView({
116
120
 
117
121
  // The start card is pulled up over the seam by a fixed amount, so the header
118
122
  // pads its bottom by that same amount and the greeting never slides under it.
123
+ // The header reaches up under the status bar (no white strip above it), so
124
+ // it grows by that inset and keeps its content where it was.
125
+ const topInset = useTopInset();
119
126
  const headerStyle = [
120
127
  styles.header,
128
+ topInset > 0
129
+ ? { paddingTop: spacing.xl + topInset, minHeight: HEADER_MIN_HEIGHT + topInset }
130
+ : null,
121
131
  allowNewConversations ? { paddingBottom: HEADER_BOTTOM_PADDING + START_CARD_LIFT } : null,
122
132
  ];
123
133
 
@@ -125,12 +135,7 @@ export function HomeView({
125
135
  <>
126
136
  <View style={styles.headerTop}>
127
137
  {config?.homeLogoUrl ? (
128
- <Image
129
- source={{ uri: config.homeLogoUrl }}
130
- style={styles.logo}
131
- resizeMode="contain"
132
- accessibilityIgnoresInvertColors
133
- />
138
+ <HomeLogo key={config.homeLogoUrl} uri={config.homeLogoUrl} />
134
139
  ) : (
135
140
  <View />
136
141
  )}
@@ -296,6 +301,29 @@ export function HomeView({
296
301
  );
297
302
  }
298
303
 
304
+ /**
305
+ * The workspace logo at its own shape, flush with the greeting's left edge.
306
+ * A fixed-width box with `contain` centred a square logo inside it, so it
307
+ * floated in from the edge; the web widget sizes it to the image instead.
308
+ * Square until the image reports its size, then as wide as its ratio needs,
309
+ * capped like the web widget.
310
+ */
311
+ function HomeLogo({ uri }: { uri: string }) {
312
+ const [aspectRatio, setAspectRatio] = useState(1);
313
+ return (
314
+ <Image
315
+ source={{ uri }}
316
+ style={[styles.logo, { width: Math.min(LOGO_MAX_WIDTH, LOGO_HEIGHT * aspectRatio) }]}
317
+ resizeMode="contain"
318
+ accessibilityIgnoresInvertColors
319
+ onLoad={(event) => {
320
+ const { width, height } = event.nativeEvent.source ?? {};
321
+ if (width && height) setAspectRatio(width / height);
322
+ }}
323
+ />
324
+ );
325
+ }
326
+
299
327
  /**
300
328
  * Staggered entrance for the body sections. Opacity and translate only, so the
301
329
  * whole thing runs on the native driver and never touches layout.