@appsupportkit/support-sdk 0.1.1 → 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/api.js +29 -0
- package/lib/commonjs/api.js.map +1 -1
- 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 +76 -146
- 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 +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/push.js +103 -0
- package/lib/commonjs/push.js.map +1 -0
- 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/api.js +27 -0
- package/lib/module/api.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 +79 -149
- 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 +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/push.js +98 -0
- package/lib/module/push.js.map +1 -0
- 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/api.d.ts +17 -1
- package/lib/typescript/api.d.ts.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 +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/push.d.ts +18 -0
- package/lib/typescript/push.d.ts.map +1 -0
- package/lib/typescript/theme.d.ts +44 -3
- package/lib/typescript/theme.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +37 -4
- 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 +8 -2
- package/src/api.ts +41 -1
- package/src/components/BottomSheet.tsx +61 -43
- package/src/components/LauncherButton.tsx +217 -0
- package/src/components/SupportButton.tsx +81 -162
- 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 +3 -0
- package/src/push.ts +97 -0
- package/src/theme.ts +165 -18
- package/src/types.ts +39 -4
- package/src/unread.ts +91 -0
- package/src/useSupportThreads.ts +372 -0
- package/src/utils.ts +33 -0
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
postMessage,
|
|
5
|
+
registerPush,
|
|
6
|
+
submitQuery,
|
|
7
|
+
SupportApiError,
|
|
8
|
+
type ApiConfig,
|
|
9
|
+
} from '../api';
|
|
10
|
+
import { ensurePushPermission, isPushAvailable } from '../push';
|
|
4
11
|
import { resolveTheme } from '../theme';
|
|
5
12
|
import type { QueryType, SupportButtonProps, Thread } from '../types';
|
|
13
|
+
import { IDLE_POLL_MS, useSupportThreads } from '../useSupportThreads';
|
|
6
14
|
import { validateProps } from '../utils';
|
|
7
15
|
import { BottomSheet } from './BottomSheet';
|
|
8
|
-
import {
|
|
16
|
+
import { LauncherButton } from './LauncherButton';
|
|
9
17
|
import { ComposeScreen } from './screens/ComposeScreen';
|
|
10
18
|
import { HomeScreen } from './screens/HomeScreen';
|
|
11
19
|
import { ThreadScreen } from './screens/ThreadScreen';
|
|
@@ -16,13 +24,16 @@ type Screen =
|
|
|
16
24
|
| { name: 'thread'; threadId: string };
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
|
-
* The single entry point of the SDK: a
|
|
20
|
-
* opens the support bottom sheet.
|
|
21
|
-
*
|
|
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.
|
|
22
30
|
*/
|
|
23
31
|
export function SupportButton(props: SupportButtonProps) {
|
|
24
32
|
const validationError = validateProps(props);
|
|
25
|
-
const theme =
|
|
33
|
+
const theme = useMemo(
|
|
34
|
+
() => resolveTheme(props.theme, props.accentColor),
|
|
35
|
+
[props.theme, props.accentColor],
|
|
36
|
+
);
|
|
26
37
|
|
|
27
38
|
const config: ApiConfig = useMemo(
|
|
28
39
|
() => ({
|
|
@@ -35,71 +46,44 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
35
46
|
|
|
36
47
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
37
48
|
const [screen, setScreen] = useState<Screen>({ name: 'home' });
|
|
38
|
-
const [threads, setThreads] = useState<Thread[] | null>(null);
|
|
39
|
-
const [totalUnread, setTotalUnread] = useState(0);
|
|
40
|
-
const [loading, setLoading] = useState(false);
|
|
41
|
-
const [error, setError] = useState<string | null>(null);
|
|
42
|
-
const mounted = useRef(true);
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
58
|
+
});
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Asks for notification permission the moment the sheet opens, then
|
|
62
|
+
* reports the outcome (including denials) so the dashboard knows whether
|
|
63
|
+
* this user is reachable. Deliberately fire-and-forget: nothing in the
|
|
64
|
+
* support flow waits on it or breaks if it fails.
|
|
65
|
+
*/
|
|
66
|
+
const syncPushPermission = useCallback(async () => {
|
|
67
|
+
if (validationError !== null || !isPushAvailable) return;
|
|
55
68
|
try {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
: "Couldn't load threads",
|
|
66
|
-
);
|
|
67
|
-
} finally {
|
|
68
|
-
if (mounted.current) setLoading(false);
|
|
69
|
+
const registration = await ensurePushPermission(props.expoProjectId);
|
|
70
|
+
await registerPush(config, {
|
|
71
|
+
revenuecatId: props.user.revenuecatId,
|
|
72
|
+
permissionStatus: registration.status,
|
|
73
|
+
expoPushToken: registration.token,
|
|
74
|
+
asked: registration.asked,
|
|
75
|
+
});
|
|
76
|
+
} catch {
|
|
77
|
+
// Push is an enhancement — never surfaced to the user as an error.
|
|
69
78
|
}
|
|
70
|
-
}, [config, props.user.revenuecatId, validationError]);
|
|
71
|
-
|
|
72
|
-
// Poll on mount…
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
void refresh();
|
|
75
|
-
}, [refresh]);
|
|
79
|
+
}, [config, props.user.revenuecatId, props.expoProjectId, validationError]);
|
|
76
80
|
|
|
77
|
-
// …in the background so the badge stays current without opening the
|
|
78
|
-
// sheet, and whenever the app returns to the foreground.
|
|
79
|
-
const pollIntervalMs = props.pollIntervalMs ?? 30_000;
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
// Clamp to a sane floor so a typo like `pollIntervalMs={30}` doesn't
|
|
82
|
-
// hammer the API. 0 (or negative) disables interval polling.
|
|
83
|
-
const interval =
|
|
84
|
-
pollIntervalMs > 0
|
|
85
|
-
? setInterval(() => void refresh(), Math.max(pollIntervalMs, 5_000))
|
|
86
|
-
: null;
|
|
87
|
-
|
|
88
|
-
const appStateSub = AppState.addEventListener('change', (state) => {
|
|
89
|
-
if (state === 'active') void refresh();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
return () => {
|
|
93
|
-
if (interval !== null) clearInterval(interval);
|
|
94
|
-
appStateSub.remove();
|
|
95
|
-
};
|
|
96
|
-
}, [pollIntervalMs, refresh]);
|
|
97
|
-
|
|
98
|
-
// …and on sheet open.
|
|
99
81
|
function openSheet() {
|
|
100
82
|
setScreen({ name: 'home' });
|
|
101
83
|
setSheetOpen(true);
|
|
102
|
-
|
|
84
|
+
// The OS prompt appears right as the sheet opens; if the user declines,
|
|
85
|
+
// everything below carries on exactly as before.
|
|
86
|
+
void syncPushPermission();
|
|
103
87
|
}
|
|
104
88
|
|
|
105
89
|
function closeSheet() {
|
|
@@ -107,7 +91,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
107
91
|
setScreen({ name: 'home' });
|
|
108
92
|
}
|
|
109
93
|
|
|
110
|
-
if (validationError) {
|
|
94
|
+
if (validationError !== null) {
|
|
111
95
|
// Dev-mode: loud and actionable. Production: render nothing rather than
|
|
112
96
|
// a broken button.
|
|
113
97
|
if (__DEV__) {
|
|
@@ -130,26 +114,6 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
130
114
|
return null;
|
|
131
115
|
}
|
|
132
116
|
|
|
133
|
-
function openThread(thread: Thread) {
|
|
134
|
-
setScreen({ name: 'thread', threadId: thread.id });
|
|
135
|
-
if (thread.unread_count > 0) {
|
|
136
|
-
// Optimistically clear the badge; persist via mark-read.
|
|
137
|
-
setThreads(
|
|
138
|
-
(prev) =>
|
|
139
|
-
prev?.map((t) =>
|
|
140
|
-
t.id === thread.id ? { ...t, unread_count: 0 } : t,
|
|
141
|
-
) ?? null,
|
|
142
|
-
);
|
|
143
|
-
setTotalUnread((n) => Math.max(0, n - thread.unread_count));
|
|
144
|
-
void markRead(config, {
|
|
145
|
-
queryId: thread.id,
|
|
146
|
-
revenuecatId: props.user.revenuecatId,
|
|
147
|
-
}).catch(() => {
|
|
148
|
-
// Non-fatal: the next get-threads reconciles the badge.
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
117
|
function writeErrorMessage(e: unknown): string {
|
|
154
118
|
if (e instanceof SupportApiError) {
|
|
155
119
|
if (e.status === 429) return 'Too many requests — try again later';
|
|
@@ -171,7 +135,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
171
135
|
revenuecatId: props.user.revenuecatId,
|
|
172
136
|
appVersion: props.user.appVersion,
|
|
173
137
|
});
|
|
174
|
-
void
|
|
138
|
+
void reload();
|
|
175
139
|
return null;
|
|
176
140
|
} catch (e) {
|
|
177
141
|
return writeErrorMessage(e);
|
|
@@ -188,84 +152,48 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
188
152
|
revenuecatId: props.user.revenuecatId,
|
|
189
153
|
message,
|
|
190
154
|
});
|
|
191
|
-
await
|
|
155
|
+
await reload();
|
|
192
156
|
return null;
|
|
193
157
|
} catch (e) {
|
|
194
158
|
return writeErrorMessage(e);
|
|
195
159
|
}
|
|
196
160
|
}
|
|
197
161
|
|
|
198
|
-
const activeThread =
|
|
162
|
+
const activeThread: Thread | null =
|
|
199
163
|
screen.name === 'thread'
|
|
200
|
-
? (threads ?? []).find((t) => t.id === screen.threadId) ?? null
|
|
164
|
+
? ((threads ?? []).find((t) => t.id === screen.threadId) ?? null)
|
|
201
165
|
: null;
|
|
202
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
|
+
|
|
203
183
|
return (
|
|
204
184
|
<>
|
|
205
|
-
<
|
|
185
|
+
<LauncherButton
|
|
186
|
+
theme={theme}
|
|
187
|
+
unread={totalUnread}
|
|
188
|
+
variant={props.variant}
|
|
189
|
+
size={props.size}
|
|
190
|
+
label={props.label}
|
|
191
|
+
style={props.style}
|
|
206
192
|
onPress={openSheet}
|
|
207
|
-
|
|
208
|
-
accessibilityLabel={
|
|
209
|
-
totalUnread > 0
|
|
210
|
-
? `Support, ${totalUnread} unread ${totalUnread === 1 ? 'reply' : 'replies'}`
|
|
211
|
-
: 'Support'
|
|
212
|
-
}
|
|
213
|
-
style={({ pressed }) => ({
|
|
214
|
-
width: 44,
|
|
215
|
-
height: 44,
|
|
216
|
-
borderRadius: 22,
|
|
217
|
-
borderWidth: 1,
|
|
218
|
-
borderColor: theme.colors.border,
|
|
219
|
-
backgroundColor: pressed
|
|
220
|
-
? theme.colors.surfaceHover
|
|
221
|
-
: theme.colors.surface,
|
|
222
|
-
alignItems: 'center',
|
|
223
|
-
justifyContent: 'center',
|
|
224
|
-
})}
|
|
225
|
-
>
|
|
226
|
-
<BubbleMark color={theme.colors.textPrimary} size={18} />
|
|
227
|
-
{totalUnread > 0 && (
|
|
228
|
-
<View
|
|
229
|
-
style={{
|
|
230
|
-
position: 'absolute',
|
|
231
|
-
top: -3,
|
|
232
|
-
right: -3,
|
|
233
|
-
minWidth: 16,
|
|
234
|
-
height: 16,
|
|
235
|
-
borderRadius: 8,
|
|
236
|
-
paddingHorizontal: 4,
|
|
237
|
-
backgroundColor: theme.colors.accent,
|
|
238
|
-
alignItems: 'center',
|
|
239
|
-
justifyContent: 'center',
|
|
240
|
-
}}
|
|
241
|
-
>
|
|
242
|
-
<Text
|
|
243
|
-
style={{
|
|
244
|
-
fontSize: 10,
|
|
245
|
-
fontWeight: '600',
|
|
246
|
-
color: theme.colors.accentForeground,
|
|
247
|
-
fontVariant: ['tabular-nums'],
|
|
248
|
-
}}
|
|
249
|
-
>
|
|
250
|
-
{totalUnread > 9 ? '9+' : totalUnread}
|
|
251
|
-
</Text>
|
|
252
|
-
</View>
|
|
253
|
-
)}
|
|
254
|
-
</Pressable>
|
|
193
|
+
/>
|
|
255
194
|
|
|
256
195
|
<BottomSheet visible={sheetOpen} onClose={closeSheet} theme={theme}>
|
|
257
|
-
{screen.name === 'home' &&
|
|
258
|
-
<HomeScreen
|
|
259
|
-
theme={theme}
|
|
260
|
-
threads={threads}
|
|
261
|
-
loading={loading}
|
|
262
|
-
error={error}
|
|
263
|
-
onRetry={() => void refresh()}
|
|
264
|
-
onCompose={(type) => setScreen({ name: 'compose', type })}
|
|
265
|
-
onOpenThread={openThread}
|
|
266
|
-
onClose={closeSheet}
|
|
267
|
-
/>
|
|
268
|
-
)}
|
|
196
|
+
{screen.name === 'home' && home}
|
|
269
197
|
{screen.name === 'compose' && (
|
|
270
198
|
<ComposeScreen
|
|
271
199
|
theme={theme}
|
|
@@ -275,7 +203,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
275
203
|
/>
|
|
276
204
|
)}
|
|
277
205
|
{screen.name === 'thread' &&
|
|
278
|
-
(activeThread ? (
|
|
206
|
+
(activeThread !== null ? (
|
|
279
207
|
<ThreadScreen
|
|
280
208
|
theme={theme}
|
|
281
209
|
thread={activeThread}
|
|
@@ -283,16 +211,7 @@ export function SupportButton(props: SupportButtonProps) {
|
|
|
283
211
|
onSend={(message) => handleSend(activeThread.id, message)}
|
|
284
212
|
/>
|
|
285
213
|
) : (
|
|
286
|
-
|
|
287
|
-
theme={theme}
|
|
288
|
-
threads={threads}
|
|
289
|
-
loading={loading}
|
|
290
|
-
error={error}
|
|
291
|
-
onRetry={() => void refresh()}
|
|
292
|
-
onCompose={(type) => setScreen({ name: 'compose', type })}
|
|
293
|
-
onOpenThread={openThread}
|
|
294
|
-
onClose={closeSheet}
|
|
295
|
-
/>
|
|
214
|
+
home
|
|
296
215
|
))}
|
|
297
216
|
</BottomSheet>
|
|
298
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>
|