@planningcenter/chat-react-native 3.12.0-rc.1 → 3.12.0-rc.11
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/build/components/conversation/attachments/attachment_card.js +1 -0
- package/build/components/conversation/attachments/attachment_card.js.map +1 -1
- package/build/components/conversation/attachments/image_attachment.d.ts +3 -1
- package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/image_attachment.js +223 -107
- package/build/components/conversation/attachments/image_attachment.js.map +1 -1
- package/build/components/conversation/message_attachments.d.ts +1 -1
- package/build/components/conversation/message_attachments.d.ts.map +1 -1
- package/build/components/conversation/message_attachments.js +7 -13
- package/build/components/conversation/message_attachments.js.map +1 -1
- package/build/components/display/index.d.ts +1 -0
- package/build/components/display/index.d.ts.map +1 -1
- package/build/components/display/index.js +1 -0
- package/build/components/display/index.js.map +1 -1
- package/build/components/primitive/form_sheet.d.ts +7 -6
- package/build/components/primitive/form_sheet.d.ts.map +1 -1
- package/build/components/primitive/form_sheet.js +8 -8
- package/build/components/primitive/form_sheet.js.map +1 -1
- package/build/hooks/use_report_bug_action.d.ts +1 -0
- package/build/hooks/use_report_bug_action.d.ts.map +1 -1
- package/build/hooks/use_report_bug_action.js +2 -1
- package/build/hooks/use_report_bug_action.js.map +1 -1
- package/build/screens/bug_report_screen.d.ts.map +1 -1
- package/build/screens/bug_report_screen.js +216 -46
- package/build/screens/bug_report_screen.js.map +1 -1
- package/build/screens/conversation/message_read_receipts_screen.js +2 -2
- package/build/screens/conversation/message_read_receipts_screen.js.map +1 -1
- package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.js +3 -3
- package/build/screens/conversation_filter_recipients/conversation_filter_recipients_screen.js.map +1 -1
- package/build/screens/conversation_filters/components/conversation_filters.d.ts.map +1 -1
- package/build/screens/conversation_filters/components/conversation_filters.js +2 -1
- package/build/screens/conversation_filters/components/conversation_filters.js.map +1 -1
- package/build/screens/conversation_filters/group_filters.d.ts.map +1 -1
- package/build/screens/conversation_filters/group_filters.js +2 -1
- package/build/screens/conversation_filters/group_filters.js.map +1 -1
- package/build/screens/conversation_filters/team_filters.d.ts.map +1 -1
- package/build/screens/conversation_filters/team_filters.js +2 -1
- package/build/screens/conversation_filters/team_filters.js.map +1 -1
- package/build/screens/conversation_filters_screen.d.ts +1 -2
- package/build/screens/conversation_filters_screen.d.ts.map +1 -1
- package/build/screens/conversation_filters_screen.js +32 -75
- package/build/screens/conversation_filters_screen.js.map +1 -1
- package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
- package/build/screens/conversation_new/components/groups_form.js +11 -6
- package/build/screens/conversation_new/components/groups_form.js.map +1 -1
- package/build/screens/conversation_new/components/services_form.js +1 -1
- package/build/screens/conversation_new/components/services_form.js.map +1 -1
- package/package.json +2 -2
- package/src/components/conversation/attachments/attachment_card.tsx +1 -0
- package/src/components/conversation/attachments/image_attachment.tsx +393 -141
- package/src/components/conversation/message_attachments.tsx +7 -23
- package/src/components/display/index.ts +1 -0
- package/src/components/primitive/form_sheet.tsx +22 -17
- package/src/hooks/use_report_bug_action.ts +3 -0
- package/src/screens/bug_report_screen.tsx +302 -80
- package/src/screens/conversation/message_read_receipts_screen.tsx +2 -2
- package/src/screens/conversation_filter_recipients/conversation_filter_recipients_screen.tsx +3 -3
- package/src/screens/conversation_filters/components/conversation_filters.tsx +2 -1
- package/src/screens/conversation_filters/group_filters.tsx +2 -1
- package/src/screens/conversation_filters/team_filters.tsx +2 -1
- package/src/screens/conversation_filters_screen.tsx +36 -88
- package/src/screens/conversation_new/components/groups_form.tsx +12 -5
- package/src/screens/conversation_new/components/services_form.tsx +1 -1
- package/build/components/conversation/attachments/image_attachment_legacy.d.ts +0 -12
- package/build/components/conversation/attachments/image_attachment_legacy.d.ts.map +0 -1
- package/build/components/conversation/attachments/image_attachment_legacy.js +0 -142
- package/build/components/conversation/attachments/image_attachment_legacy.js.map +0 -1
- package/src/components/conversation/attachments/image_attachment_legacy.tsx +0 -258
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment_card.js","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/attachment_card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoC,MAAM,OAAO,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAA;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAIzC,MAAM,UAAU,cAAc,CAAC,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAuB;IACxE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAClC;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,IAAI,CAAC,CACR,CAAA;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,EAAE,QAAQ,EAA2B;IACvE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAC7D;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,IAAI,CAAC,CACR,CAAA;AACH,CAAC;AAED,MAAM,SAAS,GAAG,GAAG,EAAE;IACrB,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAA;IAE7B,OAAO,UAAU,CAAC,MAAM,CAAC;QACvB,IAAI,EAAE;YACJ,OAAO,EAAE,CAAC;YACV,eAAe,EAAE,MAAM,CAAC,eAAe;YACvC,YAAY,EAAE,CAAC;YACf,QAAQ,EAAE,MAAM;YAChB,SAAS,EAAE,EAAE;YACb,cAAc,EAAE,QAAQ;SACzB;QACD,KAAK,EAAE;YACL,KAAK,EAAE,MAAM,CAAC,uBAAuB;YACrC,QAAQ,EAAE,MAAM,CAAC,UAAU;
|
|
1
|
+
{"version":3,"file":"attachment_card.js","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/attachment_card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoC,MAAM,OAAO,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAA;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAIzC,MAAM,UAAU,cAAc,CAAC,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAuB;IACxE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAClC;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,IAAI,CAAC,CACR,CAAA;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,EAAE,QAAQ,EAA2B;IACvE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAC7D;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,IAAI,CAAC,CACR,CAAA;AACH,CAAC;AAED,MAAM,SAAS,GAAG,GAAG,EAAE;IACrB,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAA;IAE7B,OAAO,UAAU,CAAC,MAAM,CAAC;QACvB,IAAI,EAAE;YACJ,OAAO,EAAE,CAAC;YACV,eAAe,EAAE,MAAM,CAAC,eAAe;YACvC,YAAY,EAAE,CAAC;YACf,QAAQ,EAAE,MAAM;YAChB,SAAS,EAAE,EAAE;YACb,cAAc,EAAE,QAAQ;SACzB;QACD,KAAK,EAAE;YACL,KAAK,EAAE,MAAM,CAAC,uBAAuB;YACrC,QAAQ,EAAE,MAAM,CAAC,UAAU;YAC3B,IAAI,EAAE,CAAC;SACR;KACF,CAAC,CAAA;AACJ,CAAC,CAAA","sourcesContent":["import React, { ComponentProps, ReactNode } from 'react'\nimport { StyleSheet, View } from 'react-native'\nimport { Text } from '../../display'\nimport { tokens } from '../../../vendor/tapestry/tokens'\nimport { useTheme } from '../../../hooks'\n\ntype AttachmentCardProps = ComponentProps<typeof View>\n\nexport function AttachmentCard({ children, ...props }: AttachmentCardProps) {\n const styles = useStyles()\n\n return (\n <View style={styles.card} {...props}>\n {children}\n </View>\n )\n}\n\nexport function AttachmentCardTitle({ children }: { children: ReactNode }) {\n const styles = useStyles()\n\n return (\n <Text style={styles.title} numberOfLines={1} selectable={false}>\n {children}\n </Text>\n )\n}\n\nconst useStyles = () => {\n const { colors } = useTheme()\n\n return StyleSheet.create({\n card: {\n padding: 8,\n backgroundColor: colors.surfaceColor100,\n borderRadius: 8,\n minWidth: '100%',\n minHeight: 60,\n justifyContent: 'center',\n },\n title: {\n color: colors.textColorDefaultPrimary,\n fontSize: tokens.fontSizeSm,\n flex: 1,\n },\n })\n}\n"]}
|
|
@@ -4,8 +4,10 @@ export type MetaProps = {
|
|
|
4
4
|
authorName: string;
|
|
5
5
|
createdAt: string;
|
|
6
6
|
};
|
|
7
|
-
export declare function ImageAttachment({ attachment, metaProps, onMessageAttachmentLongPress, }: {
|
|
7
|
+
export declare function ImageAttachment({ attachment, imageAttachments, currentImageIndex, metaProps, onMessageAttachmentLongPress, }: {
|
|
8
8
|
attachment: DenormalizedMessageAttachmentResource;
|
|
9
|
+
imageAttachments: DenormalizedMessageAttachmentResource[];
|
|
10
|
+
currentImageIndex: number;
|
|
9
11
|
metaProps: MetaProps;
|
|
10
12
|
onMessageAttachmentLongPress: (attachment: DenormalizedMessageAttachmentResource) => void;
|
|
11
13
|
}): React.JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image_attachment.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/image_attachment.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"image_attachment.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation/attachments/image_attachment.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAA;AAkCvF,OAAO,EAAE,qCAAqC,EAAE,MAAM,2DAA2D,CAAA;AAwBjH,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,4BAA4B,GAC7B,EAAE;IACD,UAAU,EAAE,qCAAqC,CAAA;IACjD,gBAAgB,EAAE,qCAAqC,EAAE,CAAA;IACzD,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,SAAS,CAAA;IACpB,4BAA4B,EAAE,CAAC,UAAU,EAAE,qCAAqC,KAAK,IAAI,CAAA;CAC1F,qBAyCA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react';
|
|
2
|
-
import { StatusBar, StyleSheet, Modal, View, Linking, Dimensions, Platform } from 'react-native';
|
|
1
|
+
import React, { useMemo, useState, useCallback } from 'react';
|
|
2
|
+
import { StatusBar, StyleSheet, Modal, View, Linking, Dimensions, Platform, } from 'react-native';
|
|
3
3
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
-
import { Gesture, GestureDetector, GestureHandlerRootView, Pressable, } from 'react-native-gesture-handler';
|
|
4
|
+
import { FlatList, Gesture, GestureDetector, GestureHandlerRootView, Pressable, } from 'react-native-gesture-handler';
|
|
5
5
|
import Animated, { runOnJS, useAnimatedStyle, useAnimatedReaction, useSharedValue, withSpring, withDecay, } from 'react-native-reanimated';
|
|
6
6
|
import { tokens } from '../../../vendor/tapestry/tokens';
|
|
7
7
|
import { IconButton, Image, Heading, Text } from '../../display';
|
|
@@ -9,11 +9,15 @@ import colorFunction from 'color';
|
|
|
9
9
|
import { formatDatePreview } from '../../../utils/date';
|
|
10
10
|
import { PlatformPressable } from '@react-navigation/elements';
|
|
11
11
|
import { useTheme } from '../../../hooks';
|
|
12
|
+
import { platformFontWeightMedium } from '../../../utils';
|
|
12
13
|
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window');
|
|
13
|
-
const DISMISS_PAN_THRESHOLD =
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const DISMISS_PAN_THRESHOLD = 250;
|
|
15
|
+
const MIN_DISTANCE_FOR_PAN = 10; // Higher threshold gives pinching priority
|
|
16
|
+
const SINGLE_FINGER_POINTER = 1; // Single-finger panning / tapping helps to avoid conflicts with pinching
|
|
17
|
+
const MAX_TRAVEL_DISTANCE_FOR_DOUBLE_TAP_PLATFORM = Platform.OS === 'ios' ? 8 : 0; // Causes taps to be unreliable on Android
|
|
16
18
|
const DEFAULT_OPACITY = 1;
|
|
19
|
+
const DEFAULT_TRANSLATE_X = 0;
|
|
20
|
+
const DEFAULT_TRANSLATE_Y = 0;
|
|
17
21
|
const DEFAULT_SCALE = 1;
|
|
18
22
|
const MIN_SCALE = 0.5;
|
|
19
23
|
const MAX_SCALE = 5;
|
|
@@ -24,23 +28,34 @@ const RESET_SPRING_CONFIG = {
|
|
|
24
28
|
damping: 20,
|
|
25
29
|
stiffness: 150,
|
|
26
30
|
};
|
|
27
|
-
export function ImageAttachment({ attachment, metaProps, onMessageAttachmentLongPress, }) {
|
|
31
|
+
export function ImageAttachment({ attachment, imageAttachments, currentImageIndex, metaProps, onMessageAttachmentLongPress, }) {
|
|
28
32
|
const { attributes } = attachment;
|
|
29
33
|
const { url, urlMedium, filename, metadata = {} } = attributes;
|
|
30
34
|
const { colors } = useTheme();
|
|
31
35
|
const styles = useStyles({ imageWidth: metadata.width, imageHeight: metadata.height });
|
|
32
36
|
const [visible, setVisible] = useState(false);
|
|
37
|
+
// Force modal to remount with fresh state
|
|
38
|
+
// Fixes a bug where dismissing the modal too quickly causes the Reanimated shared values (like toolbarVisible) to not reset.
|
|
39
|
+
const [modalKey, setModalKey] = useState(0);
|
|
33
40
|
return (<>
|
|
34
|
-
<PlatformPressable style={styles.container} onPress={() =>
|
|
35
|
-
|
|
41
|
+
<PlatformPressable style={styles.container} onPress={() => {
|
|
42
|
+
setModalKey(prev => prev + 1);
|
|
43
|
+
setVisible(true);
|
|
44
|
+
}} onLongPress={() => onMessageAttachmentLongPress(attachment)} android_ripple={{ color: colors.androidRippleNeutral, foreground: true }} accessibilityHint="Long press for more options">
|
|
45
|
+
<Image source={{ uri: urlMedium || url }} style={styles.image} wrapperStyle={styles.attachmentImageWrapper} alt={filename}/>
|
|
36
46
|
</PlatformPressable>
|
|
37
|
-
<LightboxModal visible={visible} setModalVisible={setVisible}
|
|
47
|
+
<LightboxModal key={modalKey} visible={visible} setModalVisible={setVisible} imageAttachments={imageAttachments} initialImageIndex={currentImageIndex} metaProps={metaProps}/>
|
|
38
48
|
</>);
|
|
39
49
|
}
|
|
40
|
-
const LightboxModal = ({
|
|
50
|
+
const LightboxModal = ({ visible, setModalVisible, imageAttachments, initialImageIndex, metaProps, }) => {
|
|
41
51
|
const styles = useStyles();
|
|
42
52
|
const insets = useSafeAreaInsets();
|
|
43
|
-
const
|
|
53
|
+
const [currentImageIndex, setCurrentImageIndex] = useState(initialImageIndex);
|
|
54
|
+
// Get current image data
|
|
55
|
+
const currentImage = imageAttachments[currentImageIndex];
|
|
56
|
+
const { url, urlMedium, metadata = {} } = currentImage.attributes;
|
|
57
|
+
const imageWidth = metadata.width;
|
|
58
|
+
const imageHeight = metadata.height;
|
|
44
59
|
// Calculate available space for image display
|
|
45
60
|
const availableWindowWidth = WINDOW_WIDTH;
|
|
46
61
|
const availableWindowHeight = WINDOW_HEIGHT - insets.top - insets.bottom;
|
|
@@ -53,45 +68,64 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
53
68
|
const scale = useSharedValue(DEFAULT_SCALE); // zoom level of image
|
|
54
69
|
const focalX = useSharedValue(0); // focal point of image between fingers
|
|
55
70
|
const focalY = useSharedValue(0); // focal point of image between fingers
|
|
56
|
-
const translateX = useSharedValue(
|
|
57
|
-
const translateY = useSharedValue(
|
|
71
|
+
const translateX = useSharedValue(DEFAULT_TRANSLATE_X); // horizontal distance to pan image
|
|
72
|
+
const translateY = useSharedValue(DEFAULT_TRANSLATE_Y); // vertical distance to pan image
|
|
58
73
|
const savedScale = useSharedValue(DEFAULT_SCALE); // previous zoom level
|
|
59
|
-
const savedTranslateX = useSharedValue(
|
|
60
|
-
const savedTranslateY = useSharedValue(
|
|
61
|
-
const
|
|
74
|
+
const savedTranslateX = useSharedValue(DEFAULT_TRANSLATE_X); // previous horizontal position
|
|
75
|
+
const savedTranslateY = useSharedValue(DEFAULT_TRANSLATE_Y); // previous vertical position
|
|
76
|
+
const lightboxToolbarVisible = useSharedValue(1); // toolbar visibility state
|
|
62
77
|
// React (JS) State:
|
|
63
78
|
const [isStatusBarHidden, setIsStatusBarHidden] = useState(false);
|
|
79
|
+
const [flatListScrollEnabled, setFlatListScrollEnabled] = useState(true);
|
|
80
|
+
const [panGestureEnabled, setPanGestureEnabled] = useState(false);
|
|
64
81
|
// Syncs toolbar useSharedValue state with React's state so that the status bar can be hidden/shown based on the toolbar's animation
|
|
65
|
-
useAnimatedReaction(() =>
|
|
82
|
+
useAnimatedReaction(() => lightboxToolbarVisible.value, value => {
|
|
66
83
|
runOnJS(setIsStatusBarHidden)(value === 0);
|
|
67
84
|
});
|
|
85
|
+
// Syncs FlatList scroll state with scale changes
|
|
86
|
+
// When image is at default scale, enable scroll and disable pan gesture
|
|
87
|
+
// When image is zoomed in, disable scroll and enable pan gesture
|
|
88
|
+
useAnimatedReaction(() => scale.value, value => {
|
|
89
|
+
const enableFlatListScroll = value === DEFAULT_SCALE;
|
|
90
|
+
runOnJS(setFlatListScrollEnabled)(enableFlatListScroll);
|
|
91
|
+
runOnJS(setPanGestureEnabled)(!enableFlatListScroll);
|
|
92
|
+
});
|
|
68
93
|
/* ============================
|
|
69
94
|
HANDLERS
|
|
70
95
|
============================ */
|
|
71
|
-
const handleOpenInBrowser = () => {
|
|
72
|
-
Linking.openURL(
|
|
73
|
-
};
|
|
74
|
-
const resetDismissGestures = () => {
|
|
96
|
+
const handleOpenInBrowser = useCallback(() => {
|
|
97
|
+
Linking.openURL(urlMedium || url);
|
|
98
|
+
}, [urlMedium, url]);
|
|
99
|
+
const resetDismissGestures = useCallback(() => {
|
|
75
100
|
dismissY.value = withSpring(0, RESET_SPRING_CONFIG);
|
|
76
101
|
opacity.value = withSpring(DEFAULT_OPACITY, RESET_SPRING_CONFIG);
|
|
77
|
-
};
|
|
78
|
-
const resetAllGestures = () => {
|
|
102
|
+
}, [dismissY, opacity]);
|
|
103
|
+
const resetAllGestures = useCallback(() => {
|
|
79
104
|
resetDismissGestures();
|
|
80
105
|
scale.value = withSpring(DEFAULT_SCALE, RESET_SPRING_CONFIG);
|
|
81
|
-
translateX.value = withSpring(
|
|
82
|
-
translateY.value = withSpring(
|
|
106
|
+
translateX.value = withSpring(DEFAULT_TRANSLATE_X, RESET_SPRING_CONFIG);
|
|
107
|
+
translateY.value = withSpring(DEFAULT_TRANSLATE_Y, RESET_SPRING_CONFIG);
|
|
83
108
|
savedScale.value = DEFAULT_SCALE;
|
|
84
|
-
savedTranslateX.value =
|
|
85
|
-
savedTranslateY.value =
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
109
|
+
savedTranslateX.value = DEFAULT_TRANSLATE_X;
|
|
110
|
+
savedTranslateY.value = DEFAULT_TRANSLATE_Y;
|
|
111
|
+
lightboxToolbarVisible.value = withSpring(1, RESET_SPRING_CONFIG);
|
|
112
|
+
}, [
|
|
113
|
+
resetDismissGestures,
|
|
114
|
+
scale,
|
|
115
|
+
translateX,
|
|
116
|
+
translateY,
|
|
117
|
+
savedScale,
|
|
118
|
+
savedTranslateX,
|
|
119
|
+
savedTranslateY,
|
|
120
|
+
lightboxToolbarVisible,
|
|
121
|
+
]);
|
|
122
|
+
const handleCloseModal = useCallback(() => {
|
|
89
123
|
setModalVisible(false);
|
|
90
124
|
resetAllGestures();
|
|
91
|
-
};
|
|
125
|
+
}, [setModalVisible, resetAllGestures]);
|
|
92
126
|
/* ============================
|
|
93
|
-
UTILITY FUNCTIONS
|
|
94
|
-
'worklet' runs functions on the UI thread, instead of the JS thread for better performance
|
|
127
|
+
UTILITY WORKLET FUNCTIONS
|
|
128
|
+
'worklet' runs functions on the UI thread, instead of the JS thread for better performance.
|
|
95
129
|
============================ */
|
|
96
130
|
const getImageContainedToWindowDimensions = () => {
|
|
97
131
|
'worklet';
|
|
@@ -140,8 +174,8 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
140
174
|
const excessWidth = scaledWidth - availableWindowWidth;
|
|
141
175
|
const excessHeight = scaledHeight - availableWindowHeight;
|
|
142
176
|
// How far the image can move in each direction before hitting window edges
|
|
143
|
-
const maxTranslateX = Math.max(
|
|
144
|
-
const maxTranslateY = Math.max(
|
|
177
|
+
const maxTranslateX = Math.max(DEFAULT_TRANSLATE_X, excessWidth / 2);
|
|
178
|
+
const maxTranslateY = Math.max(DEFAULT_TRANSLATE_Y, excessHeight / 2);
|
|
145
179
|
return { maxTranslateX, maxTranslateY };
|
|
146
180
|
};
|
|
147
181
|
const clampDecay = (currentScale) => {
|
|
@@ -166,22 +200,62 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
166
200
|
// Calculate how much the image can exceed the window container
|
|
167
201
|
const scaledHeight = containedImageHeight * currentScale;
|
|
168
202
|
const excessHeight = scaledHeight - availableWindowHeight;
|
|
169
|
-
const maxTranslateY = Math.max(
|
|
203
|
+
const maxTranslateY = Math.max(DEFAULT_TRANSLATE_Y, excessHeight / 2);
|
|
170
204
|
const currentTranslateY = translateY.value;
|
|
171
205
|
const panPositionTolerance = 1; // buffer to account for translateY being at a subpixel position
|
|
172
206
|
const atTopBoundry = currentTranslateY >= maxTranslateY - panPositionTolerance;
|
|
173
207
|
const atBottomBoundry = currentTranslateY <= -maxTranslateY + panPositionTolerance;
|
|
174
208
|
return atTopBoundry || atBottomBoundry;
|
|
175
209
|
};
|
|
210
|
+
/* ============================
|
|
211
|
+
UTILITY FLATLIST FUNCTIONS
|
|
212
|
+
Supports the image gallery layout and swipe functionality.
|
|
213
|
+
============================ */
|
|
214
|
+
// Used in tandem with FlatList's initialScrollIndex to quickly calculate the position and size of each image before they load.
|
|
215
|
+
const getItemLayout = useCallback((_, index) => ({
|
|
216
|
+
length: WINDOW_WIDTH,
|
|
217
|
+
offset: WINDOW_WIDTH * index,
|
|
218
|
+
index,
|
|
219
|
+
}), []);
|
|
220
|
+
// Captures the current image's index after the FlatList finishes its scroll animation.
|
|
221
|
+
// Used in tandem with onViewableItemsChanged to ensure the final value for currentImageIndex is set.
|
|
222
|
+
const onMomentumScrollEnd = useCallback((event) => {
|
|
223
|
+
// Calculate the index of the image that is currently visible
|
|
224
|
+
const imageOffsetX = event.nativeEvent.contentOffset.x;
|
|
225
|
+
const newImageIndex = Math.round(imageOffsetX / WINDOW_WIDTH);
|
|
226
|
+
// Check if the image index has changed and the FlatList didn't scroll past the first or last image
|
|
227
|
+
const didImageIndexChange = newImageIndex !== currentImageIndex;
|
|
228
|
+
const isImageIndexWithinBounds = newImageIndex >= 0 && newImageIndex < imageAttachments.length;
|
|
229
|
+
if (didImageIndexChange && isImageIndexWithinBounds) {
|
|
230
|
+
setCurrentImageIndex(newImageIndex);
|
|
231
|
+
}
|
|
232
|
+
}, [currentImageIndex, imageAttachments.length]);
|
|
233
|
+
// Supplements onMomentumScrollEnd by capturing the current image's index while the FlatList is actively scrolling.
|
|
234
|
+
// Used in tandem with viewabilityConfig to trigger when the image is 50% visible in the window.
|
|
235
|
+
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
|
|
236
|
+
if (viewableItems.length === 0)
|
|
237
|
+
return;
|
|
238
|
+
// Use the first viewable item which is enforced by the FlatList's pagingEnabled prop that allows only two images to be visible at a time when scrolling.
|
|
239
|
+
const firstViewableItem = viewableItems[0];
|
|
240
|
+
const newIndex = firstViewableItem.index;
|
|
241
|
+
if (newIndex !== null && newIndex !== currentImageIndex) {
|
|
242
|
+
setCurrentImageIndex(newIndex);
|
|
243
|
+
}
|
|
244
|
+
}, [currentImageIndex]);
|
|
176
245
|
/* ============================
|
|
177
246
|
GESTURES
|
|
178
247
|
============================ */
|
|
179
248
|
const singleTapGesture = Gesture.Tap()
|
|
249
|
+
.minPointers(SINGLE_FINGER_POINTER)
|
|
180
250
|
.numberOfTaps(1)
|
|
181
251
|
.onStart(() => {
|
|
182
|
-
|
|
252
|
+
lightboxToolbarVisible.value = withSpring(lightboxToolbarVisible.value > 0.5 ? 0 : 1, RESET_SPRING_CONFIG);
|
|
183
253
|
});
|
|
184
254
|
const doubleTapGesture = Gesture.Tap()
|
|
255
|
+
.maxDeltaX(MAX_TRAVEL_DISTANCE_FOR_DOUBLE_TAP_PLATFORM)
|
|
256
|
+
.maxDeltaY(MAX_TRAVEL_DISTANCE_FOR_DOUBLE_TAP_PLATFORM)
|
|
257
|
+
.maxDistance(MAX_TRAVEL_DISTANCE_FOR_DOUBLE_TAP_PLATFORM)
|
|
258
|
+
.minPointers(SINGLE_FINGER_POINTER)
|
|
185
259
|
.numberOfTaps(2)
|
|
186
260
|
.onStart(e => {
|
|
187
261
|
const isZoomedIn = scale.value > DEFAULT_SCALE;
|
|
@@ -190,7 +264,7 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
190
264
|
}
|
|
191
265
|
else {
|
|
192
266
|
// Hide toolbar when starting to zoom
|
|
193
|
-
|
|
267
|
+
lightboxToolbarVisible.value = withSpring(0, RESET_SPRING_CONFIG);
|
|
194
268
|
// Zoom to 2x at tap location
|
|
195
269
|
const newTranslation = zoomToFocalPoint({
|
|
196
270
|
focalPointX: e.x,
|
|
@@ -221,7 +295,7 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
221
295
|
focalX.value = e.focalX;
|
|
222
296
|
focalY.value = e.focalY;
|
|
223
297
|
// Hide toolbar when starting to zoom
|
|
224
|
-
|
|
298
|
+
lightboxToolbarVisible.value = withSpring(0, RESET_SPRING_CONFIG);
|
|
225
299
|
// Ensure that pinch accounts for the decay animation and starts from the true current position.
|
|
226
300
|
savedTranslateX.value = translateX.value;
|
|
227
301
|
savedTranslateY.value = translateY.value;
|
|
@@ -284,9 +358,10 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
284
358
|
savedTranslateY.value = clampedTranslate.y;
|
|
285
359
|
});
|
|
286
360
|
const panGesture = Gesture.Pan()
|
|
287
|
-
.minDistance(
|
|
288
|
-
.minPointers(
|
|
289
|
-
.maxPointers(
|
|
361
|
+
.minDistance(MIN_DISTANCE_FOR_PAN)
|
|
362
|
+
.minPointers(SINGLE_FINGER_POINTER)
|
|
363
|
+
.maxPointers(SINGLE_FINGER_POINTER)
|
|
364
|
+
.enabled(panGestureEnabled)
|
|
290
365
|
.onStart(() => {
|
|
291
366
|
// Update saved position to current animated position
|
|
292
367
|
// This ensures smooth continuation if decay is running
|
|
@@ -339,12 +414,15 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
339
414
|
const atVerticalBoundry = isImageAtVerticalBoundry(scale.value);
|
|
340
415
|
const panDirectionIsPrimarilyVertical = Math.abs(e.translationY) > Math.abs(e.translationX);
|
|
341
416
|
const canDismissWhileZoomed = atVerticalBoundry && panDirectionIsPrimarilyVertical;
|
|
342
|
-
if (atDefaultScale || canDismissWhileZoomed)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
417
|
+
if (!(atDefaultScale || canDismissWhileZoomed))
|
|
418
|
+
return;
|
|
419
|
+
// Fade image if its been panned past 50% of the dismiss threshold
|
|
420
|
+
const panDistance = Math.abs(e.translationY);
|
|
421
|
+
const halfThreshold = DISMISS_PAN_THRESHOLD / 2;
|
|
422
|
+
const fadeDistance = Math.max(0, panDistance - halfThreshold);
|
|
423
|
+
const fadeProgress = fadeDistance / halfThreshold;
|
|
424
|
+
opacity.value = Math.max(0, DEFAULT_OPACITY - fadeProgress);
|
|
425
|
+
dismissY.value = e.translationY;
|
|
348
426
|
})
|
|
349
427
|
.onEnd(() => {
|
|
350
428
|
const exceededDismissThreshold = Math.abs(dismissY.value) > DISMISS_PAN_THRESHOLD;
|
|
@@ -356,7 +434,7 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
356
434
|
}
|
|
357
435
|
});
|
|
358
436
|
/* ==============================
|
|
359
|
-
|
|
437
|
+
COMPOSE GESTURES
|
|
360
438
|
================================= */
|
|
361
439
|
// Race between pinch and pan ensures only one is active at a time, preserving focal point logic
|
|
362
440
|
const pinchOrPanGestures = Gesture.Race(pinchGesture, panGesture);
|
|
@@ -366,43 +444,19 @@ const LightboxModal = ({ uri, visible, setModalVisible, metaProps, imageWidth, i
|
|
|
366
444
|
const transformImageGestures = Gesture.Race(tapGestures, pinchOrPanGestures);
|
|
367
445
|
// Dismiss can work simultaneously with all gestures
|
|
368
446
|
const composedGesture = Gesture.Simultaneous(transformImageGestures, panToDismissModalGesture);
|
|
369
|
-
const animatedImageStyles = useAnimatedStyle(() => ({
|
|
370
|
-
transform: [
|
|
371
|
-
{ translateX: translateX.value },
|
|
372
|
-
{ translateY: translateY.value + dismissY.value },
|
|
373
|
-
{ scale: scale.value },
|
|
374
|
-
],
|
|
375
|
-
opacity: opacity.value,
|
|
376
|
-
}));
|
|
377
|
-
const animatedToolbarStyles = useAnimatedStyle(() => ({
|
|
378
|
-
opacity: toolbarVisible.value,
|
|
379
|
-
transform: [
|
|
380
|
-
{ translateY: (1 - toolbarVisible.value) * -20 }, // slide up when hiding
|
|
381
|
-
],
|
|
382
|
-
}));
|
|
383
447
|
return (<Modal visible={visible} transparent animationType="fade" onRequestClose={handleCloseModal}>
|
|
384
448
|
<StatusBar barStyle="light-content" hidden={isStatusBarHidden} animated showHideTransition="slide"/>
|
|
385
|
-
<
|
|
386
|
-
<
|
|
387
|
-
<
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
</
|
|
392
|
-
</
|
|
393
|
-
</
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
<Heading variant="h3" style={styles.actionToolbarTitle} numberOfLines={1}>
|
|
397
|
-
{authorName}
|
|
398
|
-
</Heading>
|
|
399
|
-
<Text variant="tertiary" style={styles.actionToolbarSubtitle}>
|
|
400
|
-
{formatDatePreview(createdAt)}
|
|
401
|
-
</Text>
|
|
402
|
-
</View>
|
|
403
|
-
<IconButton onPress={handleOpenInBrowser} name="general.newWindow" accessibilityRole="link" accessibilityLabel="Open image in browser" accessibilityHint="Image can be downloaded and shared through the browser." style={styles.actionButton} iconStyle={styles.actionButtonIcon} size="lg"/>
|
|
404
|
-
<IconButton onPress={handleCloseModal} name="general.x" accessibilityLabel="Close image" style={styles.actionButton} iconStyle={styles.actionButtonIcon} size="lg"/>
|
|
405
|
-
</Animated.View>
|
|
449
|
+
<GestureHandlerRootView>
|
|
450
|
+
<GestureDetector gesture={composedGesture}>
|
|
451
|
+
<PreventPressEventsBubbling>
|
|
452
|
+
<FlatList data={imageAttachments} renderItem={({ item, index }) => (<GestureImage item={item} gesturesEnabled={index === currentImageIndex} scale={scale} translateX={translateX} translateY={translateY} dismissY={dismissY} opacity={opacity}/>)} keyExtractor={(item, index) => `${item.id}-${index}`} horizontal pagingEnabled scrollEnabled={flatListScrollEnabled} showsHorizontalScrollIndicator={false} initialScrollIndex={initialImageIndex} getItemLayout={getItemLayout} onMomentumScrollEnd={onMomentumScrollEnd} onViewableItemsChanged={onViewableItemsChanged} viewabilityConfig={{
|
|
453
|
+
itemVisiblePercentThreshold: 50, // 50% of the image must be visible in the window to be considered viewable
|
|
454
|
+
}} style={styles.gallery} contentContainerStyle={styles.galleryContentContainer}/>
|
|
455
|
+
</PreventPressEventsBubbling>
|
|
456
|
+
</GestureDetector>
|
|
457
|
+
</GestureHandlerRootView>
|
|
458
|
+
<LightboxToolbarHeader metaProps={metaProps} lightboxToolbarVisible={lightboxToolbarVisible} isStatusBarHidden={isStatusBarHidden} handleOpenInBrowser={handleOpenInBrowser} handleCloseModal={handleCloseModal}/>
|
|
459
|
+
{imageAttachments.length > 1 && (<LightboxToolbarFooter lightboxToolbarVisible={lightboxToolbarVisible} totalImages={imageAttachments.length} currentImageIndex={currentImageIndex}/>)}
|
|
406
460
|
</Modal>);
|
|
407
461
|
};
|
|
408
462
|
const PreventPressEventsBubbling = ({ children }) => {
|
|
@@ -410,6 +464,57 @@ const PreventPressEventsBubbling = ({ children }) => {
|
|
|
410
464
|
{children}
|
|
411
465
|
</Pressable>);
|
|
412
466
|
};
|
|
467
|
+
const GestureImage = ({ item, gesturesEnabled, scale, translateX, translateY, dismissY, opacity, }) => {
|
|
468
|
+
const styles = useStyles();
|
|
469
|
+
const { url: itemUrl, urlMedium: itemUrlMedium } = item.attributes;
|
|
470
|
+
const animatedImageStyles = useAnimatedStyle(() => {
|
|
471
|
+
return {
|
|
472
|
+
transform: [
|
|
473
|
+
{ translateX: gesturesEnabled ? translateX.value : DEFAULT_TRANSLATE_X },
|
|
474
|
+
{ translateY: gesturesEnabled ? translateY.value + dismissY.value : DEFAULT_TRANSLATE_Y },
|
|
475
|
+
{ scale: gesturesEnabled ? scale.value : DEFAULT_SCALE },
|
|
476
|
+
],
|
|
477
|
+
opacity: opacity.value,
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
return (<Image source={{ uri: itemUrlMedium || itemUrl }} style={styles.gestureImage} animatedImageStyle={animatedImageStyles} loadingBackgroundStyles={styles.gestureImageLoading} resizeMode="contain" alt=""/>);
|
|
481
|
+
};
|
|
482
|
+
const LightboxToolbarHeader = ({ metaProps, lightboxToolbarVisible, isStatusBarHidden, handleOpenInBrowser, handleCloseModal, }) => {
|
|
483
|
+
const styles = useStyles();
|
|
484
|
+
const { authorName, createdAt } = metaProps;
|
|
485
|
+
const animatedHeaderStyles = useAnimatedStyle(() => ({
|
|
486
|
+
opacity: lightboxToolbarVisible.value,
|
|
487
|
+
transform: [
|
|
488
|
+
{ translateY: (1 - lightboxToolbarVisible.value) * -20 }, // slide up when hiding
|
|
489
|
+
],
|
|
490
|
+
}));
|
|
491
|
+
return (<Animated.View style={[styles.lightboxToolbar, styles.lightboxToolbarHeader, animatedHeaderStyles]} accessibilityRole="toolbar">
|
|
492
|
+
<View style={styles.lightboxToolbarHeaderMetaContainer}>
|
|
493
|
+
<Heading variant="h3" style={styles.lightboxToolbarHeaderTitle} numberOfLines={1}>
|
|
494
|
+
{authorName}
|
|
495
|
+
</Heading>
|
|
496
|
+
<Text variant="tertiary" style={styles.lightboxToolbarHeaderSubtitle}>
|
|
497
|
+
{formatDatePreview(createdAt)}
|
|
498
|
+
</Text>
|
|
499
|
+
</View>
|
|
500
|
+
<IconButton onPress={handleOpenInBrowser} disabled={isStatusBarHidden} name="general.newWindow" accessibilityRole="link" accessibilityLabel="Open image in browser" accessibilityHint="Image can be downloaded and shared through the browser." style={styles.lightboxToolbarButton} iconStyle={styles.lightboxToolbarButtonIcon} size="lg"/>
|
|
501
|
+
<IconButton onPress={handleCloseModal} disabled={isStatusBarHidden} name="general.x" accessibilityLabel="Close image" style={styles.lightboxToolbarButton} iconStyle={styles.lightboxToolbarButtonIcon} size="lg"/>
|
|
502
|
+
</Animated.View>);
|
|
503
|
+
};
|
|
504
|
+
const LightboxToolbarFooter = ({ lightboxToolbarVisible, totalImages, currentImageIndex, }) => {
|
|
505
|
+
const styles = useStyles();
|
|
506
|
+
const animatedFooterStyles = useAnimatedStyle(() => ({
|
|
507
|
+
opacity: lightboxToolbarVisible.value,
|
|
508
|
+
transform: [
|
|
509
|
+
{ translateY: (1 - lightboxToolbarVisible.value) * 20 }, // slide down when showing
|
|
510
|
+
],
|
|
511
|
+
}));
|
|
512
|
+
return (<Animated.View style={[styles.lightboxToolbar, styles.lightboxToolbarFooter, animatedFooterStyles]} accessibilityRole="toolbar">
|
|
513
|
+
<Text style={styles.lightboxToolbarFooterText}>
|
|
514
|
+
{currentImageIndex + 1} of {totalImages}
|
|
515
|
+
</Text>
|
|
516
|
+
</Animated.View>);
|
|
517
|
+
};
|
|
413
518
|
const useStyles = ({ imageWidth = 100, imageHeight = 100 } = {}) => {
|
|
414
519
|
const { top, bottom } = useSafeAreaInsets();
|
|
415
520
|
const backgroundColor = tokens.colorNeutral7;
|
|
@@ -418,7 +523,7 @@ const useStyles = ({ imageWidth = 100, imageHeight = 100 } = {}) => {
|
|
|
418
523
|
container: {
|
|
419
524
|
maxWidth: '100%',
|
|
420
525
|
},
|
|
421
|
-
|
|
526
|
+
attachmentImageWrapper: {
|
|
422
527
|
width: '100%',
|
|
423
528
|
minWidth: 200,
|
|
424
529
|
aspectRatio: imageWidth / imageHeight,
|
|
@@ -426,55 +531,66 @@ const useStyles = ({ imageWidth = 100, imageHeight = 100 } = {}) => {
|
|
|
426
531
|
image: {
|
|
427
532
|
borderRadius: 8,
|
|
428
533
|
},
|
|
429
|
-
|
|
430
|
-
flex: 1,
|
|
534
|
+
gallery: {
|
|
431
535
|
backgroundColor,
|
|
432
|
-
|
|
433
|
-
|
|
536
|
+
},
|
|
537
|
+
galleryContentContainer: {
|
|
434
538
|
paddingTop: top,
|
|
435
539
|
paddingBottom: bottom,
|
|
436
540
|
},
|
|
437
|
-
|
|
541
|
+
gestureImage: {
|
|
438
542
|
height: '100%',
|
|
439
543
|
width: WINDOW_WIDTH,
|
|
440
|
-
backgroundColor,
|
|
544
|
+
backgroundColor: 'transparent',
|
|
441
545
|
},
|
|
442
|
-
|
|
546
|
+
gestureImageLoading: {
|
|
443
547
|
backgroundColor,
|
|
444
548
|
},
|
|
445
|
-
|
|
549
|
+
lightboxToolbar: {
|
|
446
550
|
width: '100%',
|
|
447
551
|
position: 'absolute',
|
|
448
|
-
top: 0,
|
|
449
552
|
flexDirection: 'row',
|
|
450
553
|
alignItems: 'center',
|
|
451
554
|
gap: 20,
|
|
452
555
|
paddingHorizontal: 16,
|
|
556
|
+
backgroundColor: transparentBackgroundColor,
|
|
557
|
+
},
|
|
558
|
+
lightboxToolbarButton: {
|
|
559
|
+
backgroundColor,
|
|
560
|
+
height: 40,
|
|
561
|
+
width: 40,
|
|
562
|
+
borderRadius: 50,
|
|
563
|
+
borderWidth: 1,
|
|
564
|
+
borderColor: tokens.colorNeutral24,
|
|
565
|
+
},
|
|
566
|
+
lightboxToolbarButtonIcon: {
|
|
567
|
+
color: tokens.colorNeutral88,
|
|
568
|
+
},
|
|
569
|
+
lightboxToolbarHeader: {
|
|
570
|
+
top: 0,
|
|
453
571
|
paddingTop: top + 16,
|
|
454
572
|
paddingBottom: 8,
|
|
455
|
-
backgroundColor: transparentBackgroundColor,
|
|
456
573
|
},
|
|
457
|
-
|
|
574
|
+
lightboxToolbarHeaderMetaContainer: {
|
|
458
575
|
flex: 1,
|
|
459
576
|
},
|
|
460
|
-
|
|
577
|
+
lightboxToolbarHeaderTitle: {
|
|
461
578
|
marginRight: 'auto',
|
|
462
579
|
flexShrink: 1,
|
|
463
580
|
color: tokens.colorNeutral88,
|
|
464
581
|
},
|
|
465
|
-
|
|
582
|
+
lightboxToolbarHeaderSubtitle: {
|
|
466
583
|
color: tokens.colorNeutral68,
|
|
467
584
|
},
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
borderWidth: 1,
|
|
474
|
-
borderColor: tokens.colorNeutral24,
|
|
585
|
+
lightboxToolbarFooter: {
|
|
586
|
+
justifyContent: 'center',
|
|
587
|
+
bottom: 0,
|
|
588
|
+
paddingTop: 8,
|
|
589
|
+
paddingBottom: bottom + 16,
|
|
475
590
|
},
|
|
476
|
-
|
|
591
|
+
lightboxToolbarFooterText: {
|
|
477
592
|
color: tokens.colorNeutral88,
|
|
593
|
+
fontWeight: platformFontWeightMedium,
|
|
478
594
|
},
|
|
479
595
|
});
|
|
480
596
|
};
|