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