@appsupportkit/support-sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/api.js +17 -2
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/components/SupportButton.js +123 -23
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +36 -0
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +9 -7
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ComposeScreen.js +14 -23
- package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/HomeScreen.js +70 -22
- package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
- package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
- package/lib/commonjs/components/screens/ThreadScreen.js +31 -19
- package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
- package/lib/commonjs/i18n/index.js +101 -0
- package/lib/commonjs/i18n/index.js.map +1 -0
- package/lib/commonjs/i18n/languages.js +95 -0
- package/lib/commonjs/i18n/languages.js.map +1 -0
- package/lib/commonjs/i18n/strings/de.js +72 -0
- package/lib/commonjs/i18n/strings/de.js.map +1 -0
- package/lib/commonjs/i18n/strings/en.js +88 -0
- package/lib/commonjs/i18n/strings/en.js.map +1 -0
- package/lib/commonjs/i18n/strings/es.js +72 -0
- package/lib/commonjs/i18n/strings/es.js.map +1 -0
- package/lib/commonjs/i18n/strings/fr.js +72 -0
- package/lib/commonjs/i18n/strings/fr.js.map +1 -0
- package/lib/commonjs/i18n/strings/hi.js +72 -0
- package/lib/commonjs/i18n/strings/hi.js.map +1 -0
- package/lib/commonjs/i18n/strings/it.js +72 -0
- package/lib/commonjs/i18n/strings/it.js.map +1 -0
- package/lib/commonjs/i18n/strings/ja.js +72 -0
- package/lib/commonjs/i18n/strings/ja.js.map +1 -0
- package/lib/commonjs/i18n/strings/ko.js +72 -0
- package/lib/commonjs/i18n/strings/ko.js.map +1 -0
- package/lib/commonjs/i18n/strings/nl.js +72 -0
- package/lib/commonjs/i18n/strings/nl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pl.js +72 -0
- package/lib/commonjs/i18n/strings/pl.js.map +1 -0
- package/lib/commonjs/i18n/strings/pt.js +72 -0
- package/lib/commonjs/i18n/strings/pt.js.map +1 -0
- package/lib/commonjs/i18n/strings/ru.js +72 -0
- package/lib/commonjs/i18n/strings/ru.js.map +1 -0
- package/lib/commonjs/i18n/strings/tr.js +72 -0
- package/lib/commonjs/i18n/strings/tr.js.map +1 -0
- package/lib/commonjs/i18n/strings/zh.js +72 -0
- package/lib/commonjs/i18n/strings/zh.js.map +1 -0
- package/lib/commonjs/index.js +31 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/types.js +11 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/useSupportThreads.js +19 -9
- package/lib/commonjs/useSupportThreads.js.map +1 -1
- package/lib/commonjs/utils.js +32 -17
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/module/api.js +16 -2
- package/lib/module/api.js.map +1 -1
- package/lib/module/components/SupportButton.js +126 -26
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +35 -0
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +9 -7
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ComposeScreen.js +15 -22
- package/lib/module/components/screens/ComposeScreen.js.map +1 -1
- package/lib/module/components/screens/HomeScreen.js +71 -23
- package/lib/module/components/screens/HomeScreen.js.map +1 -1
- package/lib/module/components/screens/LanguageScreen.js +147 -0
- package/lib/module/components/screens/LanguageScreen.js.map +1 -0
- package/lib/module/components/screens/ThreadScreen.js +31 -19
- package/lib/module/components/screens/ThreadScreen.js.map +1 -1
- package/lib/module/i18n/index.js +56 -0
- package/lib/module/i18n/index.js.map +1 -0
- package/lib/module/i18n/languages.js +88 -0
- package/lib/module/i18n/languages.js.map +1 -0
- package/lib/module/i18n/strings/de.js +68 -0
- package/lib/module/i18n/strings/de.js.map +1 -0
- package/lib/module/i18n/strings/en.js +84 -0
- package/lib/module/i18n/strings/en.js.map +1 -0
- package/lib/module/i18n/strings/es.js +68 -0
- package/lib/module/i18n/strings/es.js.map +1 -0
- package/lib/module/i18n/strings/fr.js +68 -0
- package/lib/module/i18n/strings/fr.js.map +1 -0
- package/lib/module/i18n/strings/hi.js +68 -0
- package/lib/module/i18n/strings/hi.js.map +1 -0
- package/lib/module/i18n/strings/it.js +68 -0
- package/lib/module/i18n/strings/it.js.map +1 -0
- package/lib/module/i18n/strings/ja.js +68 -0
- package/lib/module/i18n/strings/ja.js.map +1 -0
- package/lib/module/i18n/strings/ko.js +68 -0
- package/lib/module/i18n/strings/ko.js.map +1 -0
- package/lib/module/i18n/strings/nl.js +68 -0
- package/lib/module/i18n/strings/nl.js.map +1 -0
- package/lib/module/i18n/strings/pl.js +68 -0
- package/lib/module/i18n/strings/pl.js.map +1 -0
- package/lib/module/i18n/strings/pt.js +68 -0
- package/lib/module/i18n/strings/pt.js.map +1 -0
- package/lib/module/i18n/strings/ru.js +68 -0
- package/lib/module/i18n/strings/ru.js.map +1 -0
- package/lib/module/i18n/strings/tr.js +68 -0
- package/lib/module/i18n/strings/tr.js.map +1 -0
- package/lib/module/i18n/strings/zh.js +68 -0
- package/lib/module/i18n/strings/zh.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +11 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/useSupportThreads.js +19 -9
- package/lib/module/useSupportThreads.js.map +1 -1
- package/lib/module/utils.js +32 -17
- package/lib/module/utils.js.map +1 -1
- package/lib/typescript/api.d.ts +9 -0
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/components/SupportButton.d.ts.map +1 -1
- package/lib/typescript/components/icons.d.ts +5 -0
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +7 -3
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts +3 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +6 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
- package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
- package/lib/typescript/components/screens/ThreadScreen.d.ts +3 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
- package/lib/typescript/i18n/index.d.ts +15 -0
- package/lib/typescript/i18n/index.d.ts.map +1 -0
- package/lib/typescript/i18n/languages.d.ts +36 -0
- package/lib/typescript/i18n/languages.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/de.d.ts +3 -0
- package/lib/typescript/i18n/strings/de.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/en.d.ts +83 -0
- package/lib/typescript/i18n/strings/en.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/es.d.ts +3 -0
- package/lib/typescript/i18n/strings/es.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/fr.d.ts +3 -0
- package/lib/typescript/i18n/strings/fr.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/hi.d.ts +3 -0
- package/lib/typescript/i18n/strings/hi.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/it.d.ts +3 -0
- package/lib/typescript/i18n/strings/it.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ja.d.ts +3 -0
- package/lib/typescript/i18n/strings/ja.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ko.d.ts +3 -0
- package/lib/typescript/i18n/strings/ko.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/nl.d.ts +3 -0
- package/lib/typescript/i18n/strings/nl.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/pl.d.ts +3 -0
- package/lib/typescript/i18n/strings/pl.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/pt.d.ts +3 -0
- package/lib/typescript/i18n/strings/pt.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/ru.d.ts +3 -0
- package/lib/typescript/i18n/strings/ru.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/tr.d.ts +3 -0
- package/lib/typescript/i18n/strings/tr.d.ts.map +1 -0
- package/lib/typescript/i18n/strings/zh.d.ts +3 -0
- package/lib/typescript/i18n/strings/zh.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +32 -2
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/useSupportThreads.d.ts +11 -1
- package/lib/typescript/useSupportThreads.d.ts.map +1 -1
- package/lib/typescript/utils.d.ts +6 -5
- package/lib/typescript/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/api.ts +24 -1
- package/src/components/SupportButton.tsx +162 -33
- package/src/components/icons.tsx +36 -0
- package/src/components/primitives.tsx +14 -4
- package/src/components/screens/ComposeScreen.tsx +19 -23
- package/src/components/screens/HomeScreen.tsx +84 -25
- package/src/components/screens/LanguageScreen.tsx +172 -0
- package/src/components/screens/ThreadScreen.tsx +48 -21
- package/src/i18n/index.ts +81 -0
- package/src/i18n/languages.ts +106 -0
- package/src/i18n/strings/de.ts +78 -0
- package/src/i18n/strings/en.ts +94 -0
- package/src/i18n/strings/es.ts +77 -0
- package/src/i18n/strings/fr.ts +78 -0
- package/src/i18n/strings/hi.ts +77 -0
- package/src/i18n/strings/it.ts +77 -0
- package/src/i18n/strings/ja.ts +77 -0
- package/src/i18n/strings/ko.ts +76 -0
- package/src/i18n/strings/nl.ts +77 -0
- package/src/i18n/strings/pl.ts +77 -0
- package/src/i18n/strings/pt.ts +77 -0
- package/src/i18n/strings/ru.ts +77 -0
- package/src/i18n/strings/tr.ts +77 -0
- package/src/i18n/strings/zh.ts +75 -0
- package/src/index.tsx +8 -0
- package/src/types.ts +32 -2
- package/src/useSupportThreads.ts +30 -10
- package/src/utils.ts +22 -17
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { RefreshControl, ScrollView, Text, View } from 'react-native';
|
|
2
|
+
import type { Translator } from '../../i18n';
|
|
3
|
+
import { LANGUAGE_LABELS, type Language } from '../../i18n/languages';
|
|
2
4
|
import type { SupportTheme } from '../../theme';
|
|
3
5
|
import type { QueryType, Thread } from '../../types';
|
|
4
|
-
import { QUERY_TYPE_LABEL } from '../../types';
|
|
5
6
|
import { relativeTime } from '../../utils';
|
|
6
|
-
import { Chevron, CloseIcon } from '../icons';
|
|
7
|
+
import { Chevron, CloseIcon, GlobeIcon } from '../icons';
|
|
7
8
|
import {
|
|
8
9
|
EmptyState,
|
|
9
10
|
ErrorState,
|
|
@@ -18,25 +19,22 @@ import {
|
|
|
18
19
|
|
|
19
20
|
const COMPOSE_TYPES: QueryType[] = ['feedback', 'feature_request', 'bug_report'];
|
|
20
21
|
|
|
21
|
-
const COMPOSE_HINT: Record<QueryType, string> = {
|
|
22
|
-
feedback: 'Tell us what you think',
|
|
23
|
-
feature_request: 'Suggest an improvement',
|
|
24
|
-
bug_report: "Something isn't working",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
22
|
function ThreadRow({
|
|
28
23
|
thread,
|
|
29
24
|
theme,
|
|
25
|
+
strings,
|
|
30
26
|
onPress,
|
|
31
27
|
last,
|
|
32
28
|
}: {
|
|
33
29
|
thread: Thread;
|
|
34
30
|
theme: SupportTheme;
|
|
31
|
+
strings: Translator;
|
|
35
32
|
onPress: () => void;
|
|
36
33
|
last: boolean;
|
|
37
34
|
}) {
|
|
38
35
|
const unread = thread.unread_count > 0;
|
|
39
|
-
const
|
|
36
|
+
const type = strings.t(`type.${thread.type}`);
|
|
37
|
+
const preview = thread.last_message?.body ?? strings.t('home.noMessages');
|
|
40
38
|
const at = thread.last_message?.created_at ?? thread.created_at;
|
|
41
39
|
|
|
42
40
|
return (
|
|
@@ -45,8 +43,11 @@ function ThreadRow({
|
|
|
45
43
|
theme={theme}
|
|
46
44
|
accessibilityLabel={
|
|
47
45
|
unread
|
|
48
|
-
?
|
|
49
|
-
|
|
46
|
+
? strings.t('home.threadUnreadA11y', {
|
|
47
|
+
type,
|
|
48
|
+
count: thread.unread_count,
|
|
49
|
+
})
|
|
50
|
+
: strings.t('home.threadA11y', { type })
|
|
50
51
|
}
|
|
51
52
|
style={{
|
|
52
53
|
flexDirection: 'row',
|
|
@@ -73,9 +74,9 @@ function ThreadRow({
|
|
|
73
74
|
color: theme.colors.textPrimary,
|
|
74
75
|
}}
|
|
75
76
|
>
|
|
76
|
-
{
|
|
77
|
+
{type}
|
|
77
78
|
</Text>
|
|
78
|
-
<Text style={textStyles.meta(theme)}>{relativeTime(at)}</Text>
|
|
79
|
+
<Text style={textStyles.meta(theme)}>{relativeTime(at, strings)}</Text>
|
|
79
80
|
</View>
|
|
80
81
|
|
|
81
82
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
@@ -88,7 +89,9 @@ function ThreadRow({
|
|
|
88
89
|
color: unread ? theme.colors.textPrimary : theme.colors.textSecondary,
|
|
89
90
|
}}
|
|
90
91
|
>
|
|
91
|
-
{thread.last_message?.sender === 'user'
|
|
92
|
+
{thread.last_message?.sender === 'user'
|
|
93
|
+
? `${strings.t('home.you')}: `
|
|
94
|
+
: ''}
|
|
92
95
|
{preview}
|
|
93
96
|
</Text>
|
|
94
97
|
{unread ? (
|
|
@@ -106,11 +109,13 @@ function ThreadGroup({
|
|
|
106
109
|
label,
|
|
107
110
|
threads,
|
|
108
111
|
theme,
|
|
112
|
+
strings,
|
|
109
113
|
onOpenThread,
|
|
110
114
|
}: {
|
|
111
115
|
label: string;
|
|
112
116
|
threads: Thread[];
|
|
113
117
|
theme: SupportTheme;
|
|
118
|
+
strings: Translator;
|
|
114
119
|
onOpenThread: (thread: Thread) => void;
|
|
115
120
|
}) {
|
|
116
121
|
if (threads.length === 0) return null;
|
|
@@ -134,6 +139,7 @@ function ThreadGroup({
|
|
|
134
139
|
key={thread.id}
|
|
135
140
|
thread={thread}
|
|
136
141
|
theme={theme}
|
|
142
|
+
strings={strings}
|
|
137
143
|
onPress={() => onOpenThread(thread)}
|
|
138
144
|
last={i === threads.length - 1}
|
|
139
145
|
/>
|
|
@@ -145,6 +151,8 @@ function ThreadGroup({
|
|
|
145
151
|
|
|
146
152
|
export function HomeScreen({
|
|
147
153
|
theme,
|
|
154
|
+
strings,
|
|
155
|
+
language,
|
|
148
156
|
threads,
|
|
149
157
|
loading,
|
|
150
158
|
refreshing,
|
|
@@ -152,9 +160,12 @@ export function HomeScreen({
|
|
|
152
160
|
onRefresh,
|
|
153
161
|
onCompose,
|
|
154
162
|
onOpenThread,
|
|
163
|
+
onChangeLanguage,
|
|
155
164
|
onClose,
|
|
156
165
|
}: {
|
|
157
166
|
theme: SupportTheme;
|
|
167
|
+
strings: Translator;
|
|
168
|
+
language: Language;
|
|
158
169
|
threads: Thread[] | null;
|
|
159
170
|
loading: boolean;
|
|
160
171
|
refreshing: boolean;
|
|
@@ -162,6 +173,7 @@ export function HomeScreen({
|
|
|
162
173
|
onRefresh: () => void;
|
|
163
174
|
onCompose: (type: QueryType) => void;
|
|
164
175
|
onOpenThread: (thread: Thread) => void;
|
|
176
|
+
onChangeLanguage: () => void;
|
|
165
177
|
onClose: () => void;
|
|
166
178
|
}) {
|
|
167
179
|
const open = (threads ?? []).filter((t) => t.status === 'open');
|
|
@@ -180,15 +192,15 @@ export function HomeScreen({
|
|
|
180
192
|
}}
|
|
181
193
|
>
|
|
182
194
|
<View style={{ flex: 1, gap: 2 }}>
|
|
183
|
-
<Text style={textStyles.title(theme)}>
|
|
195
|
+
<Text style={textStyles.title(theme)}>{strings.t('home.title')}</Text>
|
|
184
196
|
<Text style={textStyles.secondary(theme)}>
|
|
185
|
-
|
|
197
|
+
{strings.t('home.subtitle')}
|
|
186
198
|
</Text>
|
|
187
199
|
</View>
|
|
188
200
|
<IconButton
|
|
189
201
|
onPress={onClose}
|
|
190
202
|
theme={theme}
|
|
191
|
-
accessibilityLabel=
|
|
203
|
+
accessibilityLabel={strings.t('home.close')}
|
|
192
204
|
size={32}
|
|
193
205
|
>
|
|
194
206
|
<CloseIcon color={theme.colors.textSecondary} size={12} />
|
|
@@ -225,7 +237,9 @@ export function HomeScreen({
|
|
|
225
237
|
key={type}
|
|
226
238
|
onPress={() => onCompose(type)}
|
|
227
239
|
theme={theme}
|
|
228
|
-
accessibilityLabel={
|
|
240
|
+
accessibilityLabel={strings.t('home.newA11y', {
|
|
241
|
+
type: strings.t(`type.${type}`),
|
|
242
|
+
})}
|
|
229
243
|
style={{
|
|
230
244
|
flexDirection: 'row',
|
|
231
245
|
alignItems: 'center',
|
|
@@ -239,20 +253,63 @@ export function HomeScreen({
|
|
|
239
253
|
<TypeGlyph type={type} theme={theme} size={34} />
|
|
240
254
|
<View style={{ flex: 1, gap: 1 }}>
|
|
241
255
|
<Text style={textStyles.heading(theme)}>
|
|
242
|
-
{
|
|
256
|
+
{strings.t(`type.${type}`)}
|
|
257
|
+
</Text>
|
|
258
|
+
<Text style={textStyles.meta(theme)}>
|
|
259
|
+
{strings.t(`hint.${type}`)}
|
|
243
260
|
</Text>
|
|
244
|
-
<Text style={textStyles.meta(theme)}>{COMPOSE_HINT[type]}</Text>
|
|
245
261
|
</View>
|
|
246
262
|
<Chevron color={theme.colors.textTertiary} />
|
|
247
263
|
</PressableRow>
|
|
248
264
|
))}
|
|
249
265
|
</View>
|
|
250
266
|
|
|
267
|
+
{/* Language — a quiet row rather than a settings screen, since it is
|
|
268
|
+
the only preference the sheet has. */}
|
|
269
|
+
<View
|
|
270
|
+
style={{
|
|
271
|
+
marginTop: 12,
|
|
272
|
+
marginHorizontal: 16,
|
|
273
|
+
borderWidth: 1,
|
|
274
|
+
borderColor: theme.colors.border,
|
|
275
|
+
borderRadius: theme.radii.card,
|
|
276
|
+
backgroundColor: theme.colors.surface,
|
|
277
|
+
overflow: 'hidden',
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
<PressableRow
|
|
281
|
+
onPress={onChangeLanguage}
|
|
282
|
+
theme={theme}
|
|
283
|
+
accessibilityLabel={strings.t('lang.change')}
|
|
284
|
+
style={{
|
|
285
|
+
flexDirection: 'row',
|
|
286
|
+
alignItems: 'center',
|
|
287
|
+
gap: 12,
|
|
288
|
+
paddingHorizontal: 14,
|
|
289
|
+
paddingVertical: 11,
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
<GlobeIcon color={theme.colors.textTertiary} size={16} />
|
|
293
|
+
<Text style={[textStyles.secondary(theme), { flex: 1 }]}>
|
|
294
|
+
{strings.t('home.language')}
|
|
295
|
+
</Text>
|
|
296
|
+
<Text style={textStyles.meta(theme)}>
|
|
297
|
+
{LANGUAGE_LABELS[language]}
|
|
298
|
+
</Text>
|
|
299
|
+
<Chevron color={theme.colors.textTertiary} />
|
|
300
|
+
</PressableRow>
|
|
301
|
+
</View>
|
|
302
|
+
|
|
251
303
|
{/* Threads */}
|
|
252
304
|
{error !== null && threads === null ? (
|
|
253
305
|
// Only take over the screen when there's nothing cached to show —
|
|
254
306
|
// a failed background poll shouldn't hide an already-loaded list.
|
|
255
|
-
<ErrorState
|
|
307
|
+
<ErrorState
|
|
308
|
+
message={error}
|
|
309
|
+
onRetry={onRefresh}
|
|
310
|
+
theme={theme}
|
|
311
|
+
strings={strings}
|
|
312
|
+
/>
|
|
256
313
|
) : loading && threads === null ? (
|
|
257
314
|
<View style={{ paddingHorizontal: 16, paddingTop: 28, gap: 12 }}>
|
|
258
315
|
<SkeletonBlock width={110} height={11} theme={theme} radius={4} />
|
|
@@ -261,22 +318,24 @@ export function HomeScreen({
|
|
|
261
318
|
</View>
|
|
262
319
|
) : (threads ?? []).length === 0 ? (
|
|
263
320
|
<EmptyState
|
|
264
|
-
message=
|
|
265
|
-
hint=
|
|
321
|
+
message={strings.t('home.empty')}
|
|
322
|
+
hint={strings.t('home.emptyHint')}
|
|
266
323
|
theme={theme}
|
|
267
324
|
/>
|
|
268
325
|
) : (
|
|
269
326
|
<>
|
|
270
327
|
<ThreadGroup
|
|
271
|
-
label={
|
|
328
|
+
label={`${strings.t('home.open')} · ${open.length}`}
|
|
272
329
|
threads={open}
|
|
273
330
|
theme={theme}
|
|
331
|
+
strings={strings}
|
|
274
332
|
onOpenThread={onOpenThread}
|
|
275
333
|
/>
|
|
276
334
|
<ThreadGroup
|
|
277
|
-
label=
|
|
335
|
+
label={strings.t('home.resolved')}
|
|
278
336
|
threads={resolved}
|
|
279
337
|
theme={theme}
|
|
338
|
+
strings={strings}
|
|
280
339
|
onOpenThread={onOpenThread}
|
|
281
340
|
/>
|
|
282
341
|
</>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { ScrollView, Text, View } from 'react-native';
|
|
3
|
+
import type { Translator } from '../../i18n';
|
|
4
|
+
import {
|
|
5
|
+
LANGUAGE_LABELS,
|
|
6
|
+
SUPPORTED_LANGUAGES,
|
|
7
|
+
type Language,
|
|
8
|
+
} from '../../i18n/languages';
|
|
9
|
+
import type { SupportTheme } from '../../theme';
|
|
10
|
+
import { CheckIcon, Chevron, CloseIcon } from '../icons';
|
|
11
|
+
import {
|
|
12
|
+
IconButton,
|
|
13
|
+
PressableRow,
|
|
14
|
+
PrimaryButton,
|
|
15
|
+
textStyles,
|
|
16
|
+
} from '../primitives';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The one-time language prompt, and the same screen again whenever the user
|
|
20
|
+
* changes their mind from the home header.
|
|
21
|
+
*
|
|
22
|
+
* Selecting a row re-renders the whole sheet in that language immediately —
|
|
23
|
+
* the confirmation button is the only way to see what you are choosing before
|
|
24
|
+
* you commit to it.
|
|
25
|
+
*/
|
|
26
|
+
export function LanguageScreen({
|
|
27
|
+
theme,
|
|
28
|
+
strings,
|
|
29
|
+
selected,
|
|
30
|
+
onSelect,
|
|
31
|
+
onConfirm,
|
|
32
|
+
onBack,
|
|
33
|
+
onDismiss,
|
|
34
|
+
saving,
|
|
35
|
+
error,
|
|
36
|
+
}: {
|
|
37
|
+
theme: SupportTheme;
|
|
38
|
+
strings: Translator;
|
|
39
|
+
selected: Language;
|
|
40
|
+
onSelect: (language: Language) => void;
|
|
41
|
+
onConfirm: () => void;
|
|
42
|
+
/** Absent on first run: there is nothing to go back to yet. */
|
|
43
|
+
onBack?: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* First run only — dismissing without choosing closes the sheet and leaves
|
|
46
|
+
* the preference unset, so the prompt appears again next time. Saving a
|
|
47
|
+
* language the user never actually picked would be worse: every message
|
|
48
|
+
* they write would be translated from the wrong one.
|
|
49
|
+
*/
|
|
50
|
+
onDismiss?: () => void;
|
|
51
|
+
saving: boolean;
|
|
52
|
+
error: string | null;
|
|
53
|
+
}) {
|
|
54
|
+
// Held so the list doesn't reorder under a finger mid-scroll.
|
|
55
|
+
const [languages] = useState(() => [...SUPPORTED_LANGUAGES]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View style={{ flex: 1 }}>
|
|
59
|
+
<View
|
|
60
|
+
style={{
|
|
61
|
+
flexDirection: 'row',
|
|
62
|
+
alignItems: 'flex-start',
|
|
63
|
+
paddingHorizontal: 16,
|
|
64
|
+
paddingTop: 4,
|
|
65
|
+
paddingBottom: 16,
|
|
66
|
+
gap: 10,
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{onBack !== undefined && (
|
|
70
|
+
<IconButton
|
|
71
|
+
onPress={onBack}
|
|
72
|
+
theme={theme}
|
|
73
|
+
accessibilityLabel={strings.t('compose.back')}
|
|
74
|
+
size={32}
|
|
75
|
+
>
|
|
76
|
+
<Chevron
|
|
77
|
+
color={theme.colors.textSecondary}
|
|
78
|
+
direction="left"
|
|
79
|
+
size={12}
|
|
80
|
+
/>
|
|
81
|
+
</IconButton>
|
|
82
|
+
)}
|
|
83
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
84
|
+
<Text style={textStyles.title(theme)}>{strings.t('lang.title')}</Text>
|
|
85
|
+
<Text style={textStyles.secondary(theme)}>
|
|
86
|
+
{strings.t('lang.subtitle')}
|
|
87
|
+
</Text>
|
|
88
|
+
</View>
|
|
89
|
+
{onDismiss !== undefined && (
|
|
90
|
+
<IconButton
|
|
91
|
+
onPress={onDismiss}
|
|
92
|
+
theme={theme}
|
|
93
|
+
accessibilityLabel={strings.t('home.close')}
|
|
94
|
+
size={32}
|
|
95
|
+
>
|
|
96
|
+
<CloseIcon color={theme.colors.textSecondary} size={12} />
|
|
97
|
+
</IconButton>
|
|
98
|
+
)}
|
|
99
|
+
</View>
|
|
100
|
+
|
|
101
|
+
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 8 }}>
|
|
102
|
+
<View
|
|
103
|
+
style={{
|
|
104
|
+
marginHorizontal: 16,
|
|
105
|
+
borderWidth: 1,
|
|
106
|
+
borderColor: theme.colors.border,
|
|
107
|
+
borderRadius: theme.radii.card,
|
|
108
|
+
backgroundColor: theme.colors.surface,
|
|
109
|
+
overflow: 'hidden',
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{languages.map((language, i) => {
|
|
113
|
+
const active = language === selected;
|
|
114
|
+
return (
|
|
115
|
+
<PressableRow
|
|
116
|
+
key={language}
|
|
117
|
+
onPress={() => onSelect(language)}
|
|
118
|
+
theme={theme}
|
|
119
|
+
accessibilityLabel={LANGUAGE_LABELS[language]}
|
|
120
|
+
style={{
|
|
121
|
+
flexDirection: 'row',
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
gap: 12,
|
|
124
|
+
paddingHorizontal: 16,
|
|
125
|
+
paddingVertical: 13,
|
|
126
|
+
borderTopWidth: i === 0 ? 0 : 1,
|
|
127
|
+
borderTopColor: theme.colors.border,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<Text
|
|
131
|
+
style={{
|
|
132
|
+
flex: 1,
|
|
133
|
+
fontSize: 15,
|
|
134
|
+
fontWeight: active ? '600' : '400',
|
|
135
|
+
color: theme.colors.textPrimary,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{LANGUAGE_LABELS[language]}
|
|
139
|
+
</Text>
|
|
140
|
+
{active && <CheckIcon color={theme.colors.accent} size={16} />}
|
|
141
|
+
</PressableRow>
|
|
142
|
+
);
|
|
143
|
+
})}
|
|
144
|
+
</View>
|
|
145
|
+
</ScrollView>
|
|
146
|
+
|
|
147
|
+
<View
|
|
148
|
+
style={{
|
|
149
|
+
paddingHorizontal: 16,
|
|
150
|
+
paddingTop: 12,
|
|
151
|
+
paddingBottom: 8,
|
|
152
|
+
gap: 8,
|
|
153
|
+
borderTopWidth: 1,
|
|
154
|
+
borderTopColor: theme.colors.border,
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
{error !== null && (
|
|
158
|
+
<Text style={{ fontSize: 12, color: theme.colors.statusDanger }}>
|
|
159
|
+
{error}
|
|
160
|
+
</Text>
|
|
161
|
+
)}
|
|
162
|
+
<PrimaryButton
|
|
163
|
+
label={saving ? strings.t('compose.sending') : strings.t('lang.continue')}
|
|
164
|
+
onPress={onConfirm}
|
|
165
|
+
disabled={saving}
|
|
166
|
+
theme={theme}
|
|
167
|
+
fullWidth
|
|
168
|
+
/>
|
|
169
|
+
</View>
|
|
170
|
+
</View>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TextInput,
|
|
8
8
|
View,
|
|
9
9
|
} from 'react-native';
|
|
10
|
+
import type { Translator } from '../../i18n';
|
|
10
11
|
import type { SupportTheme } from '../../theme';
|
|
11
12
|
import type { Thread, ThreadMessage } from '../../types';
|
|
12
13
|
import { USER_MESSAGE_MAX, USER_MESSAGE_MIN } from '../../types';
|
|
@@ -34,7 +35,11 @@ interface Row {
|
|
|
34
35
|
startsUnread: boolean;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function buildRows(
|
|
38
|
+
function buildRows(
|
|
39
|
+
messages: ThreadMessage[],
|
|
40
|
+
unreadCount: number,
|
|
41
|
+
strings: Translator,
|
|
42
|
+
): Row[] {
|
|
38
43
|
// Unread messages are always the newest admin ones, so the divider goes
|
|
39
44
|
// above the Nth-from-last admin message.
|
|
40
45
|
let remaining = unreadCount;
|
|
@@ -49,16 +54,18 @@ function buildRows(messages: ThreadMessage[], unreadCount: number): Row[] {
|
|
|
49
54
|
return messages.map((message, i) => {
|
|
50
55
|
const previous = i > 0 ? (messages[i - 1] ?? null) : null;
|
|
51
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.
|
|
52
60
|
const withinGroup = (a: ThreadMessage | null, b: ThreadMessage) =>
|
|
53
61
|
a !== null &&
|
|
54
62
|
a.sender === b.sender &&
|
|
55
|
-
a.admin_name === b.admin_name &&
|
|
56
63
|
Math.abs(Date.parse(b.created_at) - Date.parse(a.created_at)) <
|
|
57
64
|
GROUP_WINDOW_MS;
|
|
58
65
|
|
|
59
66
|
const day =
|
|
60
67
|
previous === null || !isSameDay(previous.created_at, message.created_at)
|
|
61
|
-
? dayLabel(message.created_at)
|
|
68
|
+
? dayLabel(message.created_at, strings)
|
|
62
69
|
: null;
|
|
63
70
|
const startsUnread = message.id === firstUnreadId;
|
|
64
71
|
|
|
@@ -127,7 +134,15 @@ function Separator({
|
|
|
127
134
|
);
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
function MessageBubble({
|
|
137
|
+
function MessageBubble({
|
|
138
|
+
row,
|
|
139
|
+
theme,
|
|
140
|
+
strings,
|
|
141
|
+
}: {
|
|
142
|
+
row: Row;
|
|
143
|
+
theme: SupportTheme;
|
|
144
|
+
strings: Translator;
|
|
145
|
+
}) {
|
|
131
146
|
const { message, firstInGroup, lastInGroup } = row;
|
|
132
147
|
const own = message.sender === 'user';
|
|
133
148
|
const round = theme.radii.bubble;
|
|
@@ -145,7 +160,7 @@ function MessageBubble({ row, theme }: { row: Row; theme: SupportTheme }) {
|
|
|
145
160
|
>
|
|
146
161
|
{!own && firstInGroup && (
|
|
147
162
|
<Text style={[textStyles.meta(theme), { paddingHorizontal: 4 }]}>
|
|
148
|
-
{
|
|
163
|
+
{strings.t('thread.support')}
|
|
149
164
|
</Text>
|
|
150
165
|
)}
|
|
151
166
|
<View
|
|
@@ -172,7 +187,11 @@ function MessageBubble({ row, theme }: { row: Row; theme: SupportTheme }) {
|
|
|
172
187
|
</View>
|
|
173
188
|
{lastInGroup && (
|
|
174
189
|
<Text style={[textStyles.meta(theme), { paddingHorizontal: 4 }]}>
|
|
175
|
-
{
|
|
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)}
|
|
176
195
|
</Text>
|
|
177
196
|
)}
|
|
178
197
|
</View>
|
|
@@ -181,11 +200,13 @@ function MessageBubble({ row, theme }: { row: Row; theme: SupportTheme }) {
|
|
|
181
200
|
|
|
182
201
|
export function ThreadScreen({
|
|
183
202
|
theme,
|
|
203
|
+
strings,
|
|
184
204
|
thread,
|
|
185
205
|
onBack,
|
|
186
206
|
onSend,
|
|
187
207
|
}: {
|
|
188
208
|
theme: SupportTheme;
|
|
209
|
+
strings: Translator;
|
|
189
210
|
thread: Thread;
|
|
190
211
|
onBack: () => void;
|
|
191
212
|
/** Resolves null on success, or a user-facing error message. */
|
|
@@ -206,8 +227,8 @@ export function ThreadScreen({
|
|
|
206
227
|
const tooShort = trimmed.length > 0 && trimmed.length < USER_MESSAGE_MIN;
|
|
207
228
|
|
|
208
229
|
const rows = useMemo(
|
|
209
|
-
() => buildRows(thread.messages, unreadOnOpen),
|
|
210
|
-
[thread.messages, unreadOnOpen],
|
|
230
|
+
() => buildRows(thread.messages, unreadOnOpen, strings),
|
|
231
|
+
[thread.messages, unreadOnOpen, strings],
|
|
211
232
|
);
|
|
212
233
|
|
|
213
234
|
useEffect(() => {
|
|
@@ -244,15 +265,15 @@ export function ThreadScreen({
|
|
|
244
265
|
<IconButton
|
|
245
266
|
onPress={onBack}
|
|
246
267
|
theme={theme}
|
|
247
|
-
accessibilityLabel=
|
|
268
|
+
accessibilityLabel={strings.t('compose.back')}
|
|
248
269
|
size={32}
|
|
249
270
|
>
|
|
250
271
|
<Chevron color={theme.colors.textSecondary} direction="left" size={12} />
|
|
251
272
|
</IconButton>
|
|
252
|
-
<TypeChipView type={thread.type} theme={theme} />
|
|
253
|
-
<StatusIndicator status={thread.status} theme={theme} />
|
|
273
|
+
<TypeChipView type={thread.type} theme={theme} strings={strings} />
|
|
274
|
+
<StatusIndicator status={thread.status} theme={theme} strings={strings} />
|
|
254
275
|
<Text style={[textStyles.meta(theme), { marginLeft: 'auto' }]}>
|
|
255
|
-
{relativeTime(thread.created_at)}
|
|
276
|
+
{relativeTime(thread.created_at, strings)}
|
|
256
277
|
</Text>
|
|
257
278
|
</View>
|
|
258
279
|
|
|
@@ -270,18 +291,22 @@ export function ThreadScreen({
|
|
|
270
291
|
<View key={row.message.id}>
|
|
271
292
|
{row.day !== null && <Separator label={row.day} theme={theme} />}
|
|
272
293
|
{row.startsUnread && (
|
|
273
|
-
<Separator
|
|
294
|
+
<Separator
|
|
295
|
+
label={strings.t('thread.new')}
|
|
296
|
+
theme={theme}
|
|
297
|
+
tone="accent"
|
|
298
|
+
/>
|
|
274
299
|
)}
|
|
275
|
-
<MessageBubble row={row} theme={theme} />
|
|
300
|
+
<MessageBubble row={row} theme={theme} strings={strings} />
|
|
276
301
|
</View>
|
|
277
302
|
))}
|
|
278
303
|
|
|
279
304
|
{resolved && thread.resolved_at !== null && (
|
|
280
305
|
<View style={{ marginTop: 16 }}>
|
|
281
306
|
<Separator
|
|
282
|
-
label={
|
|
283
|
-
thread.resolved_at,
|
|
284
|
-
)}
|
|
307
|
+
label={strings.t('thread.resolvedAt', {
|
|
308
|
+
date: formatDate(thread.resolved_at, strings),
|
|
309
|
+
})}
|
|
285
310
|
theme={theme}
|
|
286
311
|
/>
|
|
287
312
|
</View>
|
|
@@ -308,7 +333,7 @@ export function ThreadScreen({
|
|
|
308
333
|
}}
|
|
309
334
|
>
|
|
310
335
|
<Text style={textStyles.secondary(theme)}>
|
|
311
|
-
|
|
336
|
+
{strings.t('thread.readOnly')}
|
|
312
337
|
</Text>
|
|
313
338
|
</View>
|
|
314
339
|
) : (
|
|
@@ -322,7 +347,7 @@ export function ThreadScreen({
|
|
|
322
347
|
}}
|
|
323
348
|
multiline
|
|
324
349
|
editable={!sending}
|
|
325
|
-
placeholder=
|
|
350
|
+
placeholder={strings.t('thread.placeholder')}
|
|
326
351
|
placeholderTextColor={theme.colors.textTertiary}
|
|
327
352
|
style={{
|
|
328
353
|
flex: 1,
|
|
@@ -347,7 +372,9 @@ export function ThreadScreen({
|
|
|
347
372
|
tone="accent"
|
|
348
373
|
size={44}
|
|
349
374
|
disabled={!valid || sending}
|
|
350
|
-
accessibilityLabel={
|
|
375
|
+
accessibilityLabel={strings.t(
|
|
376
|
+
sending ? 'thread.sendingA11y' : 'thread.sendA11y',
|
|
377
|
+
)}
|
|
351
378
|
>
|
|
352
379
|
<ArrowUpIcon color={theme.colors.accentForeground} size={19} />
|
|
353
380
|
</IconButton>
|
|
@@ -371,7 +398,7 @@ export function ThreadScreen({
|
|
|
371
398
|
</Text>
|
|
372
399
|
) : tooShort ? (
|
|
373
400
|
<Text style={textStyles.meta(theme)}>
|
|
374
|
-
|
|
401
|
+
{strings.t('compose.minChars', { count: USER_MESSAGE_MIN })}
|
|
375
402
|
</Text>
|
|
376
403
|
) : null}
|
|
377
404
|
{draft.length >= COUNTER_VISIBLE_FROM && (
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// UI copy, in the user's language.
|
|
2
|
+
//
|
|
3
|
+
// Message bodies are translated by the backend at write time; the sheet's own
|
|
4
|
+
// chrome is not — it is a fixed set of strings that never changes, so it ships
|
|
5
|
+
// as a bundled table. A model call to render the word "Send" would be absurd,
|
|
6
|
+
// and it would also mean the SDK couldn't draw its own UI offline.
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_LANGUAGE, type Language } from './languages';
|
|
9
|
+
import { en, type StringKey, type StringTable } from './strings/en';
|
|
10
|
+
import { de } from './strings/de';
|
|
11
|
+
import { es } from './strings/es';
|
|
12
|
+
import { fr } from './strings/fr';
|
|
13
|
+
import { hi } from './strings/hi';
|
|
14
|
+
import { it } from './strings/it';
|
|
15
|
+
import { ja } from './strings/ja';
|
|
16
|
+
import { ko } from './strings/ko';
|
|
17
|
+
import { nl } from './strings/nl';
|
|
18
|
+
import { pl } from './strings/pl';
|
|
19
|
+
import { pt } from './strings/pt';
|
|
20
|
+
import { ru } from './strings/ru';
|
|
21
|
+
import { tr } from './strings/tr';
|
|
22
|
+
import { zh } from './strings/zh';
|
|
23
|
+
|
|
24
|
+
const TABLES: Record<Language, StringTable> = {
|
|
25
|
+
en,
|
|
26
|
+
es,
|
|
27
|
+
fr,
|
|
28
|
+
de,
|
|
29
|
+
nl,
|
|
30
|
+
pt,
|
|
31
|
+
it,
|
|
32
|
+
pl,
|
|
33
|
+
tr,
|
|
34
|
+
ru,
|
|
35
|
+
ja,
|
|
36
|
+
ko,
|
|
37
|
+
zh,
|
|
38
|
+
hi,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Fills `{name}` placeholders. Unknown tokens are left alone, not blanked. */
|
|
42
|
+
function format(
|
|
43
|
+
template: string,
|
|
44
|
+
vars?: Record<string, string | number>,
|
|
45
|
+
): string {
|
|
46
|
+
if (!vars) return template;
|
|
47
|
+
return template.replace(/\{(\w+)\}/g, (match, key: string) =>
|
|
48
|
+
key in vars ? String(vars[key]) : match,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Translator {
|
|
53
|
+
language: Language;
|
|
54
|
+
/**
|
|
55
|
+
* BCP-47 tag for Intl — dates and times stay in the user's chosen language
|
|
56
|
+
* rather than the device's, so a sheet never mixes the two.
|
|
57
|
+
*/
|
|
58
|
+
locale: string;
|
|
59
|
+
t: (key: StringKey, vars?: Record<string, string | number>) => string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createTranslator(language: Language): Translator {
|
|
63
|
+
const table = TABLES[language] ?? TABLES[DEFAULT_LANGUAGE];
|
|
64
|
+
return {
|
|
65
|
+
language,
|
|
66
|
+
locale: language,
|
|
67
|
+
t: (key, vars) => format(table[key] ?? en[key], vars),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type { StringKey, StringTable };
|
|
72
|
+
export {
|
|
73
|
+
asLanguage,
|
|
74
|
+
baseLanguage,
|
|
75
|
+
DEFAULT_LANGUAGE,
|
|
76
|
+
deviceLanguage,
|
|
77
|
+
isSupportedLanguage,
|
|
78
|
+
LANGUAGE_LABELS,
|
|
79
|
+
SUPPORTED_LANGUAGES,
|
|
80
|
+
type Language,
|
|
81
|
+
} from './languages';
|