diff options
Diffstat (limited to 'src/view/com/util')
-rw-r--r-- | src/view/com/util/EmptyState.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/ErrorBoundary.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 9 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 108 | ||||
-rw-r--r-- | src/view/com/util/ViewHeader.tsx | 31 | ||||
-rw-r--r-- | src/view/com/util/ViewSelector.tsx | 15 | ||||
-rw-r--r-- | src/view/com/util/fab/FABInner.tsx | 48 | ||||
-rw-r--r-- | src/view/com/util/images/AutoSizedImage.tsx | 12 | ||||
-rw-r--r-- | src/view/com/util/images/Gallery.tsx | 17 | ||||
-rw-r--r-- | src/view/com/util/images/ImageLayoutGrid.tsx | 118 | ||||
-rw-r--r-- | src/view/com/util/load-latest/LoadLatestBtn.tsx | 34 |
11 files changed, 192 insertions, 205 deletions
diff --git a/src/view/com/util/EmptyState.tsx b/src/view/com/util/EmptyState.tsx index a495fcd3f..7486b212f 100644 --- a/src/view/com/util/EmptyState.tsx +++ b/src/view/com/util/EmptyState.tsx @@ -22,7 +22,7 @@ export function EmptyState({ }) { const pal = usePalette('default') return ( - <View testID={testID} style={[styles.container, style]}> + <View testID={testID} style={[styles.container, pal.border, style]}> <View style={styles.iconContainer}> {icon === 'user-group' ? ( <UserGroupIcon size="64" style={styles.icon} /> @@ -50,6 +50,7 @@ const styles = StyleSheet.create({ container: { paddingVertical: 20, paddingHorizontal: 36, + borderTopWidth: 1, }, iconContainer: { flexDirection: 'row', diff --git a/src/view/com/util/ErrorBoundary.tsx b/src/view/com/util/ErrorBoundary.tsx index c7374e195..529435cf1 100644 --- a/src/view/com/util/ErrorBoundary.tsx +++ b/src/view/com/util/ErrorBoundary.tsx @@ -28,7 +28,7 @@ export class ErrorBoundary extends Component<Props, State> { public render() { if (this.state.hasError) { return ( - <CenteredView> + <CenteredView style={{height: '100%', flex: 1}}> <ErrorScreen title="Oh no!" message="There was an unexpected issue in the application. Please let us know if this happened to you!" diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 472d943e1..94fe75536 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -1,5 +1,4 @@ -import React, {ComponentProps, useMemo} from 'react' -import {observer} from 'mobx-react-lite' +import React, {ComponentProps, memo, useMemo} from 'react' import { Linking, GestureResponderEvent, @@ -49,7 +48,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> { anchorNoUnderline?: boolean } -export const Link = observer(function Link({ +export const Link = memo(function Link({ testID, style, href, @@ -135,7 +134,7 @@ export const Link = observer(function Link({ ) }) -export const TextLink = observer(function TextLink({ +export const TextLink = memo(function TextLink({ testID, type = 'md', style, @@ -235,7 +234,7 @@ interface DesktopWebTextLinkProps extends TextProps { accessibilityHint?: string title?: string } -export const DesktopWebTextLink = observer(function DesktopWebTextLink({ +export const DesktopWebTextLink = memo(function DesktopWebTextLink({ testID, type = 'md', style, diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index d24e47499..fbc0b5e11 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -23,14 +23,18 @@ interface BaseUserAvatarProps { type?: Type size: number avatar?: string | null - moderation?: ModerationUI } interface UserAvatarProps extends BaseUserAvatarProps { - onSelectNewAvatar?: (img: RNImage | null) => void + moderation?: ModerationUI +} + +interface EditableUserAvatarProps extends BaseUserAvatarProps { + onSelectNewAvatar: (img: RNImage | null) => void } interface PreviewableUserAvatarProps extends BaseUserAvatarProps { + moderation?: ModerationUI did: string handle: string } @@ -106,8 +110,65 @@ export function UserAvatar({ size, avatar, moderation, - onSelectNewAvatar, }: UserAvatarProps) { + const pal = usePalette('default') + + const aviStyle = useMemo(() => { + if (type === 'algo' || type === 'list') { + return { + width: size, + height: size, + borderRadius: size > 32 ? 8 : 3, + } + } + return { + width: size, + height: size, + borderRadius: Math.floor(size / 2), + } + }, [type, size]) + + const alert = useMemo(() => { + if (!moderation?.alert) { + return null + } + return ( + <View style={[styles.alertIconContainer, pal.view]}> + <FontAwesomeIcon + icon="exclamation-circle" + style={styles.alertIcon} + size={Math.floor(size / 3)} + /> + </View> + ) + }, [moderation?.alert, size, pal]) + + return avatar && + !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( + <View style={{width: size, height: size}}> + <HighPriorityImage + testID="userAvatarImage" + style={aviStyle} + contentFit="cover" + source={{uri: avatar}} + blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} + /> + {alert} + </View> + ) : ( + <View style={{width: size, height: size}}> + <DefaultAvatar type={type} size={size} /> + {alert} + </View> + ) +} + +export function EditableUserAvatar({ + type = 'user', + size, + avatar, + onSelectNewAvatar, +}: EditableUserAvatarProps) { const store = useStores() const pal = usePalette('default') const {requestCameraAccessIfNeeded} = useCameraPermission() @@ -146,7 +207,7 @@ export function UserAvatar({ return } - onSelectNewAvatar?.( + onSelectNewAvatar( await openCamera(store, { width: 1000, height: 1000, @@ -186,7 +247,7 @@ export function UserAvatar({ path: item.path, }) - onSelectNewAvatar?.(croppedImage) + onSelectNewAvatar(croppedImage) }, }, !!avatar && { @@ -203,7 +264,7 @@ export function UserAvatar({ web: 'trash', }, onPress: async () => { - onSelectNewAvatar?.(null) + onSelectNewAvatar(null) }, }, ].filter(Boolean) as DropdownItem[], @@ -216,23 +277,7 @@ export function UserAvatar({ ], ) - const alert = useMemo(() => { - if (!moderation?.alert) { - return null - } - return ( - <View style={[styles.alertIconContainer, pal.view]}> - <FontAwesomeIcon - icon="exclamation-circle" - style={styles.alertIcon} - size={Math.floor(size / 3)} - /> - </View> - ) - }, [moderation?.alert, size, pal]) - - // onSelectNewAvatar is only passed as prop on the EditProfile component - return onSelectNewAvatar ? ( + return ( <NativeDropdown testID="changeAvatarBtn" items={dropdownItems} @@ -256,23 +301,6 @@ export function UserAvatar({ /> </View> </NativeDropdown> - ) : avatar && - !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( - <View style={{width: size, height: size}}> - <HighPriorityImage - testID="userAvatarImage" - style={aviStyle} - contentFit="cover" - source={{uri: avatar}} - blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} - /> - {alert} - </View> - ) : ( - <View style={{width: size, height: size}}> - <DefaultAvatar type={type} size={size} /> - {alert} - </View> ) } diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index 164028708..ec459b4eb 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -1,16 +1,17 @@ import React from 'react' import {observer} from 'mobx-react-lite' -import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' +import {StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' import {CenteredView} from './Views' import {Text} from './text/Text' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useAnalytics} from 'lib/analytics/analytics' import {NavigationProp} from 'lib/routes/types' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import Animated from 'react-native-reanimated' const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} @@ -149,30 +150,8 @@ const Container = observer(function ContainerImpl({ hideOnScroll: boolean showBorder?: boolean }) { - const store = useStores() const pal = usePalette('default') - const interp = useAnimatedValue(0) - - React.useEffect(() => { - if (store.shell.minimalShellMode) { - Animated.timing(interp, { - toValue: 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - } else { - Animated.timing(interp, { - toValue: 0, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - } - }, [interp, store.shell.minimalShellMode]) - const transform = { - transform: [{translateY: Animated.multiply(interp, -100)}], - } + const {headerMinimalShellTransform} = useMinimalShellMode() if (!hideOnScroll) { return ( @@ -195,7 +174,7 @@ const Container = observer(function ContainerImpl({ styles.headerFloating, pal.view, pal.border, - transform, + headerMinimalShellTransform, showBorder && styles.border, ]}> {children} diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index 6c0e4c6cc..935d93033 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -144,8 +144,6 @@ export function Selector({ items: string[] onSelect?: (index: number) => void }) { - const [height, setHeight] = useState(0) - const pal = usePalette('default') const borderColor = useColorSchemeStyle( {borderColor: colors.black}, @@ -160,22 +158,13 @@ export function Selector({ <View style={{ width: '100%', - position: 'relative', - overflow: 'hidden', - height, backgroundColor: pal.colors.background, }}> <ScrollView testID="selector" horizontal - showsHorizontalScrollIndicator={false} - style={{position: 'absolute'}}> - <View - style={[pal.view, styles.outer]} - onLayout={e => { - const {height: layoutHeight} = e.nativeEvent.layout - setHeight(layoutHeight || 60) - }}> + showsHorizontalScrollIndicator={false}> + <View style={[pal.view, styles.outer]}> {items.map((item, i) => { const selected = i === selectedIndex return ( diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index 6c96eef2c..5b1d5d888 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -1,13 +1,13 @@ import React, {ComponentProps} from 'react' import {observer} from 'mobx-react-lite' -import {Animated, StyleSheet, TouchableWithoutFeedback} from 'react-native' +import {StyleSheet, TouchableWithoutFeedback} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {gradients} from 'lib/styles' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {clamp} from 'lib/numbers' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import Animated from 'react-native-reanimated' export interface FABProps extends ComponentProps<typeof TouchableWithoutFeedback> { @@ -21,28 +21,30 @@ export const FABInner = observer(function FABInnerImpl({ ...props }: FABProps) { const insets = useSafeAreaInsets() - const {isTablet} = useWebMediaQueries() - const store = useStores() - const interp = useAnimatedValue(0) - React.useEffect(() => { - Animated.timing(interp, { - toValue: store.shell.minimalShellMode ? 0 : 1, - duration: 100, - useNativeDriver: true, - isInteraction: false, - }).start() - }, [interp, store.shell.minimalShellMode]) - const transform = isTablet - ? undefined - : { - transform: [{translateY: Animated.multiply(interp, -44)}], - } - const size = isTablet ? styles.sizeLarge : styles.sizeRegular - const right = isTablet ? 50 : 24 - const bottom = isTablet ? 50 : clamp(insets.bottom, 15, 60) + 15 + const {isMobile, isTablet} = useWebMediaQueries() + const {fabMinimalShellTransform} = useMinimalShellMode() + + const size = React.useMemo(() => { + return isTablet ? styles.sizeLarge : styles.sizeRegular + }, [isTablet]) + const tabletSpacing = React.useMemo(() => { + return isTablet + ? {right: 50, bottom: 50} + : { + right: 24, + bottom: clamp(insets.bottom, 15, 60) + 15, + } + }, [insets.bottom, isTablet]) + return ( <TouchableWithoutFeedback testID={testID} {...props}> - <Animated.View style={[styles.outer, size, {right, bottom}, transform]}> + <Animated.View + style={[ + styles.outer, + size, + tabletSpacing, + isMobile && fabMinimalShellTransform, + ]}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 035e29c25..6cbcddc32 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -52,20 +52,20 @@ export function AutoSizedImage({ if (onPress || onLongPress || onPressIn) { return ( + // disable a11y rule because in this case we want the tags on the image (#1640) + // eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors <Pressable onPress={onPress} onLongPress={onLongPress} onPressIn={onPressIn} - style={[styles.container, style]} - accessible={true} - accessibilityRole="button" - accessibilityLabel={alt || 'Image'} - accessibilityHint="Tap to view fully"> + style={[styles.container, style]}> <Image style={[styles.image, {aspectRatio}]} source={uri} - accessible={false} // Must set for `accessibilityLabel` to work + accessible={true} // Must set for `accessibilityLabel` to work accessibilityIgnoresInvertColors + accessibilityLabel={alt} + accessibilityHint="Tap to view fully" /> {children} </Pressable> diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 679f71c99..094b0c56c 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -23,19 +23,19 @@ export const GalleryItem: FC<GalleryItemProps> = ({ onLongPress, }) => { const image = images[index] - return ( - <View> + <View style={styles.fullWidth}> <Pressable onPress={onPress ? () => onPress(index) : undefined} onPressIn={onPressIn ? () => onPressIn(index) : undefined} onLongPress={onLongPress ? () => onLongPress(index) : undefined} + style={styles.fullWidth} accessibilityRole="button" accessibilityLabel={image.alt || 'Image'} accessibilityHint=""> <Image source={{uri: image.thumb}} - style={imageStyle} + style={[styles.image, imageStyle]} accessible={true} accessibilityLabel={image.alt} accessibilityHint="" @@ -54,14 +54,21 @@ export const GalleryItem: FC<GalleryItemProps> = ({ } const styles = StyleSheet.create({ + fullWidth: { + flex: 1, + }, + image: { + flex: 1, + borderRadius: 4, + }, altContainer: { backgroundColor: 'rgba(0, 0, 0, 0.75)', borderRadius: 6, paddingHorizontal: 6, paddingVertical: 3, position: 'absolute', - left: 6, - bottom: 6, + left: 8, + bottom: 8, }, alt: { color: 'white', diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 4c0901304..4aa6f28de 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -1,13 +1,5 @@ -import React, {useMemo, useState} from 'react' -import { - LayoutChangeEvent, - StyleProp, - StyleSheet, - View, - ViewStyle, -} from 'react-native' -import {ImageStyle} from 'expo-image' -import {Dimensions} from 'lib/media/types' +import React from 'react' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {AppBskyEmbedImages} from '@atproto/api' import {GalleryItem} from './Gallery' @@ -20,21 +12,11 @@ interface ImageLayoutGridProps { } export function ImageLayoutGrid({style, ...props}: ImageLayoutGridProps) { - const [containerInfo, setContainerInfo] = useState<Dimensions | undefined>() - - const onLayout = (evt: LayoutChangeEvent) => { - const {width, height} = evt.nativeEvent.layout - setContainerInfo({ - width, - height, - }) - } - return ( - <View style={style} onLayout={onLayout}> - {containerInfo ? ( - <ImageLayoutGridInner {...props} containerInfo={containerInfo} /> - ) : undefined} + <View style={style}> + <View style={styles.container}> + <ImageLayoutGridInner {...props} /> + </View> </View> ) } @@ -44,70 +26,80 @@ interface ImageLayoutGridInnerProps { onPress?: (index: number) => void onLongPress?: (index: number) => void onPressIn?: (index: number) => void - containerInfo: Dimensions } -function ImageLayoutGridInner({ - containerInfo, - ...props -}: ImageLayoutGridInnerProps) { +function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { const count = props.images.length - const size1 = useMemo<ImageStyle>(() => { - if (count === 3) { - const size = (containerInfo.width - 10) / 3 - return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} - } else { - const size = (containerInfo.width - 5) / 2 - return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} - } - }, [count, containerInfo]) - const size2 = React.useMemo<ImageStyle>(() => { - if (count === 3) { - const size = ((containerInfo.width - 10) / 3) * 2 + 5 - return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} - } else { - const size = (containerInfo.width - 5) / 2 - return {width: size, height: size, resizeMode: 'cover', borderRadius: 4} - } - }, [count, containerInfo]) switch (count) { case 2: return ( <View style={styles.flexRow}> - <GalleryItem index={0} {...props} imageStyle={size1} /> - <GalleryItem index={1} {...props} imageStyle={size1} /> + <View style={styles.smallItem}> + <GalleryItem {...props} index={0} imageStyle={styles.image} /> + </View> + <View style={styles.smallItem}> + <GalleryItem {...props} index={1} imageStyle={styles.image} /> + </View> </View> ) + case 3: return ( <View style={styles.flexRow}> - <GalleryItem index={0} {...props} imageStyle={size2} /> - <View style={styles.flexColumn}> - <GalleryItem index={1} {...props} imageStyle={size1} /> - <GalleryItem index={2} {...props} imageStyle={size1} /> + <View style={{flex: 2, aspectRatio: 1}}> + <GalleryItem {...props} index={0} imageStyle={styles.image} /> + </View> + <View style={{flex: 1}}> + <View style={styles.smallItem}> + <GalleryItem {...props} index={1} imageStyle={styles.image} /> + </View> + <View style={styles.smallItem}> + <GalleryItem {...props} index={2} imageStyle={styles.image} /> + </View> </View> </View> ) + case 4: return ( - <View style={styles.flexRow}> - <View style={styles.flexColumn}> - <GalleryItem index={0} {...props} imageStyle={size1} /> - <GalleryItem index={2} {...props} imageStyle={size1} /> + <> + <View style={styles.flexRow}> + <View style={styles.smallItem}> + <GalleryItem {...props} index={0} imageStyle={styles.image} /> + </View> + <View style={styles.smallItem}> + <GalleryItem {...props} index={2} imageStyle={styles.image} /> + </View> </View> - <View style={styles.flexColumn}> - <GalleryItem index={1} {...props} imageStyle={size1} /> - <GalleryItem index={3} {...props} imageStyle={size1} /> + <View style={styles.flexRow}> + <View style={styles.smallItem}> + <GalleryItem {...props} index={1} imageStyle={styles.image} /> + </View> + <View style={styles.smallItem}> + <GalleryItem {...props} index={3} imageStyle={styles.image} /> + </View> </View> - </View> + </> ) + default: return null } } +// This is used to compute margins (rather than flexbox gap) due to Yoga bugs: +// https://github.com/facebook/yoga/issues/1418 +const IMAGE_GAP = 5 + const styles = StyleSheet.create({ - flexRow: {flexDirection: 'row', gap: 5}, - flexColumn: {flexDirection: 'column', gap: 5}, + container: { + marginHorizontal: -IMAGE_GAP / 2, + marginVertical: -IMAGE_GAP / 2, + }, + flexRow: {flexDirection: 'row'}, + smallItem: {flex: 1, aspectRatio: 1}, + image: { + margin: IMAGE_GAP / 2, + }, }) diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index f5d12ce2c..b16a42396 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -2,14 +2,14 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {colors} from 'lib/styles' import {HITSLOP_20} from 'lib/constants' -import {isWeb} from 'platform/detection' -import {clamp} from 'lib/numbers' +import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import Animated from 'react-native-reanimated' +const AnimatedTouchableOpacity = + Animated.createAnimatedComponent(TouchableOpacity) export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ onPress, @@ -19,26 +19,20 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ onPress: () => void label: string showIndicator: boolean - minimalShellMode?: boolean // NOTE not used on mobile -prf }) { - const store = useStores() const pal = usePalette('default') - const {isDesktop, isTablet} = useWebMediaQueries() - const safeAreaInsets = useSafeAreaInsets() - const minMode = store.shell.minimalShellMode - const bottom = isTablet - ? 50 - : (minMode || isDesktop ? 16 : 60) + - (isWeb ? 20 : clamp(safeAreaInsets.bottom, 15, 60)) + const {isDesktop, isTablet, isMobile} = useWebMediaQueries() + const {fabMinimalShellTransform} = useMinimalShellMode() + return ( - <TouchableOpacity + <AnimatedTouchableOpacity style={[ styles.loadLatest, isDesktop && styles.loadLatestDesktop, isTablet && styles.loadLatestTablet, pal.borderDark, pal.view, - {bottom}, + isMobile && fabMinimalShellTransform, ]} onPress={onPress} hitSlop={HITSLOP_20} @@ -47,7 +41,7 @@ export const LoadLatestBtn = observer(function LoadLatestBtnImpl({ accessibilityHint=""> <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} /> {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} - </TouchableOpacity> + </AnimatedTouchableOpacity> ) }) @@ -66,15 +60,11 @@ const styles = StyleSheet.create({ }, loadLatestTablet: { // @ts-ignore web only - left: '50vw', - // @ts-ignore web only -prf - transform: 'translateX(-282px)', + left: 'calc(50vw - 282px)', }, loadLatestDesktop: { // @ts-ignore web only - left: '50vw', - // @ts-ignore web only -prf - transform: 'translateX(-382px)', + left: 'calc(50vw - 382px)', }, indicator: { position: 'absolute', |