mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
Merge pull request #38 from koolninad/fix/camera-prompt-and-drawer-first-open
fix: camera pre-prompt wording (App Store 5.1.1(iv)) and drawer first open
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { View, Text, Pressable, StyleSheet, Modal, Platform } from 'react-native';
|
||||
import { View, Text, Pressable, StyleSheet, Modal, Platform, Linking } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { CameraView, useCameraPermissions, type BarcodeScanningResult } from 'expo-camera';
|
||||
import { X } from 'lucide-react-native';
|
||||
@@ -23,6 +23,8 @@ export function QrScanModal({ visible, onClose, onScanned }: QrScanModalProps) {
|
||||
// Guards against the camera firing onBarcodeScanned dozens of times for the
|
||||
// same code before the modal tears down.
|
||||
const handled = React.useRef(false);
|
||||
// Once iOS/Android stops letting us re-prompt, the only path left is Settings.
|
||||
const blocked = !!permission && !permission.granted && !permission.canAskAgain;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) handled.current = false;
|
||||
@@ -82,12 +84,16 @@ export function QrScanModal({ visible, onClose, onScanned }: QrScanModalProps) {
|
||||
{!permission?.granted ? (
|
||||
<View style={styles.permissionWrap}>
|
||||
<Text style={styles.permissionText}>
|
||||
{permission && !permission.canAskAgain
|
||||
? 'Camera access is disabled. Enable it in Settings to scan a sign-in QR code.'
|
||||
: 'Bulwark Mail needs camera access to scan a sign-in QR code.'}
|
||||
{blocked
|
||||
? 'Camera access is turned off. Turn it on in Settings to scan a sign-in QR code.'
|
||||
: 'Scanning a sign-in QR code uses the camera. The QR code is read on this device only.'}
|
||||
</Text>
|
||||
<Button variant="default" size="md" onPress={() => void requestPermission()}>
|
||||
Allow camera access
|
||||
<Button
|
||||
variant="default"
|
||||
size="md"
|
||||
onPress={() => void (blocked ? Linking.openSettings() : requestPermission())}
|
||||
>
|
||||
{blocked ? 'Open Settings' : 'Continue'}
|
||||
</Button>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -234,19 +234,28 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
const openDuration = useAnimDuration(240);
|
||||
const closeDuration = useAnimDuration(200);
|
||||
|
||||
// Runs the slide-in. Kicked from both the visible effect and the Modal's
|
||||
// onShow: on the very first open the modal's native view is not attached yet
|
||||
// when the effect fires, so that first animation is dropped and the drawer
|
||||
// stays parked off-screen. Re-running it once the modal is on screen commits
|
||||
// the final offset (a no-op on every subsequent open).
|
||||
const runOpen = React.useCallback(() => {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideX, { toValue: 0, duration: openDuration, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
|
||||
Animated.timing(overlayOpacity, { toValue: 1, duration: openDuration, useNativeDriver: true }),
|
||||
]).start();
|
||||
}, [slideX, overlayOpacity, openDuration]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideX, { toValue: 0, duration: openDuration, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
|
||||
Animated.timing(overlayOpacity, { toValue: 1, duration: openDuration, useNativeDriver: true }),
|
||||
]).start();
|
||||
runOpen();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideX, { toValue: -Dimensions.get('window').width, duration: closeDuration, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
|
||||
Animated.timing(overlayOpacity, { toValue: 0, duration: closeDuration, useNativeDriver: true }),
|
||||
]).start();
|
||||
}
|
||||
}, [visible, slideX, overlayOpacity, openDuration, closeDuration]);
|
||||
}, [visible, runOpen, slideX, overlayOpacity, closeDuration]);
|
||||
|
||||
const accountEmail = username || '';
|
||||
const initials = React.useMemo(
|
||||
@@ -274,6 +283,7 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
||||
animationType="none"
|
||||
statusBarTranslucent
|
||||
onRequestClose={onClose}
|
||||
onShow={runOpen}
|
||||
>
|
||||
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
|
||||
<Pressable style={styles.overlayPress} onPress={onClose} />
|
||||
|
||||
@@ -54,21 +54,28 @@ export function CalendarSidebarDrawer({
|
||||
const closeDuration = useAnimDuration(200);
|
||||
const [expandedId, setExpandedId] = React.useState<string | null>(null);
|
||||
|
||||
// Also kicked from the Modal's onShow — the first open fires this effect
|
||||
// before the modal's native view exists, so that animation is dropped and
|
||||
// the drawer stays parked off-screen until the second open.
|
||||
const runOpen = React.useCallback(() => {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideX, {
|
||||
toValue: 0,
|
||||
duration: openDuration,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(overlayOpacity, {
|
||||
toValue: 1,
|
||||
duration: openDuration,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}, [slideX, overlayOpacity, openDuration]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideX, {
|
||||
toValue: 0,
|
||||
duration: openDuration,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(overlayOpacity, {
|
||||
toValue: 1,
|
||||
duration: openDuration,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
runOpen();
|
||||
} else {
|
||||
setExpandedId(null);
|
||||
Animated.parallel([
|
||||
@@ -85,7 +92,7 @@ export function CalendarSidebarDrawer({
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
}, [visible, slideX, overlayOpacity, openDuration, closeDuration]);
|
||||
}, [visible, runOpen, slideX, overlayOpacity, closeDuration]);
|
||||
|
||||
const hiddenSet = React.useMemo(() => new Set(hiddenCalendarIds), [hiddenCalendarIds]);
|
||||
|
||||
@@ -114,6 +121,7 @@ export function CalendarSidebarDrawer({
|
||||
animationType="none"
|
||||
statusBarTranslucent
|
||||
onRequestClose={onClose}
|
||||
onShow={runOpen}
|
||||
>
|
||||
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
|
||||
<Pressable style={styles.overlayPress} onPress={onClose} />
|
||||
|
||||
@@ -67,19 +67,26 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
const slideX = React.useRef(new Animated.Value(-Dimensions.get('window').width)).current;
|
||||
const overlay = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
// Also kicked from the Modal's onShow — the first open fires this effect
|
||||
// before the modal's native view exists, so that animation is dropped and
|
||||
// the drawer stays parked off-screen until the second open.
|
||||
const runOpen = React.useCallback(() => {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideX, { toValue: 0, duration: 240, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
|
||||
Animated.timing(overlay, { toValue: 1, duration: 240, useNativeDriver: true }),
|
||||
]).start();
|
||||
}, [slideX, overlay]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideX, { toValue: 0, duration: 240, easing: Easing.out(Easing.cubic), useNativeDriver: true }),
|
||||
Animated.timing(overlay, { toValue: 1, duration: 240, useNativeDriver: true }),
|
||||
]).start();
|
||||
runOpen();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(slideX, { toValue: -Dimensions.get('window').width, duration: 200, easing: Easing.in(Easing.cubic), useNativeDriver: true }),
|
||||
Animated.timing(overlay, { toValue: 0, duration: 200, useNativeDriver: true }),
|
||||
]).start();
|
||||
}
|
||||
}, [visible, slideX, overlay]);
|
||||
}, [visible, runOpen, slideX, overlay]);
|
||||
|
||||
const select = (cat: ContactCategory) => {
|
||||
setSelectedCategory(cat);
|
||||
@@ -87,7 +94,7 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="none" statusBarTranslucent onRequestClose={onClose}>
|
||||
<Modal visible={visible} transparent animationType="none" statusBarTranslucent onRequestClose={onClose} onShow={runOpen}>
|
||||
<Animated.View style={[styles.overlay, { opacity: overlay }]}>
|
||||
<Pressable style={styles.overlayPress} onPress={onClose} />
|
||||
</Animated.View>
|
||||
|
||||
Reference in New Issue
Block a user