turbodesk-livechat-react-native 0.1.0-alpha.3 → 0.1.0-alpha.30
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/CHANGELOG.md +50 -0
- package/dist/api/conversation-api.d.ts +9 -0
- package/dist/api/conversation-api.d.ts.map +1 -1
- package/dist/api/conversation-api.js +15 -1
- package/dist/api/conversation-api.js.map +1 -1
- package/dist/hooks/use-live-chat.d.ts +1 -0
- package/dist/hooks/use-live-chat.d.ts.map +1 -1
- package/dist/hooks/use-live-chat.js +2 -0
- package/dist/hooks/use-live-chat.js.map +1 -1
- package/dist/hooks/use-send-message.d.ts +1 -0
- package/dist/hooks/use-send-message.d.ts.map +1 -1
- package/dist/hooks/use-send-message.js +20 -14
- package/dist/hooks/use-send-message.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/navigation/LiveChatPanel.d.ts.map +1 -1
- package/dist/navigation/LiveChatPanel.js +20 -1
- package/dist/navigation/LiveChatPanel.js.map +1 -1
- package/dist/provider/LiveChatContext.d.ts.map +1 -1
- package/dist/provider/LiveChatContext.js +2 -0
- package/dist/provider/LiveChatContext.js.map +1 -1
- package/dist/provider/LiveChatProvider.d.ts +1 -1
- package/dist/provider/LiveChatProvider.d.ts.map +1 -1
- package/dist/provider/LiveChatProvider.js +17 -2
- package/dist/provider/LiveChatProvider.js.map +1 -1
- package/dist/provider/types.d.ts +4 -0
- package/dist/provider/types.d.ts.map +1 -1
- package/dist/ui/components/ConversationHeader.d.ts.map +1 -1
- package/dist/ui/components/ConversationHeader.js +7 -4
- package/dist/ui/components/ConversationHeader.js.map +1 -1
- package/dist/ui/components/ConversationListScreen.d.ts.map +1 -1
- package/dist/ui/components/ConversationListScreen.js +11 -2
- package/dist/ui/components/ConversationListScreen.js.map +1 -1
- package/dist/ui/components/ConversationScreen.d.ts.map +1 -1
- package/dist/ui/components/ConversationScreen.js +13 -3
- package/dist/ui/components/ConversationScreen.js.map +1 -1
- package/dist/ui/components/HomeScreen.d.ts.map +1 -1
- package/dist/ui/components/HomeScreen.js +14 -4
- package/dist/ui/components/HomeScreen.js.map +1 -1
- package/dist/ui/components/MessageComposer.d.ts +1 -0
- package/dist/ui/components/MessageComposer.d.ts.map +1 -1
- package/dist/ui/components/MessageComposer.js +89 -30
- package/dist/ui/components/MessageComposer.js.map +1 -1
- package/dist/ui/safe-area.d.ts +9 -0
- package/dist/ui/safe-area.d.ts.map +1 -0
- package/dist/ui/safe-area.js +28 -0
- package/dist/ui/safe-area.js.map +1 -0
- package/package.json +11 -3
- package/src/api/conversation-api.ts +33 -8
- package/src/hooks/use-live-chat.ts +3 -0
- package/src/hooks/use-send-message.ts +169 -159
- package/src/index.ts +3 -0
- package/src/navigation/LiveChatPanel.tsx +26 -3
- package/src/provider/LiveChatContext.ts +2 -0
- package/src/provider/LiveChatProvider.tsx +396 -380
- package/src/provider/types.ts +63 -57
- package/src/ui/components/ConversationHeader.tsx +7 -6
- package/src/ui/components/ConversationListScreen.tsx +18 -5
- package/src/ui/components/ConversationScreen.tsx +369 -362
- package/src/ui/components/HomeScreen.tsx +21 -6
- package/src/ui/components/MessageComposer.tsx +116 -36
- package/src/ui/safe-area.ts +34 -0
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getWidgetName,
|
|
23
23
|
} from "../theme";
|
|
24
24
|
import { HistoryChatsIcon, SendPlaneIcon } from "../icons";
|
|
25
|
+
import { usePanelSafeAreaInsets } from "../safe-area";
|
|
25
26
|
|
|
26
27
|
let LinearGradient: any = null;
|
|
27
28
|
try {
|
|
@@ -95,6 +96,7 @@ export function HomeScreen({
|
|
|
95
96
|
visitorQueryParams,
|
|
96
97
|
embedLoadState,
|
|
97
98
|
} = useLiveChatContext();
|
|
99
|
+
const { top: safeTop, bottom: safeBottom } = usePanelSafeAreaInsets();
|
|
98
100
|
|
|
99
101
|
const [conversationCount, setConversationCount] = useState(0);
|
|
100
102
|
|
|
@@ -133,7 +135,7 @@ export function HomeScreen({
|
|
|
133
135
|
style={styles.flex}
|
|
134
136
|
>
|
|
135
137
|
{/* top — branding */}
|
|
136
|
-
<View style={styles.header}>
|
|
138
|
+
<View style={[styles.header, { paddingTop: safeTop + 20 }]}>
|
|
137
139
|
{logoUrl ? (
|
|
138
140
|
<Image
|
|
139
141
|
source={{ uri: logoUrl }}
|
|
@@ -143,11 +145,17 @@ export function HomeScreen({
|
|
|
143
145
|
) : (
|
|
144
146
|
<Avatar name={widgetName} size={44} />
|
|
145
147
|
)}
|
|
146
|
-
<Text
|
|
148
|
+
<Text
|
|
149
|
+
style={[styles.headerTitle, { color: contrastText }]}
|
|
150
|
+
numberOfLines={2}
|
|
151
|
+
>
|
|
147
152
|
{title}
|
|
148
153
|
</Text>
|
|
149
154
|
<Text
|
|
150
|
-
style={[
|
|
155
|
+
style={[
|
|
156
|
+
styles.headerSubtitle,
|
|
157
|
+
{ color: contrastText, opacity: 0.82 },
|
|
158
|
+
]}
|
|
151
159
|
numberOfLines={3}
|
|
152
160
|
>
|
|
153
161
|
{description}
|
|
@@ -182,7 +190,10 @@ export function HomeScreen({
|
|
|
182
190
|
Previous chats
|
|
183
191
|
{conversationCount > 1 ? (
|
|
184
192
|
<Text
|
|
185
|
-
style={{
|
|
193
|
+
style={{
|
|
194
|
+
fontWeight: "400",
|
|
195
|
+
color: "rgba(255,255,255,0.7)",
|
|
196
|
+
}}
|
|
186
197
|
>
|
|
187
198
|
{" "}
|
|
188
199
|
({conversationCount})
|
|
@@ -219,7 +230,12 @@ export function HomeScreen({
|
|
|
219
230
|
</View>
|
|
220
231
|
</View>
|
|
221
232
|
|
|
222
|
-
<Text
|
|
233
|
+
<Text
|
|
234
|
+
style={[
|
|
235
|
+
styles.poweredBy,
|
|
236
|
+
{ color: "rgba(255,255,255,0.7)", paddingBottom: 8 + safeBottom },
|
|
237
|
+
]}
|
|
238
|
+
>
|
|
223
239
|
{POWERED_BY_TEXT}
|
|
224
240
|
</Text>
|
|
225
241
|
</GradientBackground>
|
|
@@ -231,7 +247,6 @@ const styles = StyleSheet.create({
|
|
|
231
247
|
header: {
|
|
232
248
|
flex: 0.3,
|
|
233
249
|
paddingHorizontal: 20,
|
|
234
|
-
paddingTop: 52,
|
|
235
250
|
paddingBottom: 32,
|
|
236
251
|
gap: 12,
|
|
237
252
|
justifyContent: "flex-start",
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { useLiveChatContext } from "../../provider/LiveChatContext";
|
|
14
14
|
import { fileApi } from "../../api/file-api";
|
|
15
15
|
import { SendPlaneIcon, PaperclipIcon, CloseIcon } from "../icons";
|
|
16
|
+
import { usePanelSafeAreaInsets } from "../safe-area";
|
|
16
17
|
|
|
17
18
|
// ── types ─────────────────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -41,6 +42,7 @@ export type PendingAttachment = {
|
|
|
41
42
|
uri: string;
|
|
42
43
|
name: string;
|
|
43
44
|
mimeType: string;
|
|
45
|
+
size?: number;
|
|
44
46
|
kind: PickedAttachmentKind;
|
|
45
47
|
};
|
|
46
48
|
|
|
@@ -115,8 +117,8 @@ function AttachmentMenu({ onPick, disabled, attachmentCount, color }: Attachment
|
|
|
115
117
|
});
|
|
116
118
|
});
|
|
117
119
|
} else {
|
|
118
|
-
// audio / document — use react-native-
|
|
119
|
-
const DocumentPicker = require("react-native-
|
|
120
|
+
// audio / document — use @react-native-documents/picker
|
|
121
|
+
const DocumentPicker = require("@react-native-documents/picker");
|
|
120
122
|
const types = kind === "audio"
|
|
121
123
|
? [DocumentPicker.types.audio]
|
|
122
124
|
: [DocumentPicker.types.pdf, DocumentPicker.types.plainText, DocumentPicker.types.doc, DocumentPicker.types.docx];
|
|
@@ -135,7 +137,7 @@ function AttachmentMenu({ onPick, disabled, attachmentCount, color }: Attachment
|
|
|
135
137
|
|
|
136
138
|
if (picked) onPick(picked);
|
|
137
139
|
} catch (e: any) {
|
|
138
|
-
if (e?.code !== "DOCUMENT_PICKER_CANCELED") {
|
|
140
|
+
if (e?.code !== "DOCUMENT_PICKER_CANCELED" && e?.code !== "OPERATION_CANCELED") {
|
|
139
141
|
Alert.alert("Error", "Could not pick file.");
|
|
140
142
|
}
|
|
141
143
|
}
|
|
@@ -223,42 +225,61 @@ function FilePreviews({
|
|
|
223
225
|
|
|
224
226
|
// ── upload helper ─────────────────────────────────────────────────────────────
|
|
225
227
|
|
|
228
|
+
// Throws on any failure so the caller can surface the error and preserve the draft.
|
|
226
229
|
async function uploadAttachment(
|
|
227
230
|
pending: PendingAttachment,
|
|
228
231
|
visitorQueryParams: Record<string, string> | null
|
|
229
|
-
): Promise<UploadedAttachment
|
|
232
|
+
): Promise<UploadedAttachment> {
|
|
233
|
+
const ext = pending.name.split(".").pop() ?? "";
|
|
234
|
+
let presigned: any;
|
|
230
235
|
try {
|
|
231
|
-
|
|
232
|
-
const presigned = await fileApi.getFileUrl(
|
|
236
|
+
presigned = await fileApi.getFileUrl(
|
|
233
237
|
{
|
|
234
238
|
operation: "putObject",
|
|
235
239
|
name: pending.name,
|
|
236
240
|
type: pending.mimeType,
|
|
237
241
|
extension: ext,
|
|
238
|
-
|
|
242
|
+
bytes: pending.size,
|
|
243
|
+
folder: "livechat",
|
|
239
244
|
},
|
|
240
245
|
visitorQueryParams ? { params: visitorQueryParams } : undefined
|
|
241
|
-
)
|
|
246
|
+
);
|
|
247
|
+
} catch {
|
|
248
|
+
throw new Error(`Failed to prepare upload for "${pending.name}"`);
|
|
249
|
+
}
|
|
242
250
|
|
|
243
|
-
|
|
251
|
+
const signedUrl = presigned?.data?.signedUrl;
|
|
252
|
+
if (!signedUrl) throw new Error(`No upload URL returned for "${pending.name}"`);
|
|
253
|
+
|
|
254
|
+
let putRes: Response;
|
|
255
|
+
try {
|
|
256
|
+
putRes = await fetch(signedUrl, {
|
|
244
257
|
method: "PUT",
|
|
245
258
|
headers: { "Content-Type": pending.mimeType },
|
|
246
259
|
body: { uri: pending.uri, type: pending.mimeType, name: pending.name } as any,
|
|
247
260
|
});
|
|
248
|
-
|
|
249
|
-
const uploaded = presigned?.data?.file;
|
|
250
|
-
return {
|
|
251
|
-
id: pending.id,
|
|
252
|
-
fileId: uploaded._id,
|
|
253
|
-
fileUrl: uploaded.url,
|
|
254
|
-
fileName: uploaded.name,
|
|
255
|
-
fileExtension: uploaded.extension,
|
|
256
|
-
fileType: pending.mimeType,
|
|
257
|
-
type: pending.kind,
|
|
258
|
-
};
|
|
259
261
|
} catch {
|
|
260
|
-
|
|
262
|
+
throw new Error(`Network error while uploading "${pending.name}"`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!putRes.ok) {
|
|
266
|
+
throw new Error(`Upload failed for "${pending.name}" (HTTP ${putRes.status})`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const uploaded = presigned?.data?.file;
|
|
270
|
+
if (!uploaded?._id) {
|
|
271
|
+
throw new Error(`Upload succeeded but server returned no file metadata for "${pending.name}"`);
|
|
261
272
|
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
id: pending.id,
|
|
276
|
+
fileId: uploaded._id,
|
|
277
|
+
fileUrl: uploaded.url,
|
|
278
|
+
fileName: uploaded.name,
|
|
279
|
+
fileExtension: uploaded.extension,
|
|
280
|
+
fileType: pending.mimeType,
|
|
281
|
+
type: pending.kind,
|
|
282
|
+
};
|
|
262
283
|
}
|
|
263
284
|
|
|
264
285
|
// ── main composer ─────────────────────────────────────────────────────────────
|
|
@@ -269,18 +290,26 @@ export function MessageComposer({
|
|
|
269
290
|
placeholder = "Message…",
|
|
270
291
|
}: MessageComposerProps) {
|
|
271
292
|
const { theme: t, visitorQueryParams } = useLiveChatContext();
|
|
293
|
+
const { bottom: safeBottom } = usePanelSafeAreaInsets();
|
|
272
294
|
const [text, setText] = useState("");
|
|
273
295
|
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
|
|
274
296
|
const [isSending, setIsSending] = useState(false);
|
|
297
|
+
const [sendError, setSendError] = useState<string | null>(null);
|
|
275
298
|
const inputRef = useRef<TextInput>(null);
|
|
276
299
|
|
|
300
|
+
const isConnecting = !visitorQueryParams;
|
|
277
301
|
const trimmed = text.trim();
|
|
278
|
-
const canSend = (trimmed.length > 0 || attachments.length > 0) && !disabled && !isSending;
|
|
302
|
+
const canSend = (trimmed.length > 0 || attachments.length > 0) && !disabled && !isSending && !isConnecting;
|
|
303
|
+
|
|
304
|
+
// Clear stale error when the user edits the message
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (sendError && (text || attachments.length)) setSendError(null);
|
|
307
|
+
}, [text, attachments.length]);
|
|
279
308
|
|
|
280
309
|
const handlePick = useCallback((item: PickedAttachment) => {
|
|
281
310
|
setAttachments((prev) => {
|
|
282
311
|
if (prev.length >= MAX_ATTACHMENTS) return prev;
|
|
283
|
-
return [...prev, { id: newId(), uri: item.uri, name: item.name, mimeType: item.type, kind: item.kind }];
|
|
312
|
+
return [...prev, { id: newId(), uri: item.uri, name: item.name, mimeType: item.type, size: item.size, kind: item.kind }];
|
|
284
313
|
});
|
|
285
314
|
}, []);
|
|
286
315
|
|
|
@@ -290,26 +319,61 @@ export function MessageComposer({
|
|
|
290
319
|
|
|
291
320
|
const handleSend = useCallback(async () => {
|
|
292
321
|
if (!canSend) return;
|
|
322
|
+
|
|
293
323
|
setIsSending(true);
|
|
294
|
-
|
|
324
|
+
setSendError(null);
|
|
325
|
+
|
|
326
|
+
// Capture current values — text/attachments must NOT be cleared until send succeeds
|
|
295
327
|
const sendText = text.trim();
|
|
328
|
+
const snapshot = [...attachments];
|
|
329
|
+
|
|
296
330
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
300
|
-
|
|
331
|
+
let uploaded: UploadedAttachment[] = [];
|
|
332
|
+
|
|
333
|
+
if (snapshot.length > 0) {
|
|
334
|
+
// Upload all attachments — throws on the first failure, preserving draft
|
|
335
|
+
uploaded = await Promise.all(
|
|
336
|
+
snapshot.map((a) => uploadAttachment(a, visitorQueryParams))
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// onSend must throw on failure (ConversationScreen propagates throws from useSendMessage)
|
|
301
341
|
await onSend(sendText, uploaded);
|
|
342
|
+
|
|
343
|
+
// Only reached on confirmed success — safe to clear
|
|
302
344
|
setText("");
|
|
303
345
|
setAttachments([]);
|
|
304
|
-
|
|
305
|
-
|
|
346
|
+
setSendError(null);
|
|
347
|
+
} catch (e: any) {
|
|
348
|
+
// Draft preserved — text and attachments state untouched
|
|
349
|
+
setSendError(e?.message ?? "Failed to send. Please try again.");
|
|
306
350
|
} finally {
|
|
307
351
|
setIsSending(false);
|
|
308
352
|
}
|
|
309
353
|
}, [canSend, text, attachments, onSend, visitorQueryParams]);
|
|
310
354
|
|
|
311
355
|
return (
|
|
312
|
-
<View
|
|
356
|
+
<View
|
|
357
|
+
style={[
|
|
358
|
+
styles.container,
|
|
359
|
+
{
|
|
360
|
+
backgroundColor: t.colors.composerBackground,
|
|
361
|
+
borderTopColor: t.colors.composerBorder,
|
|
362
|
+
paddingBottom: 8 + safeBottom,
|
|
363
|
+
},
|
|
364
|
+
]}
|
|
365
|
+
>
|
|
366
|
+
{sendError ? (
|
|
367
|
+
<View style={[styles.errorBanner, { backgroundColor: t.colors.surface, borderColor: t.colors.border }]}>
|
|
368
|
+
<Text style={[styles.errorText, { color: t.colors.error ?? "#c0392b" }]} numberOfLines={2}>
|
|
369
|
+
{sendError}
|
|
370
|
+
</Text>
|
|
371
|
+
<TouchableOpacity onPress={() => setSendError(null)} accessibilityLabel="Dismiss error">
|
|
372
|
+
<CloseIcon size={14} color={t.colors.textMuted} />
|
|
373
|
+
</TouchableOpacity>
|
|
374
|
+
</View>
|
|
375
|
+
) : null}
|
|
376
|
+
|
|
313
377
|
<View style={[styles.inner, { borderColor: t.colors.composerBorder }]}>
|
|
314
378
|
<FilePreviews items={attachments} onRemove={handleRemove} borderColor={t.colors.border} />
|
|
315
379
|
|
|
@@ -318,11 +382,11 @@ export function MessageComposer({
|
|
|
318
382
|
style={[styles.input, { color: t.colors.inputText, fontSize: t.fontSizes.md }]}
|
|
319
383
|
value={text}
|
|
320
384
|
onChangeText={setText}
|
|
321
|
-
placeholder={placeholder}
|
|
385
|
+
placeholder={isConnecting ? "Connecting…" : placeholder}
|
|
322
386
|
placeholderTextColor={t.colors.textMuted}
|
|
323
387
|
multiline
|
|
324
388
|
maxLength={4000}
|
|
325
|
-
editable={!disabled}
|
|
389
|
+
editable={!disabled && !isConnecting}
|
|
326
390
|
returnKeyType="default"
|
|
327
391
|
blurOnSubmit={false}
|
|
328
392
|
/>
|
|
@@ -330,7 +394,7 @@ export function MessageComposer({
|
|
|
330
394
|
<View style={styles.toolbar}>
|
|
331
395
|
<AttachmentMenu
|
|
332
396
|
onPick={handlePick}
|
|
333
|
-
disabled={disabled || isSending}
|
|
397
|
+
disabled={disabled || isSending || isConnecting}
|
|
334
398
|
attachmentCount={attachments.length}
|
|
335
399
|
color={t.colors.textMuted}
|
|
336
400
|
/>
|
|
@@ -343,10 +407,12 @@ export function MessageComposer({
|
|
|
343
407
|
styles.sendBtn,
|
|
344
408
|
{ backgroundColor: canSend ? t.colors.sendButtonBackground : t.colors.border },
|
|
345
409
|
]}
|
|
346
|
-
accessibilityLabel="Send message"
|
|
410
|
+
accessibilityLabel={isConnecting ? "Connecting, please wait" : "Send message"}
|
|
347
411
|
>
|
|
348
412
|
{isSending ? (
|
|
349
413
|
<ActivityIndicator size="small" color={t.colors.sendButtonIcon} />
|
|
414
|
+
) : isConnecting ? (
|
|
415
|
+
<ActivityIndicator size="small" color={t.colors.textMuted} />
|
|
350
416
|
) : (
|
|
351
417
|
<SendPlaneIcon size={18} color={canSend ? t.colors.sendButtonIcon : t.colors.textMuted} />
|
|
352
418
|
)}
|
|
@@ -362,7 +428,6 @@ const styles = StyleSheet.create({
|
|
|
362
428
|
borderTopWidth: 1,
|
|
363
429
|
paddingHorizontal: 8,
|
|
364
430
|
paddingTop: 8,
|
|
365
|
-
paddingBottom: 8,
|
|
366
431
|
},
|
|
367
432
|
inner: {
|
|
368
433
|
borderWidth: 1,
|
|
@@ -371,6 +436,21 @@ const styles = StyleSheet.create({
|
|
|
371
436
|
paddingTop: 10,
|
|
372
437
|
paddingBottom: 6,
|
|
373
438
|
},
|
|
439
|
+
errorBanner: {
|
|
440
|
+
flexDirection: "row",
|
|
441
|
+
alignItems: "center",
|
|
442
|
+
justifyContent: "space-between",
|
|
443
|
+
borderWidth: 1,
|
|
444
|
+
borderRadius: 8,
|
|
445
|
+
paddingHorizontal: 12,
|
|
446
|
+
paddingVertical: 8,
|
|
447
|
+
marginBottom: 6,
|
|
448
|
+
gap: 8,
|
|
449
|
+
},
|
|
450
|
+
errorText: {
|
|
451
|
+
flex: 1,
|
|
452
|
+
fontSize: 13,
|
|
453
|
+
},
|
|
374
454
|
input: {
|
|
375
455
|
minHeight: 30,
|
|
376
456
|
maxHeight: 200,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Platform, StatusBar } from "react-native";
|
|
2
|
+
|
|
3
|
+
export type PanelSafeAreaInsets = {
|
|
4
|
+
top: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
left: number;
|
|
7
|
+
right: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function getFallbackInsets(): PanelSafeAreaInsets {
|
|
11
|
+
if (Platform.OS === "ios") {
|
|
12
|
+
// Typical notch / home-indicator insets when no SafeAreaProvider is mounted.
|
|
13
|
+
return { top: 47, bottom: 34, left: 0, right: 0 };
|
|
14
|
+
}
|
|
15
|
+
return { top: StatusBar.currentHeight ?? 0, bottom: 0, left: 0, right: 0 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type UseSafeAreaInsets = () => PanelSafeAreaInsets;
|
|
19
|
+
|
|
20
|
+
let useSafeAreaInsetsFromContext: UseSafeAreaInsets | null = null;
|
|
21
|
+
try {
|
|
22
|
+
useSafeAreaInsetsFromContext =
|
|
23
|
+
require("react-native-safe-area-context").useSafeAreaInsets;
|
|
24
|
+
} catch {
|
|
25
|
+
useSafeAreaInsetsFromContext = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Safe-area insets for panel UI. Uses react-native-safe-area-context when available. */
|
|
29
|
+
export function usePanelSafeAreaInsets(): PanelSafeAreaInsets {
|
|
30
|
+
if (useSafeAreaInsetsFromContext) {
|
|
31
|
+
return useSafeAreaInsetsFromContext();
|
|
32
|
+
}
|
|
33
|
+
return getFallbackInsets();
|
|
34
|
+
}
|