@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,12 +1,20 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { Text, View } from 'react-native';
|
|
3
3
|
import {
|
|
4
4
|
postMessage,
|
|
5
5
|
registerPush,
|
|
6
|
+
setLanguage as setLanguageRequest,
|
|
6
7
|
submitQuery,
|
|
7
8
|
SupportApiError,
|
|
8
9
|
type ApiConfig,
|
|
9
10
|
} from '../api';
|
|
11
|
+
import { createTranslator } from '../i18n';
|
|
12
|
+
import {
|
|
13
|
+
asLanguage,
|
|
14
|
+
DEFAULT_LANGUAGE,
|
|
15
|
+
deviceLanguage,
|
|
16
|
+
type Language,
|
|
17
|
+
} from '../i18n/languages';
|
|
10
18
|
import { ensurePushPermission, isPushAvailable } from '../push';
|
|
11
19
|
import { resolveTheme } from '../theme';
|
|
12
20
|
import type { QueryType, SupportButtonProps, Thread } from '../types';
|
|
@@ -16,10 +24,12 @@ import { BottomSheet } from './BottomSheet';
|
|
|
16
24
|
import { LauncherButton } from './LauncherButton';
|
|
17
25
|
import { ComposeScreen } from './screens/ComposeScreen';
|
|
18
26
|
import { HomeScreen } from './screens/HomeScreen';
|
|
27
|
+
import { LanguageScreen } from './screens/LanguageScreen';
|
|
19
28
|
import { ThreadScreen } from './screens/ThreadScreen';
|
|
20
29
|
|
|
21
30
|
type Screen =
|
|
22
31
|
| { name: 'home' }
|
|
32
|
+
| { name: 'language' }
|
|
23
33
|
| { name: 'compose'; type: QueryType }
|
|
24
34
|
| { name: 'thread'; threadId: string };
|
|
25
35
|
|
|
@@ -47,15 +57,56 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
47
57
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
48
58
|
const [screen, setScreen] = useState<Screen>({ name: 'home' });
|
|
49
59
|
|
|
50
|
-
const {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
const {
|
|
61
|
+
threads,
|
|
62
|
+
totalUnread,
|
|
63
|
+
loading,
|
|
64
|
+
refreshing,
|
|
65
|
+
errorKey,
|
|
66
|
+
serverLanguage,
|
|
67
|
+
languageLoaded,
|
|
68
|
+
refresh,
|
|
69
|
+
reload,
|
|
70
|
+
} = useSupportThreads({
|
|
71
|
+
config,
|
|
72
|
+
revenuecatId: props.user.revenuecatId,
|
|
73
|
+
enabled: validationError === null,
|
|
74
|
+
sheetOpen,
|
|
75
|
+
activeThreadId: screen.name === 'thread' ? screen.threadId : null,
|
|
76
|
+
idlePollMs: props.pollIntervalMs ?? IDLE_POLL_MS,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What the user picked, once they have picked. Until then the sheet is
|
|
81
|
+
* rendered in the host app's hint or the device's language, which is a
|
|
82
|
+
* guess — hence `chosen`, which is what decides whether to ask.
|
|
83
|
+
*/
|
|
84
|
+
const [chosen, setChosen] = useState<Language | null>(null);
|
|
85
|
+
const [draftLanguage, setDraftLanguage] = useState<Language | null>(null);
|
|
86
|
+
const [savingLanguage, setSavingLanguage] = useState(false);
|
|
87
|
+
const [languageError, setLanguageError] = useState<string | null>(null);
|
|
88
|
+
|
|
89
|
+
// A preference saved on another device (or an earlier install) wins over
|
|
90
|
+
// any local guess, and answers the "have they chosen?" question for free.
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const stored = asLanguage(serverLanguage);
|
|
93
|
+
if (stored !== null) setChosen(stored);
|
|
94
|
+
}, [serverLanguage]);
|
|
95
|
+
|
|
96
|
+
const fallbackLanguage =
|
|
97
|
+
props.defaultLanguage ?? deviceLanguage() ?? DEFAULT_LANGUAGE;
|
|
98
|
+
const language = chosen ?? fallbackLanguage;
|
|
99
|
+
const strings = useMemo(
|
|
100
|
+
() => createTranslator(draftLanguage ?? language),
|
|
101
|
+
[draftLanguage, language],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Ask once, on first open — but only once the server has told us whether
|
|
106
|
+
* this user already answered. Asking before then would show the picker to
|
|
107
|
+
* someone who chose months ago on another device.
|
|
108
|
+
*/
|
|
109
|
+
const mustChooseLanguage = languageLoaded && chosen === null;
|
|
59
110
|
|
|
60
111
|
/**
|
|
61
112
|
* Asks for notification permission the moment the sheet opens, then
|
|
@@ -89,6 +140,8 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
89
140
|
function closeSheet() {
|
|
90
141
|
setSheetOpen(false);
|
|
91
142
|
setScreen({ name: 'home' });
|
|
143
|
+
setDraftLanguage(null);
|
|
144
|
+
setLanguageError(null);
|
|
92
145
|
}
|
|
93
146
|
|
|
94
147
|
if (validationError !== null) {
|
|
@@ -116,10 +169,43 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
116
169
|
|
|
117
170
|
function writeErrorMessage(e: unknown): string {
|
|
118
171
|
if (e instanceof SupportApiError) {
|
|
119
|
-
if (e.status === 429) return '
|
|
120
|
-
if (e.offline) return
|
|
172
|
+
if (e.status === 429) return strings.t('error.rateLimited');
|
|
173
|
+
if (e.offline) return strings.t('error.offline');
|
|
174
|
+
}
|
|
175
|
+
return strings.t('error.sendFailed');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Persists the picked language and leaves the picker.
|
|
180
|
+
*
|
|
181
|
+
* A failed save keeps the user on the screen with a retry: getting this
|
|
182
|
+
* wrong means every message they write is translated from the wrong
|
|
183
|
+
* language, which is worse than one more tap.
|
|
184
|
+
*/
|
|
185
|
+
async function confirmLanguage() {
|
|
186
|
+
const next = draftLanguage ?? language;
|
|
187
|
+
setSavingLanguage(true);
|
|
188
|
+
setLanguageError(null);
|
|
189
|
+
try {
|
|
190
|
+
await setLanguageRequest(config, {
|
|
191
|
+
revenuecatId: props.user.revenuecatId,
|
|
192
|
+
language: next,
|
|
193
|
+
});
|
|
194
|
+
setChosen(next);
|
|
195
|
+
setDraftLanguage(null);
|
|
196
|
+
setScreen({ name: 'home' });
|
|
197
|
+
// On a change, the server re-queues existing replies for translation
|
|
198
|
+
// into the new language; they arrive over the next poll or two.
|
|
199
|
+
void reload();
|
|
200
|
+
} catch (e) {
|
|
201
|
+
setLanguageError(
|
|
202
|
+
e instanceof SupportApiError && e.offline
|
|
203
|
+
? strings.t('error.offline')
|
|
204
|
+
: strings.t('lang.saveFailed'),
|
|
205
|
+
);
|
|
206
|
+
} finally {
|
|
207
|
+
setSavingLanguage(false);
|
|
121
208
|
}
|
|
122
|
-
return "Couldn't send — try again";
|
|
123
209
|
}
|
|
124
210
|
|
|
125
211
|
async function handleSubmit(
|
|
@@ -134,6 +220,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
134
220
|
isPaid: props.user.isPaid,
|
|
135
221
|
revenuecatId: props.user.revenuecatId,
|
|
136
222
|
appVersion: props.user.appVersion,
|
|
223
|
+
language,
|
|
137
224
|
});
|
|
138
225
|
void reload();
|
|
139
226
|
return null;
|
|
@@ -151,6 +238,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
151
238
|
queryId: threadId,
|
|
152
239
|
revenuecatId: props.user.revenuecatId,
|
|
153
240
|
message,
|
|
241
|
+
language,
|
|
154
242
|
});
|
|
155
243
|
await reload();
|
|
156
244
|
return null;
|
|
@@ -167,19 +255,50 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
167
255
|
const home = (
|
|
168
256
|
<HomeScreen
|
|
169
257
|
theme={theme}
|
|
258
|
+
strings={strings}
|
|
259
|
+
language={language}
|
|
170
260
|
threads={threads}
|
|
171
261
|
loading={loading}
|
|
172
262
|
refreshing={refreshing}
|
|
173
|
-
error={
|
|
263
|
+
error={errorKey === null ? null : strings.t(errorKey)}
|
|
174
264
|
onRefresh={refresh}
|
|
175
265
|
onCompose={(type) => setScreen({ name: 'compose', type })}
|
|
176
266
|
onOpenThread={(thread) =>
|
|
177
267
|
setScreen({ name: 'thread', threadId: thread.id })
|
|
178
268
|
}
|
|
269
|
+
onChangeLanguage={() => {
|
|
270
|
+
setDraftLanguage(language);
|
|
271
|
+
setLanguageError(null);
|
|
272
|
+
setScreen({ name: 'language' });
|
|
273
|
+
}}
|
|
179
274
|
onClose={closeSheet}
|
|
180
275
|
/>
|
|
181
276
|
);
|
|
182
277
|
|
|
278
|
+
const languagePicker = (
|
|
279
|
+
<LanguageScreen
|
|
280
|
+
theme={theme}
|
|
281
|
+
strings={strings}
|
|
282
|
+
selected={draftLanguage ?? language}
|
|
283
|
+
onSelect={setDraftLanguage}
|
|
284
|
+
onConfirm={() => void confirmLanguage()}
|
|
285
|
+
// No way back on first run: there is nothing behind it yet, so the
|
|
286
|
+
// only exit is choosing — or closing the sheet, which asks again.
|
|
287
|
+
onBack={
|
|
288
|
+
mustChooseLanguage
|
|
289
|
+
? undefined
|
|
290
|
+
: () => {
|
|
291
|
+
setDraftLanguage(null);
|
|
292
|
+
setLanguageError(null);
|
|
293
|
+
setScreen({ name: 'home' });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
onDismiss={mustChooseLanguage ? closeSheet : undefined}
|
|
297
|
+
saving={savingLanguage}
|
|
298
|
+
error={languageError}
|
|
299
|
+
/>
|
|
300
|
+
);
|
|
301
|
+
|
|
183
302
|
return (
|
|
184
303
|
<>
|
|
185
304
|
<LauncherButton
|
|
@@ -193,26 +312,36 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
193
312
|
/>
|
|
194
313
|
|
|
195
314
|
<BottomSheet visible={sheetOpen} onClose={closeSheet} theme={theme}>
|
|
196
|
-
{screen
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
315
|
+
{/* The one-time prompt outranks whatever screen the user is on: it
|
|
316
|
+
only ever fires before they have sent anything. */}
|
|
317
|
+
{mustChooseLanguage || screen.name === 'language' ? (
|
|
318
|
+
languagePicker
|
|
319
|
+
) : (
|
|
320
|
+
<>
|
|
321
|
+
{screen.name === 'home' && home}
|
|
322
|
+
{screen.name === 'compose' && (
|
|
323
|
+
<ComposeScreen
|
|
324
|
+
theme={theme}
|
|
325
|
+
strings={strings}
|
|
326
|
+
type={screen.type}
|
|
327
|
+
onBack={() => setScreen({ name: 'home' })}
|
|
328
|
+
onSubmit={(message) => handleSubmit(screen.type, message)}
|
|
329
|
+
/>
|
|
330
|
+
)}
|
|
331
|
+
{screen.name === 'thread' &&
|
|
332
|
+
(activeThread !== null ? (
|
|
333
|
+
<ThreadScreen
|
|
334
|
+
theme={theme}
|
|
335
|
+
strings={strings}
|
|
336
|
+
thread={activeThread}
|
|
337
|
+
onBack={() => setScreen({ name: 'home' })}
|
|
338
|
+
onSend={(message) => handleSend(activeThread.id, message)}
|
|
339
|
+
/>
|
|
340
|
+
) : (
|
|
341
|
+
home
|
|
342
|
+
))}
|
|
343
|
+
</>
|
|
204
344
|
)}
|
|
205
|
-
{screen.name === 'thread' &&
|
|
206
|
-
(activeThread !== null ? (
|
|
207
|
-
<ThreadScreen
|
|
208
|
-
theme={theme}
|
|
209
|
-
thread={activeThread}
|
|
210
|
-
onBack={() => setScreen({ name: 'home' })}
|
|
211
|
-
onSend={(message) => handleSend(activeThread.id, message)}
|
|
212
|
-
/>
|
|
213
|
-
) : (
|
|
214
|
-
home
|
|
215
|
-
))}
|
|
216
345
|
</BottomSheet>
|
|
217
346
|
</>
|
|
218
347
|
);
|
package/src/components/icons.tsx
CHANGED
|
@@ -145,6 +145,42 @@ export function CheckIcon({ color, size = 16 }: { color: string; size?: number }
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** Globe: circle, equator, and a pill standing in for the meridian curve. */
|
|
149
|
+
export function GlobeIcon({ color, size = 16 }: { color: string; size?: number }) {
|
|
150
|
+
const inner = size - STROKE * 2;
|
|
151
|
+
return (
|
|
152
|
+
<View
|
|
153
|
+
style={{
|
|
154
|
+
width: size,
|
|
155
|
+
height: size,
|
|
156
|
+
borderWidth: STROKE,
|
|
157
|
+
borderColor: color,
|
|
158
|
+
borderRadius: size / 2,
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
justifyContent: 'center',
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<View
|
|
164
|
+
style={{
|
|
165
|
+
position: 'absolute',
|
|
166
|
+
width: inner,
|
|
167
|
+
height: STROKE,
|
|
168
|
+
backgroundColor: color,
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
<View
|
|
172
|
+
style={{
|
|
173
|
+
width: size * 0.52,
|
|
174
|
+
height: inner,
|
|
175
|
+
borderWidth: STROKE,
|
|
176
|
+
borderColor: color,
|
|
177
|
+
borderRadius: size * 0.26,
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
</View>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
148
184
|
/** Send arrow: a stem plus a chevron head, both centered on the same axis. */
|
|
149
185
|
export function ArrowUpIcon({ color, size = 16 }: { color: string; size?: number }) {
|
|
150
186
|
const stroke = STROKE + 0.4;
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
type TextStyle,
|
|
10
10
|
type ViewStyle,
|
|
11
11
|
} from 'react-native';
|
|
12
|
+
import type { Translator } from '../i18n';
|
|
12
13
|
import type { SupportTheme } from '../theme';
|
|
13
14
|
import type { QueryStatus, QueryType } from '../types';
|
|
14
|
-
import { QUERY_TYPE_LABEL } from '../types';
|
|
15
15
|
import { AlertCircleIcon, ChatIcon, PlusCircleIcon } from './icons';
|
|
16
16
|
|
|
17
17
|
export function typeIcon(type: QueryType, color: string, size = 15): ReactNode {
|
|
@@ -62,9 +62,11 @@ export function TypeGlyph({
|
|
|
62
62
|
export function StatusIndicator({
|
|
63
63
|
status,
|
|
64
64
|
theme,
|
|
65
|
+
strings,
|
|
65
66
|
}: {
|
|
66
67
|
status: QueryStatus;
|
|
67
68
|
theme: SupportTheme;
|
|
69
|
+
strings: Translator;
|
|
68
70
|
}) {
|
|
69
71
|
const color =
|
|
70
72
|
status === 'open' ? theme.colors.statusOpen : theme.colors.statusResolved;
|
|
@@ -74,7 +76,7 @@ export function StatusIndicator({
|
|
|
74
76
|
style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: color }}
|
|
75
77
|
/>
|
|
76
78
|
<Text style={{ fontSize: 12, color }}>
|
|
77
|
-
{status === 'open' ? '
|
|
79
|
+
{strings.t(status === 'open' ? 'status.open' : 'status.resolved')}
|
|
78
80
|
</Text>
|
|
79
81
|
</View>
|
|
80
82
|
);
|
|
@@ -351,17 +353,23 @@ export function ErrorState({
|
|
|
351
353
|
message,
|
|
352
354
|
onRetry,
|
|
353
355
|
theme,
|
|
356
|
+
strings,
|
|
354
357
|
}: {
|
|
355
358
|
message: string;
|
|
356
359
|
onRetry: () => void;
|
|
357
360
|
theme: SupportTheme;
|
|
361
|
+
strings: Translator;
|
|
358
362
|
}) {
|
|
359
363
|
return (
|
|
360
364
|
<View style={{ alignItems: 'center', paddingVertical: 40, gap: 12 }}>
|
|
361
365
|
<Text style={{ fontSize: 14, color: theme.colors.textSecondary }}>
|
|
362
366
|
{message}
|
|
363
367
|
</Text>
|
|
364
|
-
<SecondaryButton
|
|
368
|
+
<SecondaryButton
|
|
369
|
+
label={strings.t('error.tryAgain')}
|
|
370
|
+
onPress={onRetry}
|
|
371
|
+
theme={theme}
|
|
372
|
+
/>
|
|
365
373
|
</View>
|
|
366
374
|
);
|
|
367
375
|
}
|
|
@@ -452,9 +460,11 @@ export const textStyles = {
|
|
|
452
460
|
export function TypeChipView({
|
|
453
461
|
type,
|
|
454
462
|
theme,
|
|
463
|
+
strings,
|
|
455
464
|
}: {
|
|
456
465
|
type: QueryType;
|
|
457
466
|
theme: SupportTheme;
|
|
467
|
+
strings: Translator;
|
|
458
468
|
}) {
|
|
459
469
|
const color = typeColor(type, theme);
|
|
460
470
|
return (
|
|
@@ -472,7 +482,7 @@ export function TypeChipView({
|
|
|
472
482
|
>
|
|
473
483
|
{typeIcon(type, color, 11)}
|
|
474
484
|
<Text style={{ fontSize: 11.5, fontWeight: '600', color }}>
|
|
475
|
-
{
|
|
485
|
+
{strings.t(`type.${type}`)}
|
|
476
486
|
</Text>
|
|
477
487
|
</View>
|
|
478
488
|
);
|
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
TextInput,
|
|
9
9
|
View,
|
|
10
10
|
} from 'react-native';
|
|
11
|
+
import type { Translator } from '../../i18n';
|
|
11
12
|
import type { SupportTheme } from '../../theme';
|
|
12
13
|
import type { QueryType } from '../../types';
|
|
13
|
-
import {
|
|
14
|
+
import { USER_MESSAGE_MAX, USER_MESSAGE_MIN } from '../../types';
|
|
14
15
|
import { CheckIcon, Chevron } from '../icons';
|
|
15
16
|
import {
|
|
16
17
|
IconButton,
|
|
@@ -19,18 +20,6 @@ import {
|
|
|
19
20
|
TypeGlyph,
|
|
20
21
|
} from '../primitives';
|
|
21
22
|
|
|
22
|
-
const PROMPT: Record<QueryType, string> = {
|
|
23
|
-
feedback: 'What should we know?',
|
|
24
|
-
feature_request: 'What would you like to see?',
|
|
25
|
-
bug_report: 'What happened, and what did you expect?',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const SUBTITLE: Record<QueryType, string> = {
|
|
29
|
-
feedback: 'Good or bad — it goes straight to the team.',
|
|
30
|
-
feature_request: 'Tell us what you are trying to do.',
|
|
31
|
-
bug_report: 'Steps, screen, and what you expected all help.',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
23
|
/** Scales in once on success — the only celebratory motion in the SDK. */
|
|
35
24
|
function SentMark({ theme }: { theme: SupportTheme }) {
|
|
36
25
|
const scale = useRef(new Animated.Value(0.6)).current;
|
|
@@ -75,11 +64,13 @@ function SentMark({ theme }: { theme: SupportTheme }) {
|
|
|
75
64
|
|
|
76
65
|
export function ComposeScreen({
|
|
77
66
|
theme,
|
|
67
|
+
strings,
|
|
78
68
|
type,
|
|
79
69
|
onBack,
|
|
80
70
|
onSubmit,
|
|
81
71
|
}: {
|
|
82
72
|
theme: SupportTheme;
|
|
73
|
+
strings: Translator;
|
|
83
74
|
type: QueryType;
|
|
84
75
|
onBack: () => void;
|
|
85
76
|
/** Resolves null on success, or a user-facing error message. */
|
|
@@ -119,13 +110,16 @@ export function ComposeScreen({
|
|
|
119
110
|
}}
|
|
120
111
|
>
|
|
121
112
|
<SentMark theme={theme} />
|
|
122
|
-
<Text style={textStyles.title(theme)}>
|
|
113
|
+
<Text style={textStyles.title(theme)}>{strings.t('compose.sent')}</Text>
|
|
123
114
|
<Text style={[textStyles.secondary(theme), { textAlign: 'center' }]}>
|
|
124
|
-
|
|
125
|
-
Open, and the button shows a badge when we answer.
|
|
115
|
+
{strings.t('compose.sentHint')}
|
|
126
116
|
</Text>
|
|
127
117
|
<View style={{ marginTop: 6 }}>
|
|
128
|
-
<PrimaryButton
|
|
118
|
+
<PrimaryButton
|
|
119
|
+
label={strings.t('compose.done')}
|
|
120
|
+
onPress={onBack}
|
|
121
|
+
theme={theme}
|
|
122
|
+
/>
|
|
129
123
|
</View>
|
|
130
124
|
</View>
|
|
131
125
|
);
|
|
@@ -149,15 +143,17 @@ export function ComposeScreen({
|
|
|
149
143
|
<IconButton
|
|
150
144
|
onPress={onBack}
|
|
151
145
|
theme={theme}
|
|
152
|
-
accessibilityLabel=
|
|
146
|
+
accessibilityLabel={strings.t('compose.back')}
|
|
153
147
|
size={32}
|
|
154
148
|
>
|
|
155
149
|
<Chevron color={theme.colors.textSecondary} direction="left" size={12} />
|
|
156
150
|
</IconButton>
|
|
157
151
|
<TypeGlyph type={type} theme={theme} size={32} />
|
|
158
152
|
<View style={{ flex: 1, gap: 1 }}>
|
|
159
|
-
<Text style={textStyles.heading(theme)}>
|
|
160
|
-
|
|
153
|
+
<Text style={textStyles.heading(theme)}>
|
|
154
|
+
{strings.t(`type.${type}`)}
|
|
155
|
+
</Text>
|
|
156
|
+
<Text style={textStyles.meta(theme)}>{strings.t(`sub.${type}`)}</Text>
|
|
161
157
|
</View>
|
|
162
158
|
</View>
|
|
163
159
|
|
|
@@ -171,7 +167,7 @@ export function ComposeScreen({
|
|
|
171
167
|
multiline
|
|
172
168
|
autoFocus
|
|
173
169
|
editable={!submitting}
|
|
174
|
-
placeholder={
|
|
170
|
+
placeholder={strings.t(`prompt.${type}`)}
|
|
175
171
|
placeholderTextColor={theme.colors.textTertiary}
|
|
176
172
|
style={{
|
|
177
173
|
minHeight: 140,
|
|
@@ -204,7 +200,7 @@ export function ComposeScreen({
|
|
|
204
200
|
</Text>
|
|
205
201
|
) : tooShort ? (
|
|
206
202
|
<Text style={textStyles.meta(theme)}>
|
|
207
|
-
|
|
203
|
+
{strings.t('compose.minChars', { count: USER_MESSAGE_MIN })}
|
|
208
204
|
</Text>
|
|
209
205
|
) : null}
|
|
210
206
|
<Text
|
|
@@ -220,7 +216,7 @@ export function ComposeScreen({
|
|
|
220
216
|
|
|
221
217
|
<View style={{ paddingTop: 12 }}>
|
|
222
218
|
<PrimaryButton
|
|
223
|
-
label={submitting ? '
|
|
219
|
+
label={strings.t(submitting ? 'compose.sending' : 'compose.send')}
|
|
224
220
|
onPress={handleSubmit}
|
|
225
221
|
disabled={!valid || submitting}
|
|
226
222
|
theme={theme}
|