@appsupportkit/support-sdk 0.1.2 → 0.2.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/components/BottomSheet.js +67 -47
- package/lib/commonjs/components/BottomSheet.js.map +1 -1
- package/lib/commonjs/components/LauncherButton.js +177 -0
- package/lib/commonjs/components/LauncherButton.js.map +1 -0
- package/lib/commonjs/components/SupportButton.js +54 -176
- package/lib/commonjs/components/SupportButton.js.map +1 -1
- package/lib/commonjs/components/icons.js +74 -23
- package/lib/commonjs/components/icons.js.map +1 -1
- package/lib/commonjs/components/primitives.js +183 -38
- package/lib/commonjs/components/primitives.js.map +1 -1
- package/lib/commonjs/components/screens/ComposeScreen.js +115 -58
- package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/HomeScreen.js +170 -115
- package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
- package/lib/commonjs/components/screens/ThreadScreen.js +222 -108
- package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/theme.js +136 -17
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/types.js +7 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/unread.js +92 -0
- package/lib/commonjs/unread.js.map +1 -0
- package/lib/commonjs/useSupportThreads.js +286 -0
- package/lib/commonjs/useSupportThreads.js.map +1 -0
- package/lib/commonjs/utils.js +33 -0
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/module/components/BottomSheet.js +68 -48
- package/lib/module/components/BottomSheet.js.map +1 -1
- package/lib/module/components/LauncherButton.js +172 -0
- package/lib/module/components/LauncherButton.js.map +1 -0
- package/lib/module/components/SupportButton.js +57 -179
- package/lib/module/components/SupportButton.js.map +1 -1
- package/lib/module/components/icons.js +72 -22
- package/lib/module/components/icons.js.map +1 -1
- package/lib/module/components/primitives.js +179 -39
- package/lib/module/components/primitives.js.map +1 -1
- package/lib/module/components/screens/ComposeScreen.js +118 -61
- package/lib/module/components/screens/ComposeScreen.js.map +1 -1
- package/lib/module/components/screens/HomeScreen.js +172 -117
- package/lib/module/components/screens/HomeScreen.js.map +1 -1
- package/lib/module/components/screens/ThreadScreen.js +228 -112
- package/lib/module/components/screens/ThreadScreen.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme.js +135 -16
- package/lib/module/theme.js.map +1 -1
- package/lib/module/types.js +7 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/unread.js +83 -0
- package/lib/module/unread.js.map +1 -0
- package/lib/module/useSupportThreads.js +281 -0
- package/lib/module/useSupportThreads.js.map +1 -0
- package/lib/module/utils.js +30 -0
- package/lib/module/utils.js.map +1 -1
- package/lib/typescript/components/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/components/LauncherButton.d.ts +14 -0
- package/lib/typescript/components/LauncherButton.d.ts.map +1 -0
- package/lib/typescript/components/SupportButton.d.ts +3 -3
- package/lib/typescript/components/SupportButton.d.ts.map +1 -1
- package/lib/typescript/components/icons.d.ts +13 -2
- package/lib/typescript/components/icons.d.ts.map +1 -1
- package/lib/typescript/components/primitives.d.ts +35 -4
- package/lib/typescript/components/primitives.d.ts.map +1 -1
- package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/HomeScreen.d.ts +3 -2
- package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
- package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/theme.d.ts +44 -3
- package/lib/typescript/theme.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +22 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/unread.d.ts +22 -0
- package/lib/typescript/unread.d.ts.map +1 -0
- package/lib/typescript/useSupportThreads.d.ts +28 -0
- package/lib/typescript/useSupportThreads.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +5 -0
- package/lib/typescript/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/BottomSheet.tsx +61 -43
- package/src/components/LauncherButton.tsx +217 -0
- package/src/components/SupportButton.tsx +52 -196
- package/src/components/icons.tsx +77 -19
- package/src/components/primitives.tsx +217 -36
- package/src/components/screens/ComposeScreen.tsx +131 -61
- package/src/components/screens/HomeScreen.tsx +168 -117
- package/src/components/screens/ThreadScreen.tsx +289 -119
- package/src/index.tsx +1 -0
- package/src/theme.ts +165 -18
- package/src/types.ts +24 -0
- package/src/unread.ts +91 -0
- package/src/useSupportThreads.ts +372 -0
- package/src/utils.ts +33 -0
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
3
|
import {
|
|
4
|
-
getThreads,
|
|
5
|
-
getUnreadCount,
|
|
6
|
-
markRead,
|
|
7
4
|
postMessage,
|
|
8
5
|
registerPush,
|
|
9
6
|
submitQuery,
|
|
@@ -13,9 +10,10 @@ import {
|
|
|
13
10
|
import { ensurePushPermission, isPushAvailable } from '../push';
|
|
14
11
|
import { resolveTheme } from '../theme';
|
|
15
12
|
import type { QueryType, SupportButtonProps, Thread } from '../types';
|
|
13
|
+
import { IDLE_POLL_MS, useSupportThreads } from '../useSupportThreads';
|
|
16
14
|
import { validateProps } from '../utils';
|
|
17
15
|
import { BottomSheet } from './BottomSheet';
|
|
18
|
-
import {
|
|
16
|
+
import { LauncherButton } from './LauncherButton';
|
|
19
17
|
import { ComposeScreen } from './screens/ComposeScreen';
|
|
20
18
|
import { HomeScreen } from './screens/HomeScreen';
|
|
21
19
|
import { ThreadScreen } from './screens/ThreadScreen';
|
|
@@ -25,23 +23,17 @@ type Screen =
|
|
|
25
23
|
| { name: 'compose'; type: QueryType }
|
|
26
24
|
| { name: 'thread'; threadId: string };
|
|
27
25
|
|
|
28
|
-
// Poll cadence by context: fast where latency is felt (staring at a thread
|
|
29
|
-
// waiting for a reply), slow when the sheet is closed — and the idle tick
|
|
30
|
-
// uses the count-only endpoint, so it costs ~18 bytes instead of the full
|
|
31
|
-
// thread list.
|
|
32
|
-
const THREAD_POLL_MS = 5_000;
|
|
33
|
-
const SHEET_POLL_MS = 15_000;
|
|
34
|
-
const IDLE_POLL_MS = 60_000;
|
|
35
|
-
const MIN_POLL_MS = 3_000;
|
|
36
|
-
|
|
37
26
|
/**
|
|
38
|
-
* The single entry point of the SDK: a
|
|
39
|
-
* opens the support bottom sheet.
|
|
40
|
-
*
|
|
27
|
+
* The single entry point of the SDK: a launcher with an unread badge that
|
|
28
|
+
* opens the support bottom sheet. Threads, polling, and read receipts are
|
|
29
|
+
* handled by useSupportThreads.
|
|
41
30
|
*/
|
|
42
31
|
export function SupportButton(props: SupportButtonProps) {
|
|
43
32
|
const validationError = validateProps(props);
|
|
44
|
-
const theme =
|
|
33
|
+
const theme = useMemo(
|
|
34
|
+
() => resolveTheme(props.theme, props.accentColor),
|
|
35
|
+
[props.theme, props.accentColor],
|
|
36
|
+
);
|
|
45
37
|
|
|
46
38
|
const config: ApiConfig = useMemo(
|
|
47
39
|
() => ({
|
|
@@ -54,86 +46,17 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
54
46
|
|
|
55
47
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
56
48
|
const [screen, setScreen] = useState<Screen>({ name: 'home' });
|
|
57
|
-
const [threads, setThreads] = useState<Thread[] | null>(null);
|
|
58
|
-
const [totalUnread, setTotalUnread] = useState(0);
|
|
59
|
-
const [loading, setLoading] = useState(false);
|
|
60
|
-
const [error, setError] = useState<string | null>(null);
|
|
61
|
-
const mounted = useRef(true);
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
mounted.current = true;
|
|
65
|
-
return () => {
|
|
66
|
-
mounted.current = false;
|
|
67
|
-
};
|
|
68
|
-
}, []);
|
|
69
|
-
|
|
70
|
-
const refresh = useCallback(async () => {
|
|
71
|
-
if (validationError) return;
|
|
72
|
-
setLoading(true);
|
|
73
|
-
setError(null);
|
|
74
|
-
try {
|
|
75
|
-
const data = await getThreads(config, props.user.revenuecatId);
|
|
76
|
-
if (!mounted.current) return;
|
|
77
|
-
setThreads(data.threads);
|
|
78
|
-
setTotalUnread(data.total_unread);
|
|
79
|
-
} catch (e) {
|
|
80
|
-
if (!mounted.current) return;
|
|
81
|
-
setError(
|
|
82
|
-
e instanceof SupportApiError && e.offline
|
|
83
|
-
? "You're offline — check your connection"
|
|
84
|
-
: "Couldn't load threads",
|
|
85
|
-
);
|
|
86
|
-
} finally {
|
|
87
|
-
if (mounted.current) setLoading(false);
|
|
88
|
-
}
|
|
89
|
-
}, [config, props.user.revenuecatId, validationError]);
|
|
90
|
-
|
|
91
|
-
/** Badge-only refresh — the cheap endpoint used while idle. */
|
|
92
|
-
const refreshBadge = useCallback(async () => {
|
|
93
|
-
if (validationError) return;
|
|
94
|
-
try {
|
|
95
|
-
const data = await getUnreadCount(config, props.user.revenuecatId);
|
|
96
|
-
if (mounted.current) setTotalUnread(data.total_unread);
|
|
97
|
-
} catch {
|
|
98
|
-
// Best-effort: the next tick or the next sheet open reconciles.
|
|
99
|
-
}
|
|
100
|
-
}, [config, props.user.revenuecatId, validationError]);
|
|
101
|
-
|
|
102
|
-
// Poll on mount…
|
|
103
|
-
useEffect(() => {
|
|
104
|
-
void refresh();
|
|
105
|
-
}, [refresh]);
|
|
106
|
-
|
|
107
|
-
// …then adapt the cadence to what the user is looking at, and refresh
|
|
108
|
-
// whenever the app returns to the foreground.
|
|
109
|
-
const idlePollMs = props.pollIntervalMs ?? IDLE_POLL_MS;
|
|
110
|
-
const screenName = screen.name;
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
const idle = !sheetOpen;
|
|
113
|
-
const intervalMs = idle
|
|
114
|
-
? idlePollMs
|
|
115
|
-
: screenName === 'thread'
|
|
116
|
-
? THREAD_POLL_MS
|
|
117
|
-
: SHEET_POLL_MS;
|
|
118
|
-
|
|
119
|
-
// While idle only the badge matters, so use the count-only endpoint.
|
|
120
|
-
const tick = () => void (idle ? refreshBadge() : refresh());
|
|
121
|
-
|
|
122
|
-
// Clamped so a typo like `pollIntervalMs={30}` can't hammer the API;
|
|
123
|
-
// 0 (or negative) disables idle polling entirely.
|
|
124
|
-
const timer =
|
|
125
|
-
intervalMs > 0 ? setInterval(tick, Math.max(intervalMs, MIN_POLL_MS)) : null;
|
|
126
49
|
|
|
127
|
-
|
|
128
|
-
|
|
50
|
+
const { threads, totalUnread, loading, refreshing, error, refresh, reload } =
|
|
51
|
+
useSupportThreads({
|
|
52
|
+
config,
|
|
53
|
+
revenuecatId: props.user.revenuecatId,
|
|
54
|
+
enabled: validationError === null,
|
|
55
|
+
sheetOpen,
|
|
56
|
+
activeThreadId: screen.name === 'thread' ? screen.threadId : null,
|
|
57
|
+
idlePollMs: props.pollIntervalMs ?? IDLE_POLL_MS,
|
|
129
58
|
});
|
|
130
59
|
|
|
131
|
-
return () => {
|
|
132
|
-
if (timer !== null) clearInterval(timer);
|
|
133
|
-
appStateSub.remove();
|
|
134
|
-
};
|
|
135
|
-
}, [sheetOpen, screenName, idlePollMs, refresh, refreshBadge]);
|
|
136
|
-
|
|
137
60
|
/**
|
|
138
61
|
* Asks for notification permission the moment the sheet opens, then
|
|
139
62
|
* reports the outcome (including denials) so the dashboard knows whether
|
|
@@ -141,7 +64,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
141
64
|
* support flow waits on it or breaks if it fails.
|
|
142
65
|
*/
|
|
143
66
|
const syncPushPermission = useCallback(async () => {
|
|
144
|
-
if (validationError || !isPushAvailable) return;
|
|
67
|
+
if (validationError !== null || !isPushAvailable) return;
|
|
145
68
|
try {
|
|
146
69
|
const registration = await ensurePushPermission(props.expoProjectId);
|
|
147
70
|
await registerPush(config, {
|
|
@@ -155,11 +78,9 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
155
78
|
}
|
|
156
79
|
}, [config, props.user.revenuecatId, props.expoProjectId, validationError]);
|
|
157
80
|
|
|
158
|
-
// …and on sheet open.
|
|
159
81
|
function openSheet() {
|
|
160
82
|
setScreen({ name: 'home' });
|
|
161
83
|
setSheetOpen(true);
|
|
162
|
-
void refresh();
|
|
163
84
|
// The OS prompt appears right as the sheet opens; if the user declines,
|
|
164
85
|
// everything below carries on exactly as before.
|
|
165
86
|
void syncPushPermission();
|
|
@@ -170,7 +91,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
170
91
|
setScreen({ name: 'home' });
|
|
171
92
|
}
|
|
172
93
|
|
|
173
|
-
if (validationError) {
|
|
94
|
+
if (validationError !== null) {
|
|
174
95
|
// Dev-mode: loud and actionable. Production: render nothing rather than
|
|
175
96
|
// a broken button.
|
|
176
97
|
if (__DEV__) {
|
|
@@ -193,26 +114,6 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
193
114
|
return null;
|
|
194
115
|
}
|
|
195
116
|
|
|
196
|
-
function openThread(thread: Thread) {
|
|
197
|
-
setScreen({ name: 'thread', threadId: thread.id });
|
|
198
|
-
if (thread.unread_count > 0) {
|
|
199
|
-
// Optimistically clear the badge; persist via mark-read.
|
|
200
|
-
setThreads(
|
|
201
|
-
(prev) =>
|
|
202
|
-
prev?.map((t) =>
|
|
203
|
-
t.id === thread.id ? { ...t, unread_count: 0 } : t,
|
|
204
|
-
) ?? null,
|
|
205
|
-
);
|
|
206
|
-
setTotalUnread((n) => Math.max(0, n - thread.unread_count));
|
|
207
|
-
void markRead(config, {
|
|
208
|
-
queryId: thread.id,
|
|
209
|
-
revenuecatId: props.user.revenuecatId,
|
|
210
|
-
}).catch(() => {
|
|
211
|
-
// Non-fatal: the next get-threads reconciles the badge.
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
117
|
function writeErrorMessage(e: unknown): string {
|
|
217
118
|
if (e instanceof SupportApiError) {
|
|
218
119
|
if (e.status === 429) return 'Too many requests — try again later';
|
|
@@ -234,7 +135,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
234
135
|
revenuecatId: props.user.revenuecatId,
|
|
235
136
|
appVersion: props.user.appVersion,
|
|
236
137
|
});
|
|
237
|
-
void
|
|
138
|
+
void reload();
|
|
238
139
|
return null;
|
|
239
140
|
} catch (e) {
|
|
240
141
|
return writeErrorMessage(e);
|
|
@@ -251,84 +152,48 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
251
152
|
revenuecatId: props.user.revenuecatId,
|
|
252
153
|
message,
|
|
253
154
|
});
|
|
254
|
-
await
|
|
155
|
+
await reload();
|
|
255
156
|
return null;
|
|
256
157
|
} catch (e) {
|
|
257
158
|
return writeErrorMessage(e);
|
|
258
159
|
}
|
|
259
160
|
}
|
|
260
161
|
|
|
261
|
-
const activeThread =
|
|
162
|
+
const activeThread: Thread | null =
|
|
262
163
|
screen.name === 'thread'
|
|
263
|
-
? (threads ?? []).find((t) => t.id === screen.threadId) ?? null
|
|
164
|
+
? ((threads ?? []).find((t) => t.id === screen.threadId) ?? null)
|
|
264
165
|
: null;
|
|
265
166
|
|
|
167
|
+
const home = (
|
|
168
|
+
<HomeScreen
|
|
169
|
+
theme={theme}
|
|
170
|
+
threads={threads}
|
|
171
|
+
loading={loading}
|
|
172
|
+
refreshing={refreshing}
|
|
173
|
+
error={error}
|
|
174
|
+
onRefresh={refresh}
|
|
175
|
+
onCompose={(type) => setScreen({ name: 'compose', type })}
|
|
176
|
+
onOpenThread={(thread) =>
|
|
177
|
+
setScreen({ name: 'thread', threadId: thread.id })
|
|
178
|
+
}
|
|
179
|
+
onClose={closeSheet}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
|
|
266
183
|
return (
|
|
267
184
|
<>
|
|
268
|
-
<
|
|
185
|
+
<LauncherButton
|
|
186
|
+
theme={theme}
|
|
187
|
+
unread={totalUnread}
|
|
188
|
+
variant={props.variant}
|
|
189
|
+
size={props.size}
|
|
190
|
+
label={props.label}
|
|
191
|
+
style={props.style}
|
|
269
192
|
onPress={openSheet}
|
|
270
|
-
|
|
271
|
-
accessibilityLabel={
|
|
272
|
-
totalUnread > 0
|
|
273
|
-
? `Support, ${totalUnread} unread ${totalUnread === 1 ? 'reply' : 'replies'}`
|
|
274
|
-
: 'Support'
|
|
275
|
-
}
|
|
276
|
-
style={({ pressed }) => ({
|
|
277
|
-
width: 44,
|
|
278
|
-
height: 44,
|
|
279
|
-
borderRadius: 22,
|
|
280
|
-
borderWidth: 1,
|
|
281
|
-
borderColor: theme.colors.border,
|
|
282
|
-
backgroundColor: pressed
|
|
283
|
-
? theme.colors.surfaceHover
|
|
284
|
-
: theme.colors.surface,
|
|
285
|
-
alignItems: 'center',
|
|
286
|
-
justifyContent: 'center',
|
|
287
|
-
})}
|
|
288
|
-
>
|
|
289
|
-
<BubbleMark color={theme.colors.textPrimary} size={18} />
|
|
290
|
-
{totalUnread > 0 && (
|
|
291
|
-
<View
|
|
292
|
-
style={{
|
|
293
|
-
position: 'absolute',
|
|
294
|
-
top: -3,
|
|
295
|
-
right: -3,
|
|
296
|
-
minWidth: 16,
|
|
297
|
-
height: 16,
|
|
298
|
-
borderRadius: 8,
|
|
299
|
-
paddingHorizontal: 4,
|
|
300
|
-
backgroundColor: theme.colors.accent,
|
|
301
|
-
alignItems: 'center',
|
|
302
|
-
justifyContent: 'center',
|
|
303
|
-
}}
|
|
304
|
-
>
|
|
305
|
-
<Text
|
|
306
|
-
style={{
|
|
307
|
-
fontSize: 10,
|
|
308
|
-
fontWeight: '600',
|
|
309
|
-
color: theme.colors.accentForeground,
|
|
310
|
-
fontVariant: ['tabular-nums'],
|
|
311
|
-
}}
|
|
312
|
-
>
|
|
313
|
-
{totalUnread > 9 ? '9+' : totalUnread}
|
|
314
|
-
</Text>
|
|
315
|
-
</View>
|
|
316
|
-
)}
|
|
317
|
-
</Pressable>
|
|
193
|
+
/>
|
|
318
194
|
|
|
319
195
|
<BottomSheet visible={sheetOpen} onClose={closeSheet} theme={theme}>
|
|
320
|
-
{screen.name === 'home' &&
|
|
321
|
-
<HomeScreen
|
|
322
|
-
theme={theme}
|
|
323
|
-
threads={threads}
|
|
324
|
-
loading={loading}
|
|
325
|
-
error={error}
|
|
326
|
-
onRetry={() => void refresh()}
|
|
327
|
-
onCompose={(type) => setScreen({ name: 'compose', type })}
|
|
328
|
-
onOpenThread={openThread}
|
|
329
|
-
onClose={closeSheet}
|
|
330
|
-
/>
|
|
331
|
-
)}
|
|
196
|
+
{screen.name === 'home' && home}
|
|
332
197
|
{screen.name === 'compose' && (
|
|
333
198
|
<ComposeScreen
|
|
334
199
|
theme={theme}
|
|
@@ -338,7 +203,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
338
203
|
/>
|
|
339
204
|
)}
|
|
340
205
|
{screen.name === 'thread' &&
|
|
341
|
-
(activeThread ? (
|
|
206
|
+
(activeThread !== null ? (
|
|
342
207
|
<ThreadScreen
|
|
343
208
|
theme={theme}
|
|
344
209
|
thread={activeThread}
|
|
@@ -346,16 +211,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
346
211
|
onSend={(message) => handleSend(activeThread.id, message)}
|
|
347
212
|
/>
|
|
348
213
|
) : (
|
|
349
|
-
|
|
350
|
-
theme={theme}
|
|
351
|
-
threads={threads}
|
|
352
|
-
loading={loading}
|
|
353
|
-
error={error}
|
|
354
|
-
onRetry={() => void refresh()}
|
|
355
|
-
onCompose={(type) => setScreen({ name: 'compose', type })}
|
|
356
|
-
onOpenThread={openThread}
|
|
357
|
-
onClose={closeSheet}
|
|
358
|
-
/>
|
|
214
|
+
home
|
|
359
215
|
))}
|
|
360
216
|
</BottomSheet>
|
|
361
217
|
</>
|
package/src/components/icons.tsx
CHANGED
|
@@ -145,43 +145,101 @@ export function CheckIcon({ color, size = 16 }: { color: string; size?: number }
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/**
|
|
149
|
-
export function
|
|
148
|
+
/** Send arrow: a stem plus a chevron head, both centered on the same axis. */
|
|
149
|
+
export function ArrowUpIcon({ color, size = 16 }: { color: string; size?: number }) {
|
|
150
|
+
const stroke = STROKE + 0.4;
|
|
151
|
+
const head = size * 0.44;
|
|
152
|
+
return (
|
|
153
|
+
<View
|
|
154
|
+
style={{
|
|
155
|
+
width: size,
|
|
156
|
+
height: size,
|
|
157
|
+
alignItems: 'center',
|
|
158
|
+
justifyContent: 'center',
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<View
|
|
162
|
+
style={{
|
|
163
|
+
position: 'absolute',
|
|
164
|
+
width: stroke,
|
|
165
|
+
height: size * 0.66,
|
|
166
|
+
borderRadius: stroke,
|
|
167
|
+
backgroundColor: color,
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
<View
|
|
171
|
+
style={{
|
|
172
|
+
position: 'absolute',
|
|
173
|
+
// Rotating a square by 45° lifts its apex by ~0.207 × side, so
|
|
174
|
+
// offset by that to land the point on top of the stem.
|
|
175
|
+
top: size * 0.17 + head * 0.207,
|
|
176
|
+
width: head,
|
|
177
|
+
height: head,
|
|
178
|
+
borderTopWidth: stroke,
|
|
179
|
+
borderLeftWidth: stroke,
|
|
180
|
+
borderTopLeftRadius: stroke,
|
|
181
|
+
borderColor: color,
|
|
182
|
+
transform: [{ rotate: '45deg' }],
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
</View>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The chat-bubble mark on the launcher button: a filled bubble with the
|
|
191
|
+
* dots knocked out in the button's own fill color. Solid shapes read far
|
|
192
|
+
* better than 1.5px strokes at launcher size and on top of a color fill.
|
|
193
|
+
*/
|
|
194
|
+
export function ChatBubbleMark({
|
|
195
|
+
color,
|
|
196
|
+
dotColor,
|
|
197
|
+
size = 24,
|
|
198
|
+
}: {
|
|
199
|
+
color: string;
|
|
200
|
+
/** The surface behind the mark — the dots are punched out in this color. */
|
|
201
|
+
dotColor: string;
|
|
202
|
+
size?: number;
|
|
203
|
+
}) {
|
|
204
|
+
const bubbleHeight = size * 0.82;
|
|
150
205
|
const dot: ViewStyle = {
|
|
151
|
-
width: size * 0.
|
|
152
|
-
height: size * 0.
|
|
206
|
+
width: size * 0.13,
|
|
207
|
+
height: size * 0.13,
|
|
153
208
|
borderRadius: size,
|
|
154
|
-
backgroundColor:
|
|
209
|
+
backgroundColor: dotColor,
|
|
155
210
|
};
|
|
156
211
|
return (
|
|
157
|
-
<View style={{
|
|
212
|
+
<View style={{ width: size, height: size, justifyContent: 'flex-start' }}>
|
|
158
213
|
<View
|
|
159
214
|
style={{
|
|
160
215
|
width: size,
|
|
161
|
-
height:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
borderRadius: size * 0.28,
|
|
216
|
+
height: bubbleHeight,
|
|
217
|
+
borderRadius: size * 0.33,
|
|
218
|
+
backgroundColor: color,
|
|
165
219
|
flexDirection: 'row',
|
|
166
220
|
alignItems: 'center',
|
|
167
221
|
justifyContent: 'center',
|
|
168
|
-
gap: size * 0.
|
|
222
|
+
gap: size * 0.115,
|
|
169
223
|
}}
|
|
170
224
|
>
|
|
171
225
|
<View style={dot} />
|
|
172
226
|
<View style={dot} />
|
|
173
227
|
<View style={dot} />
|
|
174
228
|
</View>
|
|
229
|
+
{/* Tail, drawn as a border triangle so it needs no svg. */}
|
|
175
230
|
<View
|
|
176
231
|
style={{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
232
|
+
position: 'absolute',
|
|
233
|
+
left: size * 0.22,
|
|
234
|
+
top: bubbleHeight - 1,
|
|
235
|
+
width: 0,
|
|
236
|
+
height: 0,
|
|
237
|
+
borderLeftWidth: size * 0.09,
|
|
238
|
+
borderRightWidth: size * 0.16,
|
|
239
|
+
borderTopWidth: size * 0.2,
|
|
240
|
+
borderLeftColor: 'transparent',
|
|
241
|
+
borderRightColor: 'transparent',
|
|
242
|
+
borderTopColor: color,
|
|
185
243
|
}}
|
|
186
244
|
/>
|
|
187
245
|
</View>
|