@appsupportkit/support-sdk 0.2.0 → 0.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.
Files changed (242) hide show
  1. package/lib/commonjs/api.js +61 -12
  2. package/lib/commonjs/api.js.map +1 -1
  3. package/lib/commonjs/components/BottomSheet.js +1 -1
  4. package/lib/commonjs/components/BottomSheet.js.map +1 -1
  5. package/lib/commonjs/components/LauncherButton.js +4 -4
  6. package/lib/commonjs/components/LauncherButton.js.map +1 -1
  7. package/lib/commonjs/components/SupportButton.js +192 -66
  8. package/lib/commonjs/components/SupportButton.js.map +1 -1
  9. package/lib/commonjs/components/icons.js +117 -110
  10. package/lib/commonjs/components/icons.js.map +1 -1
  11. package/lib/commonjs/components/primitives.js +3 -154
  12. package/lib/commonjs/components/primitives.js.map +1 -1
  13. package/lib/commonjs/components/screens/ChatScreen.js +487 -0
  14. package/lib/commonjs/components/screens/ChatScreen.js.map +1 -0
  15. package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
  16. package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
  17. package/lib/commonjs/i18n/index.js +101 -0
  18. package/lib/commonjs/i18n/index.js.map +1 -0
  19. package/lib/commonjs/i18n/languages.js +95 -0
  20. package/lib/commonjs/i18n/languages.js.map +1 -0
  21. package/lib/commonjs/i18n/strings/de.js +75 -0
  22. package/lib/commonjs/i18n/strings/de.js.map +1 -0
  23. package/lib/commonjs/i18n/strings/en.js +91 -0
  24. package/lib/commonjs/i18n/strings/en.js.map +1 -0
  25. package/lib/commonjs/i18n/strings/es.js +75 -0
  26. package/lib/commonjs/i18n/strings/es.js.map +1 -0
  27. package/lib/commonjs/i18n/strings/fr.js +75 -0
  28. package/lib/commonjs/i18n/strings/fr.js.map +1 -0
  29. package/lib/commonjs/i18n/strings/hi.js +75 -0
  30. package/lib/commonjs/i18n/strings/hi.js.map +1 -0
  31. package/lib/commonjs/i18n/strings/it.js +75 -0
  32. package/lib/commonjs/i18n/strings/it.js.map +1 -0
  33. package/lib/commonjs/i18n/strings/ja.js +75 -0
  34. package/lib/commonjs/i18n/strings/ja.js.map +1 -0
  35. package/lib/commonjs/i18n/strings/ko.js +75 -0
  36. package/lib/commonjs/i18n/strings/ko.js.map +1 -0
  37. package/lib/commonjs/i18n/strings/nl.js +75 -0
  38. package/lib/commonjs/i18n/strings/nl.js.map +1 -0
  39. package/lib/commonjs/i18n/strings/pl.js +75 -0
  40. package/lib/commonjs/i18n/strings/pl.js.map +1 -0
  41. package/lib/commonjs/i18n/strings/pt.js +75 -0
  42. package/lib/commonjs/i18n/strings/pt.js.map +1 -0
  43. package/lib/commonjs/i18n/strings/ru.js +75 -0
  44. package/lib/commonjs/i18n/strings/ru.js.map +1 -0
  45. package/lib/commonjs/i18n/strings/tr.js +75 -0
  46. package/lib/commonjs/i18n/strings/tr.js.map +1 -0
  47. package/lib/commonjs/i18n/strings/zh.js +75 -0
  48. package/lib/commonjs/i18n/strings/zh.js.map +1 -0
  49. package/lib/commonjs/index.js +38 -0
  50. package/lib/commonjs/index.js.map +1 -1
  51. package/lib/commonjs/push.js +78 -3
  52. package/lib/commonjs/push.js.map +1 -1
  53. package/lib/commonjs/review.js +54 -0
  54. package/lib/commonjs/review.js.map +1 -0
  55. package/lib/commonjs/theme.js +142 -23
  56. package/lib/commonjs/theme.js.map +1 -1
  57. package/lib/commonjs/types.js +32 -3
  58. package/lib/commonjs/types.js.map +1 -1
  59. package/lib/commonjs/useSupportThreads.js +41 -24
  60. package/lib/commonjs/useSupportThreads.js.map +1 -1
  61. package/lib/commonjs/utils.js +25 -30
  62. package/lib/commonjs/utils.js.map +1 -1
  63. package/lib/commonjs/version.js +13 -0
  64. package/lib/commonjs/version.js.map +1 -0
  65. package/lib/module/api.js +57 -10
  66. package/lib/module/api.js.map +1 -1
  67. package/lib/module/components/BottomSheet.js +1 -1
  68. package/lib/module/components/BottomSheet.js.map +1 -1
  69. package/lib/module/components/LauncherButton.js +5 -5
  70. package/lib/module/components/LauncherButton.js.map +1 -1
  71. package/lib/module/components/SupportButton.js +199 -73
  72. package/lib/module/components/SupportButton.js.map +1 -1
  73. package/lib/module/components/icons.js +115 -107
  74. package/lib/module/components/icons.js.map +1 -1
  75. package/lib/module/components/primitives.js +5 -147
  76. package/lib/module/components/primitives.js.map +1 -1
  77. package/lib/module/components/screens/ChatScreen.js +485 -0
  78. package/lib/module/components/screens/ChatScreen.js.map +1 -0
  79. package/lib/module/components/screens/LanguageScreen.js +147 -0
  80. package/lib/module/components/screens/LanguageScreen.js.map +1 -0
  81. package/lib/module/i18n/index.js +56 -0
  82. package/lib/module/i18n/index.js.map +1 -0
  83. package/lib/module/i18n/languages.js +88 -0
  84. package/lib/module/i18n/languages.js.map +1 -0
  85. package/lib/module/i18n/strings/de.js +71 -0
  86. package/lib/module/i18n/strings/de.js.map +1 -0
  87. package/lib/module/i18n/strings/en.js +87 -0
  88. package/lib/module/i18n/strings/en.js.map +1 -0
  89. package/lib/module/i18n/strings/es.js +71 -0
  90. package/lib/module/i18n/strings/es.js.map +1 -0
  91. package/lib/module/i18n/strings/fr.js +71 -0
  92. package/lib/module/i18n/strings/fr.js.map +1 -0
  93. package/lib/module/i18n/strings/hi.js +71 -0
  94. package/lib/module/i18n/strings/hi.js.map +1 -0
  95. package/lib/module/i18n/strings/it.js +71 -0
  96. package/lib/module/i18n/strings/it.js.map +1 -0
  97. package/lib/module/i18n/strings/ja.js +71 -0
  98. package/lib/module/i18n/strings/ja.js.map +1 -0
  99. package/lib/module/i18n/strings/ko.js +71 -0
  100. package/lib/module/i18n/strings/ko.js.map +1 -0
  101. package/lib/module/i18n/strings/nl.js +71 -0
  102. package/lib/module/i18n/strings/nl.js.map +1 -0
  103. package/lib/module/i18n/strings/pl.js +71 -0
  104. package/lib/module/i18n/strings/pl.js.map +1 -0
  105. package/lib/module/i18n/strings/pt.js +71 -0
  106. package/lib/module/i18n/strings/pt.js.map +1 -0
  107. package/lib/module/i18n/strings/ru.js +71 -0
  108. package/lib/module/i18n/strings/ru.js.map +1 -0
  109. package/lib/module/i18n/strings/tr.js +71 -0
  110. package/lib/module/i18n/strings/tr.js.map +1 -0
  111. package/lib/module/i18n/strings/zh.js +71 -0
  112. package/lib/module/i18n/strings/zh.js.map +1 -0
  113. package/lib/module/index.js +2 -0
  114. package/lib/module/index.js.map +1 -1
  115. package/lib/module/push.js +77 -3
  116. package/lib/module/push.js.map +1 -1
  117. package/lib/module/review.js +48 -0
  118. package/lib/module/review.js.map +1 -0
  119. package/lib/module/theme.js +141 -23
  120. package/lib/module/theme.js.map +1 -1
  121. package/lib/module/types.js +32 -3
  122. package/lib/module/types.js.map +1 -1
  123. package/lib/module/useSupportThreads.js +41 -24
  124. package/lib/module/useSupportThreads.js.map +1 -1
  125. package/lib/module/utils.js +24 -29
  126. package/lib/module/utils.js.map +1 -1
  127. package/lib/module/version.js +9 -0
  128. package/lib/module/version.js.map +1 -0
  129. package/lib/typescript/api.d.ts +40 -10
  130. package/lib/typescript/api.d.ts.map +1 -1
  131. package/lib/typescript/components/SupportButton.d.ts +2 -2
  132. package/lib/typescript/components/SupportButton.d.ts.map +1 -1
  133. package/lib/typescript/components/icons.d.ts +10 -14
  134. package/lib/typescript/components/icons.d.ts.map +1 -1
  135. package/lib/typescript/components/primitives.d.ts +3 -30
  136. package/lib/typescript/components/primitives.d.ts.map +1 -1
  137. package/lib/typescript/components/screens/ChatScreen.d.ts +21 -0
  138. package/lib/typescript/components/screens/ChatScreen.d.ts.map +1 -0
  139. package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
  140. package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
  141. package/lib/typescript/i18n/index.d.ts +15 -0
  142. package/lib/typescript/i18n/index.d.ts.map +1 -0
  143. package/lib/typescript/i18n/languages.d.ts +36 -0
  144. package/lib/typescript/i18n/languages.d.ts.map +1 -0
  145. package/lib/typescript/i18n/strings/de.d.ts +3 -0
  146. package/lib/typescript/i18n/strings/de.d.ts.map +1 -0
  147. package/lib/typescript/i18n/strings/en.d.ts +86 -0
  148. package/lib/typescript/i18n/strings/en.d.ts.map +1 -0
  149. package/lib/typescript/i18n/strings/es.d.ts +3 -0
  150. package/lib/typescript/i18n/strings/es.d.ts.map +1 -0
  151. package/lib/typescript/i18n/strings/fr.d.ts +3 -0
  152. package/lib/typescript/i18n/strings/fr.d.ts.map +1 -0
  153. package/lib/typescript/i18n/strings/hi.d.ts +3 -0
  154. package/lib/typescript/i18n/strings/hi.d.ts.map +1 -0
  155. package/lib/typescript/i18n/strings/it.d.ts +3 -0
  156. package/lib/typescript/i18n/strings/it.d.ts.map +1 -0
  157. package/lib/typescript/i18n/strings/ja.d.ts +3 -0
  158. package/lib/typescript/i18n/strings/ja.d.ts.map +1 -0
  159. package/lib/typescript/i18n/strings/ko.d.ts +3 -0
  160. package/lib/typescript/i18n/strings/ko.d.ts.map +1 -0
  161. package/lib/typescript/i18n/strings/nl.d.ts +3 -0
  162. package/lib/typescript/i18n/strings/nl.d.ts.map +1 -0
  163. package/lib/typescript/i18n/strings/pl.d.ts +3 -0
  164. package/lib/typescript/i18n/strings/pl.d.ts.map +1 -0
  165. package/lib/typescript/i18n/strings/pt.d.ts +3 -0
  166. package/lib/typescript/i18n/strings/pt.d.ts.map +1 -0
  167. package/lib/typescript/i18n/strings/ru.d.ts +3 -0
  168. package/lib/typescript/i18n/strings/ru.d.ts.map +1 -0
  169. package/lib/typescript/i18n/strings/tr.d.ts +3 -0
  170. package/lib/typescript/i18n/strings/tr.d.ts.map +1 -0
  171. package/lib/typescript/i18n/strings/zh.d.ts +3 -0
  172. package/lib/typescript/i18n/strings/zh.d.ts.map +1 -0
  173. package/lib/typescript/index.d.ts +4 -2
  174. package/lib/typescript/index.d.ts.map +1 -1
  175. package/lib/typescript/push.d.ts +14 -2
  176. package/lib/typescript/push.d.ts.map +1 -1
  177. package/lib/typescript/review.d.ts +15 -0
  178. package/lib/typescript/review.d.ts.map +1 -0
  179. package/lib/typescript/theme.d.ts +23 -5
  180. package/lib/typescript/theme.d.ts.map +1 -1
  181. package/lib/typescript/types.d.ts +86 -13
  182. package/lib/typescript/types.d.ts.map +1 -1
  183. package/lib/typescript/useSupportThreads.d.ts +20 -4
  184. package/lib/typescript/useSupportThreads.d.ts.map +1 -1
  185. package/lib/typescript/utils.d.ts +8 -6
  186. package/lib/typescript/utils.d.ts.map +1 -1
  187. package/lib/typescript/version.d.ts +7 -0
  188. package/lib/typescript/version.d.ts.map +1 -0
  189. package/package.json +2 -2
  190. package/src/api.ts +75 -23
  191. package/src/components/BottomSheet.tsx +1 -1
  192. package/src/components/LauncherButton.tsx +4 -4
  193. package/src/components/SupportButton.tsx +244 -86
  194. package/src/components/icons.tsx +96 -101
  195. package/src/components/primitives.tsx +9 -161
  196. package/src/components/screens/ChatScreen.tsx +541 -0
  197. package/src/components/screens/LanguageScreen.tsx +172 -0
  198. package/src/i18n/index.ts +81 -0
  199. package/src/i18n/languages.ts +106 -0
  200. package/src/i18n/strings/de.ts +81 -0
  201. package/src/i18n/strings/en.ts +97 -0
  202. package/src/i18n/strings/es.ts +80 -0
  203. package/src/i18n/strings/fr.ts +81 -0
  204. package/src/i18n/strings/hi.ts +80 -0
  205. package/src/i18n/strings/it.ts +80 -0
  206. package/src/i18n/strings/ja.ts +80 -0
  207. package/src/i18n/strings/ko.ts +79 -0
  208. package/src/i18n/strings/nl.ts +80 -0
  209. package/src/i18n/strings/pl.ts +80 -0
  210. package/src/i18n/strings/pt.ts +80 -0
  211. package/src/i18n/strings/ru.ts +80 -0
  212. package/src/i18n/strings/tr.ts +80 -0
  213. package/src/i18n/strings/zh.ts +78 -0
  214. package/src/index.tsx +13 -0
  215. package/src/push.ts +100 -2
  216. package/src/review.ts +54 -0
  217. package/src/theme.ts +182 -28
  218. package/src/types.ts +85 -13
  219. package/src/useSupportThreads.ts +61 -28
  220. package/src/utils.ts +27 -29
  221. package/src/version.ts +6 -0
  222. package/lib/commonjs/components/screens/ComposeScreen.js +0 -228
  223. package/lib/commonjs/components/screens/ComposeScreen.js.map +0 -1
  224. package/lib/commonjs/components/screens/HomeScreen.js +0 -286
  225. package/lib/commonjs/components/screens/HomeScreen.js.map +0 -1
  226. package/lib/commonjs/components/screens/ThreadScreen.js +0 -327
  227. package/lib/commonjs/components/screens/ThreadScreen.js.map +0 -1
  228. package/lib/module/components/screens/ComposeScreen.js +0 -224
  229. package/lib/module/components/screens/ComposeScreen.js.map +0 -1
  230. package/lib/module/components/screens/HomeScreen.js +0 -282
  231. package/lib/module/components/screens/HomeScreen.js.map +0 -1
  232. package/lib/module/components/screens/ThreadScreen.js +0 -325
  233. package/lib/module/components/screens/ThreadScreen.js.map +0 -1
  234. package/lib/typescript/components/screens/ComposeScreen.d.ts +0 -10
  235. package/lib/typescript/components/screens/ComposeScreen.d.ts.map +0 -1
  236. package/lib/typescript/components/screens/HomeScreen.d.ts +0 -14
  237. package/lib/typescript/components/screens/HomeScreen.d.ts.map +0 -1
  238. package/lib/typescript/components/screens/ThreadScreen.d.ts +0 -10
  239. package/lib/typescript/components/screens/ThreadScreen.d.ts.map +0 -1
  240. package/src/components/screens/ComposeScreen.tsx +0 -233
  241. package/src/components/screens/HomeScreen.tsx +0 -287
  242. package/src/components/screens/ThreadScreen.tsx +0 -397
@@ -1,397 +0,0 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react';
2
- import {
3
- KeyboardAvoidingView,
4
- Platform,
5
- ScrollView,
6
- Text,
7
- TextInput,
8
- View,
9
- } from 'react-native';
10
- import type { SupportTheme } from '../../theme';
11
- import type { Thread, ThreadMessage } from '../../types';
12
- import { USER_MESSAGE_MAX, USER_MESSAGE_MIN } from '../../types';
13
- import { dayLabel, formatDate, formatTime, isSameDay, relativeTime } from '../../utils';
14
- import { ArrowUpIcon, Chevron } from '../icons';
15
- import {
16
- IconButton,
17
- StatusIndicator,
18
- textStyles,
19
- TypeChipView,
20
- } from '../primitives';
21
-
22
- /** Consecutive messages from one sender inside this window form a group. */
23
- const GROUP_WINDOW_MS = 5 * 60 * 1000;
24
- /** The counter only appears once the limit is actually in play. */
25
- const COUNTER_VISIBLE_FROM = USER_MESSAGE_MAX - 60;
26
-
27
- interface Row {
28
- message: ThreadMessage;
29
- firstInGroup: boolean;
30
- lastInGroup: boolean;
31
- /** Date separator to draw above this message, if any. */
32
- day: string | null;
33
- /** "New" separator above the first message the user hasn't seen. */
34
- startsUnread: boolean;
35
- }
36
-
37
- function buildRows(messages: ThreadMessage[], unreadCount: number): Row[] {
38
- // Unread messages are always the newest admin ones, so the divider goes
39
- // above the Nth-from-last admin message.
40
- let remaining = unreadCount;
41
- let firstUnreadId: string | null = null;
42
- for (let i = messages.length - 1; i >= 0 && remaining > 0; i -= 1) {
43
- const candidate = messages[i];
44
- if (candidate === undefined || candidate.sender !== 'admin') continue;
45
- remaining -= 1;
46
- firstUnreadId = candidate.id;
47
- }
48
-
49
- return messages.map((message, i) => {
50
- const previous = i > 0 ? (messages[i - 1] ?? null) : null;
51
- const next = i < messages.length - 1 ? (messages[i + 1] ?? null) : null;
52
- const withinGroup = (a: ThreadMessage | null, b: ThreadMessage) =>
53
- a !== null &&
54
- a.sender === b.sender &&
55
- a.admin_name === b.admin_name &&
56
- Math.abs(Date.parse(b.created_at) - Date.parse(a.created_at)) <
57
- GROUP_WINDOW_MS;
58
-
59
- const day =
60
- previous === null || !isSameDay(previous.created_at, message.created_at)
61
- ? dayLabel(message.created_at)
62
- : null;
63
- const startsUnread = message.id === firstUnreadId;
64
-
65
- return {
66
- message,
67
- // A separator always breaks a group.
68
- firstInGroup: day !== null || startsUnread || !withinGroup(previous, message),
69
- lastInGroup:
70
- next === null ||
71
- next.id === firstUnreadId ||
72
- !isSameDay(next.created_at, message.created_at) ||
73
- !withinGroup(next, message),
74
- day,
75
- startsUnread,
76
- };
77
- });
78
- }
79
-
80
- function Separator({
81
- label,
82
- theme,
83
- tone = 'muted',
84
- }: {
85
- label: string;
86
- theme: SupportTheme;
87
- tone?: 'muted' | 'accent';
88
- }) {
89
- const color =
90
- tone === 'accent' ? theme.colors.accent : theme.colors.textTertiary;
91
- return (
92
- <View
93
- style={{
94
- flexDirection: 'row',
95
- alignItems: 'center',
96
- gap: 10,
97
- paddingVertical: 6,
98
- }}
99
- >
100
- <View
101
- style={{
102
- flex: 1,
103
- height: 1,
104
- backgroundColor:
105
- tone === 'accent' ? theme.colors.accentMuted : theme.colors.border,
106
- }}
107
- />
108
- <Text
109
- style={{
110
- fontSize: 11,
111
- fontWeight: '600',
112
- letterSpacing: 0.3,
113
- color,
114
- }}
115
- >
116
- {label}
117
- </Text>
118
- <View
119
- style={{
120
- flex: 1,
121
- height: 1,
122
- backgroundColor:
123
- tone === 'accent' ? theme.colors.accentMuted : theme.colors.border,
124
- }}
125
- />
126
- </View>
127
- );
128
- }
129
-
130
- function MessageBubble({ row, theme }: { row: Row; theme: SupportTheme }) {
131
- const { message, firstInGroup, lastInGroup } = row;
132
- const own = message.sender === 'user';
133
- const round = theme.radii.bubble;
134
- const tight = theme.radii.bubbleTail;
135
-
136
- return (
137
- <View
138
- style={{
139
- maxWidth: '86%',
140
- alignSelf: own ? 'flex-end' : 'flex-start',
141
- alignItems: own ? 'flex-end' : 'flex-start',
142
- marginTop: firstInGroup ? 10 : 2,
143
- gap: 4,
144
- }}
145
- >
146
- {!own && firstInGroup && (
147
- <Text style={[textStyles.meta(theme), { paddingHorizontal: 4 }]}>
148
- {message.admin_name ?? 'Support'}
149
- </Text>
150
- )}
151
- <View
152
- style={{
153
- paddingHorizontal: 14,
154
- paddingVertical: 10,
155
- backgroundColor: own ? theme.colors.accent : theme.colors.surface,
156
- borderWidth: own ? 0 : 1,
157
- borderColor: theme.colors.border,
158
- borderTopLeftRadius: own || firstInGroup ? round : tight,
159
- borderBottomLeftRadius: own || lastInGroup ? round : tight,
160
- borderTopRightRadius: !own || firstInGroup ? round : tight,
161
- borderBottomRightRadius: !own || lastInGroup ? round : tight,
162
- }}
163
- >
164
- <Text
165
- style={[
166
- textStyles.body(theme),
167
- own && { color: theme.colors.accentForeground },
168
- ]}
169
- >
170
- {message.body}
171
- </Text>
172
- </View>
173
- {lastInGroup && (
174
- <Text style={[textStyles.meta(theme), { paddingHorizontal: 4 }]}>
175
- {formatTime(message.created_at)}
176
- </Text>
177
- )}
178
- </View>
179
- );
180
- }
181
-
182
- export function ThreadScreen({
183
- theme,
184
- thread,
185
- onBack,
186
- onSend,
187
- }: {
188
- theme: SupportTheme;
189
- thread: Thread;
190
- onBack: () => void;
191
- /** Resolves null on success, or a user-facing error message. */
192
- onSend: (message: string) => Promise<string | null>;
193
- }) {
194
- const [draft, setDraft] = useState('');
195
- const [sending, setSending] = useState(false);
196
- const [failedMessage, setFailedMessage] = useState<string | null>(null);
197
- const scrollRef = useRef<ScrollView>(null);
198
- // Captured once: opening the thread marks it read within a tick, but the
199
- // "New" divider should stay put while the user is reading.
200
- const [unreadOnOpen] = useState(thread.unread_count);
201
-
202
- const resolved = thread.status === 'resolved';
203
- const trimmed = draft.trim();
204
- const valid =
205
- trimmed.length >= USER_MESSAGE_MIN && trimmed.length <= USER_MESSAGE_MAX;
206
- const tooShort = trimmed.length > 0 && trimmed.length < USER_MESSAGE_MIN;
207
-
208
- const rows = useMemo(
209
- () => buildRows(thread.messages, unreadOnOpen),
210
- [thread.messages, unreadOnOpen],
211
- );
212
-
213
- useEffect(() => {
214
- scrollRef.current?.scrollToEnd({ animated: false });
215
- }, [thread.messages.length]);
216
-
217
- async function handleSend() {
218
- if (!valid || sending || resolved) return;
219
- setSending(true);
220
- setFailedMessage(null);
221
- const errorMessage = await onSend(trimmed);
222
- setSending(false);
223
- if (errorMessage === null) setDraft('');
224
- else setFailedMessage(errorMessage);
225
- }
226
-
227
- return (
228
- <KeyboardAvoidingView
229
- style={{ flex: 1 }}
230
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
231
- >
232
- {/* Header */}
233
- <View
234
- style={{
235
- flexDirection: 'row',
236
- alignItems: 'center',
237
- gap: 10,
238
- paddingHorizontal: 12,
239
- paddingBottom: 12,
240
- borderBottomWidth: 1,
241
- borderBottomColor: theme.colors.border,
242
- }}
243
- >
244
- <IconButton
245
- onPress={onBack}
246
- theme={theme}
247
- accessibilityLabel="Back to conversations"
248
- size={32}
249
- >
250
- <Chevron color={theme.colors.textSecondary} direction="left" size={12} />
251
- </IconButton>
252
- <TypeChipView type={thread.type} theme={theme} />
253
- <StatusIndicator status={thread.status} theme={theme} />
254
- <Text style={[textStyles.meta(theme), { marginLeft: 'auto' }]}>
255
- {relativeTime(thread.created_at)}
256
- </Text>
257
- </View>
258
-
259
- {/* Messages */}
260
- <ScrollView
261
- ref={scrollRef}
262
- style={{ flex: 1 }}
263
- contentContainerStyle={{ paddingHorizontal: 16, paddingVertical: 12 }}
264
- keyboardShouldPersistTaps="handled"
265
- onContentSizeChange={() =>
266
- scrollRef.current?.scrollToEnd({ animated: false })
267
- }
268
- >
269
- {rows.map((row) => (
270
- <View key={row.message.id}>
271
- {row.day !== null && <Separator label={row.day} theme={theme} />}
272
- {row.startsUnread && (
273
- <Separator label="New" theme={theme} tone="accent" />
274
- )}
275
- <MessageBubble row={row} theme={theme} />
276
- </View>
277
- ))}
278
-
279
- {resolved && thread.resolved_at !== null && (
280
- <View style={{ marginTop: 16 }}>
281
- <Separator
282
- label={`Resolved by ${thread.resolved_by_name ?? 'support'} · ${formatDate(
283
- thread.resolved_at,
284
- )}`}
285
- theme={theme}
286
- />
287
- </View>
288
- )}
289
- </ScrollView>
290
-
291
- {/* Composer */}
292
- <View
293
- style={{
294
- borderTopWidth: 1,
295
- borderTopColor: theme.colors.border,
296
- paddingHorizontal: 16,
297
- paddingTop: 10,
298
- paddingBottom: 4,
299
- }}
300
- >
301
- {resolved ? (
302
- <View
303
- style={{
304
- alignItems: 'center',
305
- paddingVertical: 10,
306
- borderRadius: theme.radii.control,
307
- backgroundColor: theme.colors.surface,
308
- }}
309
- >
310
- <Text style={textStyles.secondary(theme)}>
311
- This conversation is resolved and read-only
312
- </Text>
313
- </View>
314
- ) : (
315
- <>
316
- <View style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 10 }}>
317
- <TextInput
318
- value={draft}
319
- onChangeText={(v) => {
320
- setDraft(v);
321
- setFailedMessage(null);
322
- }}
323
- multiline
324
- editable={!sending}
325
- placeholder="Write a reply…"
326
- placeholderTextColor={theme.colors.textTertiary}
327
- style={{
328
- flex: 1,
329
- minHeight: 44,
330
- maxHeight: 120,
331
- borderWidth: 1,
332
- borderColor: theme.colors.border,
333
- borderRadius: theme.radii.cardLg,
334
- backgroundColor: theme.colors.surface,
335
- color: theme.colors.textPrimary,
336
- fontSize: 15,
337
- lineHeight: 20,
338
- paddingHorizontal: 14,
339
- paddingTop: 12,
340
- paddingBottom: 12,
341
- textAlignVertical: 'top',
342
- }}
343
- />
344
- <IconButton
345
- onPress={handleSend}
346
- theme={theme}
347
- tone="accent"
348
- size={44}
349
- disabled={!valid || sending}
350
- accessibilityLabel={sending ? 'Sending' : 'Send reply'}
351
- >
352
- <ArrowUpIcon color={theme.colors.accentForeground} size={19} />
353
- </IconButton>
354
- </View>
355
-
356
- {(tooShort ||
357
- draft.length >= COUNTER_VISIBLE_FROM ||
358
- failedMessage !== null) && (
359
- <View
360
- style={{
361
- flexDirection: 'row',
362
- alignItems: 'center',
363
- gap: 10,
364
- paddingTop: 8,
365
- paddingHorizontal: 4,
366
- }}
367
- >
368
- {failedMessage !== null ? (
369
- <Text style={{ fontSize: 12, color: theme.colors.statusDanger }}>
370
- {failedMessage}
371
- </Text>
372
- ) : tooShort ? (
373
- <Text style={textStyles.meta(theme)}>
374
- At least {USER_MESSAGE_MIN} characters
375
- </Text>
376
- ) : null}
377
- {draft.length >= COUNTER_VISIBLE_FROM && (
378
- <Text
379
- style={[
380
- textStyles.meta(theme),
381
- { marginLeft: 'auto' },
382
- draft.length > USER_MESSAGE_MAX && {
383
- color: theme.colors.statusDanger,
384
- },
385
- ]}
386
- >
387
- {draft.length}/{USER_MESSAGE_MAX}
388
- </Text>
389
- )}
390
- </View>
391
- )}
392
- </>
393
- )}
394
- </View>
395
- </KeyboardAvoidingView>
396
- );
397
- }