@appsupportkit/support-sdk 0.1.2 → 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.
Files changed (224) hide show
  1. package/lib/commonjs/api.js +17 -2
  2. package/lib/commonjs/api.js.map +1 -1
  3. package/lib/commonjs/components/BottomSheet.js +67 -47
  4. package/lib/commonjs/components/BottomSheet.js.map +1 -1
  5. package/lib/commonjs/components/LauncherButton.js +177 -0
  6. package/lib/commonjs/components/LauncherButton.js.map +1 -0
  7. package/lib/commonjs/components/SupportButton.js +168 -190
  8. package/lib/commonjs/components/SupportButton.js.map +1 -1
  9. package/lib/commonjs/components/icons.js +110 -23
  10. package/lib/commonjs/components/icons.js.map +1 -1
  11. package/lib/commonjs/components/primitives.js +191 -44
  12. package/lib/commonjs/components/primitives.js.map +1 -1
  13. package/lib/commonjs/components/screens/ComposeScreen.js +108 -60
  14. package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
  15. package/lib/commonjs/components/screens/HomeScreen.js +221 -118
  16. package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
  17. package/lib/commonjs/components/screens/LanguageScreen.js +150 -0
  18. package/lib/commonjs/components/screens/LanguageScreen.js.map +1 -0
  19. package/lib/commonjs/components/screens/ThreadScreen.js +237 -111
  20. package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
  21. package/lib/commonjs/i18n/index.js +101 -0
  22. package/lib/commonjs/i18n/index.js.map +1 -0
  23. package/lib/commonjs/i18n/languages.js +95 -0
  24. package/lib/commonjs/i18n/languages.js.map +1 -0
  25. package/lib/commonjs/i18n/strings/de.js +72 -0
  26. package/lib/commonjs/i18n/strings/de.js.map +1 -0
  27. package/lib/commonjs/i18n/strings/en.js +88 -0
  28. package/lib/commonjs/i18n/strings/en.js.map +1 -0
  29. package/lib/commonjs/i18n/strings/es.js +72 -0
  30. package/lib/commonjs/i18n/strings/es.js.map +1 -0
  31. package/lib/commonjs/i18n/strings/fr.js +72 -0
  32. package/lib/commonjs/i18n/strings/fr.js.map +1 -0
  33. package/lib/commonjs/i18n/strings/hi.js +72 -0
  34. package/lib/commonjs/i18n/strings/hi.js.map +1 -0
  35. package/lib/commonjs/i18n/strings/it.js +72 -0
  36. package/lib/commonjs/i18n/strings/it.js.map +1 -0
  37. package/lib/commonjs/i18n/strings/ja.js +72 -0
  38. package/lib/commonjs/i18n/strings/ja.js.map +1 -0
  39. package/lib/commonjs/i18n/strings/ko.js +72 -0
  40. package/lib/commonjs/i18n/strings/ko.js.map +1 -0
  41. package/lib/commonjs/i18n/strings/nl.js +72 -0
  42. package/lib/commonjs/i18n/strings/nl.js.map +1 -0
  43. package/lib/commonjs/i18n/strings/pl.js +72 -0
  44. package/lib/commonjs/i18n/strings/pl.js.map +1 -0
  45. package/lib/commonjs/i18n/strings/pt.js +72 -0
  46. package/lib/commonjs/i18n/strings/pt.js.map +1 -0
  47. package/lib/commonjs/i18n/strings/ru.js +72 -0
  48. package/lib/commonjs/i18n/strings/ru.js.map +1 -0
  49. package/lib/commonjs/i18n/strings/tr.js +72 -0
  50. package/lib/commonjs/i18n/strings/tr.js.map +1 -0
  51. package/lib/commonjs/i18n/strings/zh.js +72 -0
  52. package/lib/commonjs/i18n/strings/zh.js.map +1 -0
  53. package/lib/commonjs/index.js +31 -0
  54. package/lib/commonjs/index.js.map +1 -1
  55. package/lib/commonjs/theme.js +136 -17
  56. package/lib/commonjs/theme.js.map +1 -1
  57. package/lib/commonjs/types.js +18 -0
  58. package/lib/commonjs/types.js.map +1 -1
  59. package/lib/commonjs/unread.js +92 -0
  60. package/lib/commonjs/unread.js.map +1 -0
  61. package/lib/commonjs/useSupportThreads.js +296 -0
  62. package/lib/commonjs/useSupportThreads.js.map +1 -0
  63. package/lib/commonjs/utils.js +58 -10
  64. package/lib/commonjs/utils.js.map +1 -1
  65. package/lib/module/api.js +16 -2
  66. package/lib/module/api.js.map +1 -1
  67. package/lib/module/components/BottomSheet.js +68 -48
  68. package/lib/module/components/BottomSheet.js.map +1 -1
  69. package/lib/module/components/LauncherButton.js +172 -0
  70. package/lib/module/components/LauncherButton.js.map +1 -0
  71. package/lib/module/components/SupportButton.js +172 -194
  72. package/lib/module/components/SupportButton.js.map +1 -1
  73. package/lib/module/components/icons.js +107 -22
  74. package/lib/module/components/icons.js.map +1 -1
  75. package/lib/module/components/primitives.js +187 -45
  76. package/lib/module/components/primitives.js.map +1 -1
  77. package/lib/module/components/screens/ComposeScreen.js +114 -64
  78. package/lib/module/components/screens/ComposeScreen.js.map +1 -1
  79. package/lib/module/components/screens/HomeScreen.js +224 -121
  80. package/lib/module/components/screens/HomeScreen.js.map +1 -1
  81. package/lib/module/components/screens/LanguageScreen.js +147 -0
  82. package/lib/module/components/screens/LanguageScreen.js.map +1 -0
  83. package/lib/module/components/screens/ThreadScreen.js +243 -115
  84. package/lib/module/components/screens/ThreadScreen.js.map +1 -1
  85. package/lib/module/i18n/index.js +56 -0
  86. package/lib/module/i18n/index.js.map +1 -0
  87. package/lib/module/i18n/languages.js +88 -0
  88. package/lib/module/i18n/languages.js.map +1 -0
  89. package/lib/module/i18n/strings/de.js +68 -0
  90. package/lib/module/i18n/strings/de.js.map +1 -0
  91. package/lib/module/i18n/strings/en.js +84 -0
  92. package/lib/module/i18n/strings/en.js.map +1 -0
  93. package/lib/module/i18n/strings/es.js +68 -0
  94. package/lib/module/i18n/strings/es.js.map +1 -0
  95. package/lib/module/i18n/strings/fr.js +68 -0
  96. package/lib/module/i18n/strings/fr.js.map +1 -0
  97. package/lib/module/i18n/strings/hi.js +68 -0
  98. package/lib/module/i18n/strings/hi.js.map +1 -0
  99. package/lib/module/i18n/strings/it.js +68 -0
  100. package/lib/module/i18n/strings/it.js.map +1 -0
  101. package/lib/module/i18n/strings/ja.js +68 -0
  102. package/lib/module/i18n/strings/ja.js.map +1 -0
  103. package/lib/module/i18n/strings/ko.js +68 -0
  104. package/lib/module/i18n/strings/ko.js.map +1 -0
  105. package/lib/module/i18n/strings/nl.js +68 -0
  106. package/lib/module/i18n/strings/nl.js.map +1 -0
  107. package/lib/module/i18n/strings/pl.js +68 -0
  108. package/lib/module/i18n/strings/pl.js.map +1 -0
  109. package/lib/module/i18n/strings/pt.js +68 -0
  110. package/lib/module/i18n/strings/pt.js.map +1 -0
  111. package/lib/module/i18n/strings/ru.js +68 -0
  112. package/lib/module/i18n/strings/ru.js.map +1 -0
  113. package/lib/module/i18n/strings/tr.js +68 -0
  114. package/lib/module/i18n/strings/tr.js.map +1 -0
  115. package/lib/module/i18n/strings/zh.js +68 -0
  116. package/lib/module/i18n/strings/zh.js.map +1 -0
  117. package/lib/module/index.js +1 -0
  118. package/lib/module/index.js.map +1 -1
  119. package/lib/module/theme.js +135 -16
  120. package/lib/module/theme.js.map +1 -1
  121. package/lib/module/types.js +18 -0
  122. package/lib/module/types.js.map +1 -1
  123. package/lib/module/unread.js +83 -0
  124. package/lib/module/unread.js.map +1 -0
  125. package/lib/module/useSupportThreads.js +291 -0
  126. package/lib/module/useSupportThreads.js.map +1 -0
  127. package/lib/module/utils.js +55 -10
  128. package/lib/module/utils.js.map +1 -1
  129. package/lib/typescript/api.d.ts +9 -0
  130. package/lib/typescript/api.d.ts.map +1 -1
  131. package/lib/typescript/components/BottomSheet.d.ts.map +1 -1
  132. package/lib/typescript/components/LauncherButton.d.ts +14 -0
  133. package/lib/typescript/components/LauncherButton.d.ts.map +1 -0
  134. package/lib/typescript/components/SupportButton.d.ts +3 -3
  135. package/lib/typescript/components/SupportButton.d.ts.map +1 -1
  136. package/lib/typescript/components/icons.d.ts +18 -2
  137. package/lib/typescript/components/icons.d.ts.map +1 -1
  138. package/lib/typescript/components/primitives.d.ts +42 -7
  139. package/lib/typescript/components/primitives.d.ts.map +1 -1
  140. package/lib/typescript/components/screens/ComposeScreen.d.ts +3 -1
  141. package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
  142. package/lib/typescript/components/screens/HomeScreen.d.ts +8 -2
  143. package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
  144. package/lib/typescript/components/screens/LanguageScreen.d.ts +30 -0
  145. package/lib/typescript/components/screens/LanguageScreen.d.ts.map +1 -0
  146. package/lib/typescript/components/screens/ThreadScreen.d.ts +3 -1
  147. package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
  148. package/lib/typescript/i18n/index.d.ts +15 -0
  149. package/lib/typescript/i18n/index.d.ts.map +1 -0
  150. package/lib/typescript/i18n/languages.d.ts +36 -0
  151. package/lib/typescript/i18n/languages.d.ts.map +1 -0
  152. package/lib/typescript/i18n/strings/de.d.ts +3 -0
  153. package/lib/typescript/i18n/strings/de.d.ts.map +1 -0
  154. package/lib/typescript/i18n/strings/en.d.ts +83 -0
  155. package/lib/typescript/i18n/strings/en.d.ts.map +1 -0
  156. package/lib/typescript/i18n/strings/es.d.ts +3 -0
  157. package/lib/typescript/i18n/strings/es.d.ts.map +1 -0
  158. package/lib/typescript/i18n/strings/fr.d.ts +3 -0
  159. package/lib/typescript/i18n/strings/fr.d.ts.map +1 -0
  160. package/lib/typescript/i18n/strings/hi.d.ts +3 -0
  161. package/lib/typescript/i18n/strings/hi.d.ts.map +1 -0
  162. package/lib/typescript/i18n/strings/it.d.ts +3 -0
  163. package/lib/typescript/i18n/strings/it.d.ts.map +1 -0
  164. package/lib/typescript/i18n/strings/ja.d.ts +3 -0
  165. package/lib/typescript/i18n/strings/ja.d.ts.map +1 -0
  166. package/lib/typescript/i18n/strings/ko.d.ts +3 -0
  167. package/lib/typescript/i18n/strings/ko.d.ts.map +1 -0
  168. package/lib/typescript/i18n/strings/nl.d.ts +3 -0
  169. package/lib/typescript/i18n/strings/nl.d.ts.map +1 -0
  170. package/lib/typescript/i18n/strings/pl.d.ts +3 -0
  171. package/lib/typescript/i18n/strings/pl.d.ts.map +1 -0
  172. package/lib/typescript/i18n/strings/pt.d.ts +3 -0
  173. package/lib/typescript/i18n/strings/pt.d.ts.map +1 -0
  174. package/lib/typescript/i18n/strings/ru.d.ts +3 -0
  175. package/lib/typescript/i18n/strings/ru.d.ts.map +1 -0
  176. package/lib/typescript/i18n/strings/tr.d.ts +3 -0
  177. package/lib/typescript/i18n/strings/tr.d.ts.map +1 -0
  178. package/lib/typescript/i18n/strings/zh.d.ts +3 -0
  179. package/lib/typescript/i18n/strings/zh.d.ts.map +1 -0
  180. package/lib/typescript/index.d.ts +2 -1
  181. package/lib/typescript/index.d.ts.map +1 -1
  182. package/lib/typescript/theme.d.ts +44 -3
  183. package/lib/typescript/theme.d.ts.map +1 -1
  184. package/lib/typescript/types.d.ts +54 -2
  185. package/lib/typescript/types.d.ts.map +1 -1
  186. package/lib/typescript/unread.d.ts +22 -0
  187. package/lib/typescript/unread.d.ts.map +1 -0
  188. package/lib/typescript/useSupportThreads.d.ts +38 -0
  189. package/lib/typescript/useSupportThreads.d.ts.map +1 -0
  190. package/lib/typescript/utils.d.ts +8 -2
  191. package/lib/typescript/utils.d.ts.map +1 -1
  192. package/package.json +3 -3
  193. package/src/api.ts +24 -1
  194. package/src/components/BottomSheet.tsx +61 -43
  195. package/src/components/LauncherButton.tsx +217 -0
  196. package/src/components/SupportButton.tsx +195 -210
  197. package/src/components/icons.tsx +113 -19
  198. package/src/components/primitives.tsx +230 -39
  199. package/src/components/screens/ComposeScreen.tsx +129 -63
  200. package/src/components/screens/HomeScreen.tsx +230 -120
  201. package/src/components/screens/LanguageScreen.tsx +172 -0
  202. package/src/components/screens/ThreadScreen.tsx +319 -122
  203. package/src/i18n/index.ts +81 -0
  204. package/src/i18n/languages.ts +106 -0
  205. package/src/i18n/strings/de.ts +78 -0
  206. package/src/i18n/strings/en.ts +94 -0
  207. package/src/i18n/strings/es.ts +77 -0
  208. package/src/i18n/strings/fr.ts +78 -0
  209. package/src/i18n/strings/hi.ts +77 -0
  210. package/src/i18n/strings/it.ts +77 -0
  211. package/src/i18n/strings/ja.ts +77 -0
  212. package/src/i18n/strings/ko.ts +76 -0
  213. package/src/i18n/strings/nl.ts +77 -0
  214. package/src/i18n/strings/pl.ts +77 -0
  215. package/src/i18n/strings/pt.ts +77 -0
  216. package/src/i18n/strings/ru.ts +77 -0
  217. package/src/i18n/strings/tr.ts +77 -0
  218. package/src/i18n/strings/zh.ts +75 -0
  219. package/src/index.tsx +9 -0
  220. package/src/theme.ts +165 -18
  221. package/src/types.ts +56 -2
  222. package/src/unread.ts +91 -0
  223. package/src/useSupportThreads.ts +392 -0
  224. package/src/utils.ts +48 -10
@@ -1,17 +1,20 @@
1
- import { ScrollView, Text, View } from 'react-native';
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,
11
+ IconButton,
10
12
  PressableRow,
13
+ SectionLabel,
11
14
  SkeletonBlock,
12
15
  textStyles,
13
- typeColor,
14
- typeIcon,
16
+ TypeGlyph,
17
+ UnreadPill,
15
18
  } from '../primitives';
16
19
 
17
20
  const COMPOSE_TYPES: QueryType[] = ['feedback', 'feature_request', 'bug_report'];
@@ -19,83 +22,158 @@ const COMPOSE_TYPES: QueryType[] = ['feedback', 'feature_request', 'bug_report']
19
22
  function ThreadRow({
20
23
  thread,
21
24
  theme,
25
+ strings,
22
26
  onPress,
27
+ last,
23
28
  }: {
24
29
  thread: Thread;
25
30
  theme: SupportTheme;
31
+ strings: Translator;
26
32
  onPress: () => void;
33
+ last: boolean;
27
34
  }) {
28
- const preview = thread.last_message?.body ?? '';
35
+ const unread = thread.unread_count > 0;
36
+ const type = strings.t(`type.${thread.type}`);
37
+ const preview = thread.last_message?.body ?? strings.t('home.noMessages');
38
+ const at = thread.last_message?.created_at ?? thread.created_at;
39
+
29
40
  return (
30
41
  <PressableRow
31
42
  onPress={onPress}
32
43
  theme={theme}
33
- accessibilityLabel={`${QUERY_TYPE_LABEL[thread.type]} thread`}
44
+ accessibilityLabel={
45
+ unread
46
+ ? strings.t('home.threadUnreadA11y', {
47
+ type,
48
+ count: thread.unread_count,
49
+ })
50
+ : strings.t('home.threadA11y', { type })
51
+ }
34
52
  style={{
53
+ flexDirection: 'row',
54
+ alignItems: 'center',
55
+ gap: 12,
35
56
  paddingHorizontal: 16,
36
- paddingVertical: 10,
37
- borderBottomWidth: 1,
57
+ paddingVertical: 13,
58
+ borderBottomWidth: last ? 0 : 1,
38
59
  borderBottomColor: theme.colors.border,
60
+ opacity: thread.status === 'resolved' ? 0.75 : 1,
39
61
  }}
40
62
  >
41
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
42
- {thread.unread_count > 0 && (
43
- <View
63
+ <TypeGlyph type={thread.type} theme={theme} size={38} />
64
+
65
+ <View style={{ flex: 1, gap: 3 }}>
66
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
67
+ <Text
68
+ numberOfLines={1}
44
69
  style={{
45
- width: 6,
46
- height: 6,
47
- borderRadius: 3,
48
- backgroundColor: theme.colors.accent,
70
+ flex: 1,
71
+ fontSize: 14,
72
+ fontWeight: unread ? '700' : '600',
73
+ letterSpacing: -0.1,
74
+ color: theme.colors.textPrimary,
49
75
  }}
50
- />
51
- )}
52
- <Text style={{ fontSize: 11, fontWeight: '500', color: typeColor(thread.type, theme) }}>
53
- {QUERY_TYPE_LABEL[thread.type]}
54
- </Text>
55
- <Text style={[textStyles.meta(theme), { marginLeft: 'auto' }]}>
56
- {thread.last_message
57
- ? relativeTime(thread.last_message.created_at)
58
- : relativeTime(thread.created_at)}
59
- </Text>
60
- </View>
61
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 3 }}>
62
- <Text
63
- numberOfLines={1}
64
- style={{
65
- flex: 1,
66
- fontSize: 13,
67
- color:
68
- thread.unread_count > 0
69
- ? theme.colors.textPrimary
70
- : theme.colors.textSecondary,
71
- fontWeight: thread.unread_count > 0 ? '500' : '400',
72
- }}
73
- >
74
- {preview}
75
- </Text>
76
- <Chevron color={theme.colors.textTertiary} />
76
+ >
77
+ {type}
78
+ </Text>
79
+ <Text style={textStyles.meta(theme)}>{relativeTime(at, strings)}</Text>
80
+ </View>
81
+
82
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
83
+ <Text
84
+ numberOfLines={1}
85
+ style={{
86
+ flex: 1,
87
+ fontSize: 13,
88
+ lineHeight: 18,
89
+ color: unread ? theme.colors.textPrimary : theme.colors.textSecondary,
90
+ }}
91
+ >
92
+ {thread.last_message?.sender === 'user'
93
+ ? `${strings.t('home.you')}: `
94
+ : ''}
95
+ {preview}
96
+ </Text>
97
+ {unread ? (
98
+ <UnreadPill count={thread.unread_count} theme={theme} />
99
+ ) : (
100
+ <Chevron color={theme.colors.textTertiary} />
101
+ )}
102
+ </View>
77
103
  </View>
78
104
  </PressableRow>
79
105
  );
80
106
  }
81
107
 
108
+ function ThreadGroup({
109
+ label,
110
+ threads,
111
+ theme,
112
+ strings,
113
+ onOpenThread,
114
+ }: {
115
+ label: string;
116
+ threads: Thread[];
117
+ theme: SupportTheme;
118
+ strings: Translator;
119
+ onOpenThread: (thread: Thread) => void;
120
+ }) {
121
+ if (threads.length === 0) return null;
122
+ return (
123
+ <View style={{ marginTop: 24 }}>
124
+ <SectionLabel theme={theme} style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
125
+ {label}
126
+ </SectionLabel>
127
+ <View
128
+ style={{
129
+ marginHorizontal: 16,
130
+ borderWidth: 1,
131
+ borderColor: theme.colors.border,
132
+ borderRadius: theme.radii.card,
133
+ backgroundColor: theme.colors.surface,
134
+ overflow: 'hidden',
135
+ }}
136
+ >
137
+ {threads.map((thread, i) => (
138
+ <ThreadRow
139
+ key={thread.id}
140
+ thread={thread}
141
+ theme={theme}
142
+ strings={strings}
143
+ onPress={() => onOpenThread(thread)}
144
+ last={i === threads.length - 1}
145
+ />
146
+ ))}
147
+ </View>
148
+ </View>
149
+ );
150
+ }
151
+
82
152
  export function HomeScreen({
83
153
  theme,
154
+ strings,
155
+ language,
84
156
  threads,
85
157
  loading,
158
+ refreshing,
86
159
  error,
87
- onRetry,
160
+ onRefresh,
88
161
  onCompose,
89
162
  onOpenThread,
163
+ onChangeLanguage,
90
164
  onClose,
91
165
  }: {
92
166
  theme: SupportTheme;
167
+ strings: Translator;
168
+ language: Language;
93
169
  threads: Thread[] | null;
94
170
  loading: boolean;
171
+ refreshing: boolean;
95
172
  error: string | null;
96
- onRetry: () => void;
173
+ onRefresh: () => void;
97
174
  onCompose: (type: QueryType) => void;
98
175
  onOpenThread: (thread: Thread) => void;
176
+ onChangeLanguage: () => void;
99
177
  onClose: () => void;
100
178
  }) {
101
179
  const open = (threads ?? []).filter((t) => t.status === 'open');
@@ -107,23 +185,42 @@ export function HomeScreen({
107
185
  <View
108
186
  style={{
109
187
  flexDirection: 'row',
110
- alignItems: 'center',
188
+ alignItems: 'flex-start',
111
189
  paddingHorizontal: 16,
112
- paddingBottom: 12,
190
+ paddingTop: 4,
191
+ paddingBottom: 16,
113
192
  }}
114
193
  >
115
- <Text style={textStyles.title(theme)}>Support</Text>
116
- <PressableRow
194
+ <View style={{ flex: 1, gap: 2 }}>
195
+ <Text style={textStyles.title(theme)}>{strings.t('home.title')}</Text>
196
+ <Text style={textStyles.secondary(theme)}>
197
+ {strings.t('home.subtitle')}
198
+ </Text>
199
+ </View>
200
+ <IconButton
117
201
  onPress={onClose}
118
202
  theme={theme}
119
- accessibilityLabel="Close"
120
- style={{ marginLeft: 'auto', padding: 6, borderRadius: theme.radii.control }}
203
+ accessibilityLabel={strings.t('home.close')}
204
+ size={32}
121
205
  >
122
- <CloseIcon color={theme.colors.textSecondary} size={13} />
123
- </PressableRow>
206
+ <CloseIcon color={theme.colors.textSecondary} size={12} />
207
+ </IconButton>
124
208
  </View>
125
209
 
126
- <ScrollView style={{ flex: 1 }}>
210
+ <ScrollView
211
+ style={{ flex: 1 }}
212
+ contentContainerStyle={{ paddingBottom: 24 }}
213
+ keyboardShouldPersistTaps="handled"
214
+ refreshControl={
215
+ <RefreshControl
216
+ refreshing={refreshing}
217
+ onRefresh={onRefresh}
218
+ tintColor={theme.colors.textTertiary}
219
+ colors={[theme.colors.accent]}
220
+ progressBackgroundColor={theme.colors.surface}
221
+ />
222
+ }
223
+ >
127
224
  {/* Compose actions */}
128
225
  <View
129
226
  style={{
@@ -140,94 +237,107 @@ export function HomeScreen({
140
237
  key={type}
141
238
  onPress={() => onCompose(type)}
142
239
  theme={theme}
143
- accessibilityLabel={`New ${QUERY_TYPE_LABEL[type].toLowerCase()}`}
240
+ accessibilityLabel={strings.t('home.newA11y', {
241
+ type: strings.t(`type.${type}`),
242
+ })}
144
243
  style={{
145
244
  flexDirection: 'row',
146
245
  alignItems: 'center',
147
- gap: 10,
246
+ gap: 12,
148
247
  paddingHorizontal: 14,
149
248
  paddingVertical: 12,
150
249
  borderTopWidth: i === 0 ? 0 : 1,
151
250
  borderTopColor: theme.colors.border,
152
251
  }}
153
252
  >
154
- {typeIcon(type, typeColor(type, theme))}
155
- <Text style={{ fontSize: 14, color: theme.colors.textPrimary, flex: 1 }}>
156
- {QUERY_TYPE_LABEL[type]}
157
- </Text>
253
+ <TypeGlyph type={type} theme={theme} size={34} />
254
+ <View style={{ flex: 1, gap: 1 }}>
255
+ <Text style={textStyles.heading(theme)}>
256
+ {strings.t(`type.${type}`)}
257
+ </Text>
258
+ <Text style={textStyles.meta(theme)}>
259
+ {strings.t(`hint.${type}`)}
260
+ </Text>
261
+ </View>
158
262
  <Chevron color={theme.colors.textTertiary} />
159
263
  </PressableRow>
160
264
  ))}
161
265
  </View>
162
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
+
163
303
  {/* Threads */}
164
- {error && threads === null ? (
304
+ {error !== null && threads === null ? (
165
305
  // Only take over the screen when there's nothing cached to show —
166
306
  // a failed background poll shouldn't hide an already-loaded list.
167
- <ErrorState message={error} onRetry={onRetry} theme={theme} />
307
+ <ErrorState
308
+ message={error}
309
+ onRetry={onRefresh}
310
+ theme={theme}
311
+ strings={strings}
312
+ />
168
313
  ) : loading && threads === null ? (
169
- <View style={{ paddingHorizontal: 16, paddingTop: 24, gap: 14 }}>
170
- <SkeletonBlock width={80} height={12} theme={theme} />
171
- <SkeletonBlock width="100%" height={40} theme={theme} />
172
- <SkeletonBlock width="100%" height={40} theme={theme} />
314
+ <View style={{ paddingHorizontal: 16, paddingTop: 28, gap: 12 }}>
315
+ <SkeletonBlock width={110} height={11} theme={theme} radius={4} />
316
+ <SkeletonBlock width="100%" height={64} theme={theme} radius={theme.radii.card} />
317
+ <SkeletonBlock width="100%" height={64} theme={theme} radius={theme.radii.card} />
173
318
  </View>
174
319
  ) : (threads ?? []).length === 0 ? (
175
- <EmptyState message="No queries yet" theme={theme} />
320
+ <EmptyState
321
+ message={strings.t('home.empty')}
322
+ hint={strings.t('home.emptyHint')}
323
+ theme={theme}
324
+ />
176
325
  ) : (
177
326
  <>
178
- {open.length > 0 && (
179
- <>
180
- <Text
181
- style={{
182
- fontSize: 11,
183
- fontWeight: '500',
184
- textTransform: 'uppercase',
185
- letterSpacing: 0.4,
186
- color: theme.colors.textTertiary,
187
- paddingHorizontal: 16,
188
- paddingTop: 20,
189
- paddingBottom: 6,
190
- }}
191
- >
192
- Open
193
- </Text>
194
- {open.map((t) => (
195
- <ThreadRow
196
- key={t.id}
197
- thread={t}
198
- theme={theme}
199
- onPress={() => onOpenThread(t)}
200
- />
201
- ))}
202
- </>
203
- )}
204
- {resolved.length > 0 && (
205
- <>
206
- <Text
207
- style={{
208
- fontSize: 11,
209
- fontWeight: '500',
210
- textTransform: 'uppercase',
211
- letterSpacing: 0.4,
212
- color: theme.colors.textTertiary,
213
- paddingHorizontal: 16,
214
- paddingTop: 20,
215
- paddingBottom: 6,
216
- }}
217
- >
218
- Resolved
219
- </Text>
220
- {resolved.map((t) => (
221
- <ThreadRow
222
- key={t.id}
223
- thread={t}
224
- theme={theme}
225
- onPress={() => onOpenThread(t)}
226
- />
227
- ))}
228
- </>
229
- )}
230
- <View style={{ height: 16 }} />
327
+ <ThreadGroup
328
+ label={`${strings.t('home.open')} · ${open.length}`}
329
+ threads={open}
330
+ theme={theme}
331
+ strings={strings}
332
+ onOpenThread={onOpenThread}
333
+ />
334
+ <ThreadGroup
335
+ label={strings.t('home.resolved')}
336
+ threads={resolved}
337
+ theme={theme}
338
+ strings={strings}
339
+ onOpenThread={onOpenThread}
340
+ />
231
341
  </>
232
342
  )}
233
343
  </ScrollView>
@@ -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
+ }