forked from bluesky-social/social-app
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Saves image on long press (bluesky-social#83)
* Saves image on long press * Adds save on long press * Forking lightbox * move to wrapper only to the bottom sheet to reduce impact of this change * lint * lint * lint * Use official `share` API * Clean up cache after download * comment * comment * Reduce swipe close velocity * Updates per feedback * lint * bugfix * Adds delayed press-in for TouchableOpacity
- Loading branch information
Showing
23 changed files
with
1,567 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** | ||
* Copyright (c) JOB TODAY S.A. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
*/ | ||
|
||
import {ImageURISource, ImageRequireSource} from 'react-native' | ||
|
||
export type Dimensions = { | ||
width: number | ||
height: number | ||
} | ||
|
||
export type Position = { | ||
x: number | ||
y: number | ||
} | ||
|
||
export type ImageSource = ImageURISource | ImageRequireSource |
52 changes: 52 additions & 0 deletions
52
src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/** | ||
* Copyright (c) JOB TODAY S.A. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
*/ | ||
|
||
import React from 'react' | ||
import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native' | ||
|
||
type Props = { | ||
onRequestClose: () => void | ||
} | ||
|
||
const HIT_SLOP = {top: 16, left: 16, bottom: 16, right: 16} | ||
|
||
const ImageDefaultHeader = ({onRequestClose}: Props) => ( | ||
<SafeAreaView style={styles.root}> | ||
<TouchableOpacity | ||
style={styles.closeButton} | ||
onPress={onRequestClose} | ||
hitSlop={HIT_SLOP}> | ||
<Text style={styles.closeText}>✕</Text> | ||
</TouchableOpacity> | ||
</SafeAreaView> | ||
) | ||
|
||
const styles = StyleSheet.create({ | ||
root: { | ||
alignItems: 'flex-end', | ||
}, | ||
closeButton: { | ||
marginRight: 8, | ||
marginTop: 8, | ||
width: 44, | ||
height: 44, | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
borderRadius: 22, | ||
backgroundColor: '#00000077', | ||
}, | ||
closeText: { | ||
lineHeight: 22, | ||
fontSize: 19, | ||
textAlign: 'center', | ||
color: '#FFF', | ||
includeFontPadding: false, | ||
}, | ||
}) | ||
|
||
export default ImageDefaultHeader |
152 changes: 152 additions & 0 deletions
152
src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
/** | ||
* Copyright (c) JOB TODAY S.A. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
*/ | ||
|
||
import React, {useCallback, useRef, useState} from 'react' | ||
|
||
import { | ||
Animated, | ||
ScrollView, | ||
Dimensions, | ||
StyleSheet, | ||
NativeScrollEvent, | ||
NativeSyntheticEvent, | ||
NativeMethodsMixin, | ||
} from 'react-native' | ||
|
||
import useImageDimensions from '../../hooks/useImageDimensions' | ||
import usePanResponder from '../../hooks/usePanResponder' | ||
|
||
import {getImageStyles, getImageTransform} from '../../utils' | ||
import {ImageSource} from '../../@types' | ||
import {ImageLoading} from './ImageLoading' | ||
|
||
const SWIPE_CLOSE_OFFSET = 75 | ||
const SWIPE_CLOSE_VELOCITY = 1.75 | ||
const SCREEN = Dimensions.get('window') | ||
const SCREEN_WIDTH = SCREEN.width | ||
const SCREEN_HEIGHT = SCREEN.height | ||
|
||
type Props = { | ||
imageSrc: ImageSource | ||
onRequestClose: () => void | ||
onZoom: (isZoomed: boolean) => void | ||
onLongPress: (image: ImageSource) => void | ||
delayLongPress: number | ||
swipeToCloseEnabled?: boolean | ||
doubleTapToZoomEnabled?: boolean | ||
} | ||
|
||
const ImageItem = ({ | ||
imageSrc, | ||
onZoom, | ||
onRequestClose, | ||
onLongPress, | ||
delayLongPress, | ||
swipeToCloseEnabled = true, | ||
doubleTapToZoomEnabled = true, | ||
}: Props) => { | ||
const imageContainer = useRef<ScrollView & NativeMethodsMixin>(null) | ||
const imageDimensions = useImageDimensions(imageSrc) | ||
const [translate, scale] = getImageTransform(imageDimensions, SCREEN) | ||
const scrollValueY = new Animated.Value(0) | ||
const [isLoaded, setLoadEnd] = useState(false) | ||
|
||
const onLoaded = useCallback(() => setLoadEnd(true), []) | ||
const onZoomPerformed = useCallback( | ||
(isZoomed: boolean) => { | ||
onZoom(isZoomed) | ||
if (imageContainer?.current) { | ||
imageContainer.current.setNativeProps({ | ||
scrollEnabled: !isZoomed, | ||
}) | ||
} | ||
}, | ||
[onZoom], | ||
) | ||
|
||
const onLongPressHandler = useCallback(() => { | ||
onLongPress(imageSrc) | ||
}, [imageSrc, onLongPress]) | ||
|
||
const [panHandlers, scaleValue, translateValue] = usePanResponder({ | ||
initialScale: scale || 1, | ||
initialTranslate: translate || {x: 0, y: 0}, | ||
onZoom: onZoomPerformed, | ||
doubleTapToZoomEnabled, | ||
onLongPress: onLongPressHandler, | ||
delayLongPress, | ||
}) | ||
|
||
const imagesStyles = getImageStyles( | ||
imageDimensions, | ||
translateValue, | ||
scaleValue, | ||
) | ||
const imageOpacity = scrollValueY.interpolate({ | ||
inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET], | ||
outputRange: [0.7, 1, 0.7], | ||
}) | ||
const imageStylesWithOpacity = {...imagesStyles, opacity: imageOpacity} | ||
|
||
const onScrollEndDrag = ({ | ||
nativeEvent, | ||
}: NativeSyntheticEvent<NativeScrollEvent>) => { | ||
const velocityY = nativeEvent?.velocity?.y ?? 0 | ||
const offsetY = nativeEvent?.contentOffset?.y ?? 0 | ||
|
||
if ( | ||
(Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY && | ||
offsetY > SWIPE_CLOSE_OFFSET) || | ||
offsetY > SCREEN_HEIGHT / 2 | ||
) { | ||
onRequestClose() | ||
} | ||
} | ||
|
||
const onScroll = ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => { | ||
const offsetY = nativeEvent?.contentOffset?.y ?? 0 | ||
|
||
scrollValueY.setValue(offsetY) | ||
} | ||
|
||
return ( | ||
<ScrollView | ||
ref={imageContainer} | ||
style={styles.listItem} | ||
pagingEnabled | ||
nestedScrollEnabled | ||
showsHorizontalScrollIndicator={false} | ||
showsVerticalScrollIndicator={false} | ||
contentContainerStyle={styles.imageScrollContainer} | ||
scrollEnabled={swipeToCloseEnabled} | ||
{...(swipeToCloseEnabled && { | ||
onScroll, | ||
onScrollEndDrag, | ||
})}> | ||
<Animated.Image | ||
{...panHandlers} | ||
source={imageSrc} | ||
style={imageStylesWithOpacity} | ||
onLoad={onLoaded} | ||
/> | ||
{(!isLoaded || !imageDimensions) && <ImageLoading />} | ||
</ScrollView> | ||
) | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
listItem: { | ||
width: SCREEN_WIDTH, | ||
height: SCREEN_HEIGHT, | ||
}, | ||
imageScrollContainer: { | ||
height: SCREEN_HEIGHT * 2, | ||
}, | ||
}) | ||
|
||
export default React.memo(ImageItem) |
Oops, something went wrong.