aboutsummaryrefslogtreecommitdiff
path: root/src/components/common
diff options
context:
space:
mode:
authorIvan Chen <ivan@tagg.id>2021-05-21 20:34:30 -0400
committerIvan Chen <ivan@tagg.id>2021-05-21 20:34:30 -0400
commit442f5608aeddb5c627183e150a8c79c9d5bd2a57 (patch)
tree4346360538d0a50407ce7d76a5e8ce6b168c52aa /src/components/common
parentb4a4639f2ed05c02b9061d9febddf8339bc1fe26 (diff)
parent4849c65ff2163e1a77dcb26a12ff68840df225e7 (diff)
Merge branch 'master' into tma853-tag-selection-screen
# Conflicts: # src/components/common/index.ts # src/screens/profile/CaptionScreen.tsx
Diffstat (limited to 'src/components/common')
-rw-r--r--src/components/common/Draggable.tsx364
-rw-r--r--src/components/common/MomentTags.tsx77
-rw-r--r--src/components/common/index.ts1
3 files changed, 442 insertions, 0 deletions
diff --git a/src/components/common/Draggable.tsx b/src/components/common/Draggable.tsx
new file mode 100644
index 00000000..edd29b78
--- /dev/null
+++ b/src/components/common/Draggable.tsx
@@ -0,0 +1,364 @@
+/**
+ * * https://github.com/tongyy/react-native-draggable
+ *
+ */
+
+import React from 'react';
+import {
+ Animated,
+ Dimensions,
+ GestureResponderEvent,
+ Image,
+ PanResponder,
+ PanResponderGestureState,
+ StyleProp,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+ ViewStyle,
+} from 'react-native';
+
+function clamp(number: number, min: number, max: number) {
+ return Math.max(min, Math.min(number, max));
+}
+
+interface IProps {
+ /**** props that should probably be removed in favor of "children" */
+ renderText?: string;
+ isCircle?: boolean;
+ renderSize?: number;
+ imageSource?: number;
+ renderColor?: string;
+ /**** */
+ children?: React.ReactNode;
+ shouldReverse?: boolean;
+ disabled?: boolean;
+ debug?: boolean;
+ animatedViewProps?: object;
+ touchableOpacityProps?: object;
+ onDrag?: (
+ e: GestureResponderEvent,
+ gestureState: PanResponderGestureState,
+ ) => void;
+ onShortPressRelease?: (event: GestureResponderEvent) => void;
+ onDragRelease?: (
+ e: GestureResponderEvent,
+ gestureState: PanResponderGestureState,
+ ) => void;
+ onLongPress?: (event: GestureResponderEvent) => void;
+ onPressIn?: (event: GestureResponderEvent) => void;
+ onPressOut?: (event: GestureResponderEvent) => void;
+ onRelease?: (event: GestureResponderEvent, wasDragging: boolean) => void;
+ onReverse?: () => {x: number; y: number};
+ x?: number;
+ y?: number;
+ // z/elevation should be removed because it doesn't sync up visually and haptically
+ z?: number;
+ minX?: number;
+ minY?: number;
+ maxX?: number;
+ maxY?: number;
+ onDragStart?: () => void;
+}
+
+export default function Draggable(props: IProps) {
+ const {
+ renderText,
+ isCircle,
+ renderSize,
+ imageSource,
+ renderColor,
+ children,
+ shouldReverse,
+ disabled,
+ debug,
+ animatedViewProps,
+ touchableOpacityProps,
+ onDrag,
+ onShortPressRelease,
+ onDragRelease,
+ onLongPress,
+ onPressIn,
+ onPressOut,
+ onRelease,
+ x,
+ y,
+ z,
+ minX,
+ minY,
+ maxX,
+ maxY,
+ onDragStart,
+ } = props;
+
+ // The Animated object housing our xy value so that we can spring back
+ const pan = React.useRef(new Animated.ValueXY());
+ // Always set to xy value of pan, would like to remove
+ const offsetFromStart = React.useRef({x: 0, y: 0});
+ // Width/Height of Draggable (renderSize is arbitrary if children are passed in)
+ const childSize = React.useRef({x: renderSize, y: renderSize});
+ // Top/Left/Right/Bottom location on screen from start of most recent touch
+ const startBounds = React.useRef({top: 0, bottom: 0, left: 0, right: 0});
+ // Whether we're currently dragging or not
+ const isDragging = React.useRef(false);
+
+ // const [zIndex, setZIndex] = React.useState(z);
+ const zIndex = z;
+
+ const getBounds = React.useCallback(() => {
+ const left = x + offsetFromStart.current.x;
+ const top = y + offsetFromStart.current.y;
+ return {
+ left,
+ top,
+ right: left + childSize.current.x,
+ bottom: top + childSize.current.y,
+ };
+ }, [x, y]);
+
+ const shouldStartDrag = React.useCallback(
+ (gs) => {
+ return !disabled && (Math.abs(gs.dx) > 2 || Math.abs(gs.dy) > 2);
+ },
+ [disabled],
+ );
+
+ const reversePosition = React.useCallback(() => {
+ Animated.spring(pan.current, {
+ toValue: {x: 0, y: 0},
+ useNativeDriver: false,
+ }).start();
+ }, [pan]);
+
+ const onPanResponderRelease = React.useCallback(
+ (e: GestureResponderEvent, gestureState: PanResponderGestureState) => {
+ isDragging.current = false;
+ if (onDragRelease) {
+ onDragRelease(e, gestureState);
+ onRelease(e, true);
+ }
+ if (!shouldReverse) {
+ pan.current.flattenOffset();
+ } else {
+ reversePosition();
+ }
+ },
+ [onDragRelease, shouldReverse, onRelease, reversePosition],
+ );
+
+ const onPanResponderGrant = React.useCallback(
+ (_: GestureResponderEvent) => {
+ startBounds.current = getBounds();
+ isDragging.current = true;
+ if (!shouldReverse) {
+ pan.current.setOffset(offsetFromStart.current);
+ pan.current.setValue({x: 0, y: 0});
+ }
+ onDragStart();
+ },
+ [getBounds, shouldReverse],
+ );
+
+ const handleOnDrag = React.useCallback(
+ (e: GestureResponderEvent, gestureState: PanResponderGestureState) => {
+ const {dx, dy} = gestureState;
+ const {top, right, left, bottom} = startBounds.current;
+ const far = 999999999;
+ const changeX = clamp(
+ dx,
+ Number.isFinite(minX) ? minX - left : -far,
+ Number.isFinite(maxX) ? maxX - right : far,
+ );
+ const changeY = clamp(
+ dy,
+ Number.isFinite(minY) ? minY - top : -far,
+ Number.isFinite(maxY) ? maxY - bottom : far,
+ );
+ pan.current.setValue({x: changeX, y: changeY});
+ onDrag(e, gestureState);
+ },
+ [maxX, maxY, minX, minY, onDrag],
+ );
+
+ const panResponder = React.useMemo(() => {
+ return PanResponder.create({
+ onMoveShouldSetPanResponder: (_, gestureState) =>
+ shouldStartDrag(gestureState),
+ onMoveShouldSetPanResponderCapture: (_, gestureState) =>
+ shouldStartDrag(gestureState),
+ onPanResponderGrant,
+ // onPanResponderStart,
+ onPanResponderMove: Animated.event([], {
+ // Typed incorrectly https://reactnative.dev/docs/panresponder
+ listener: handleOnDrag,
+ useNativeDriver: false,
+ }),
+ // onPanResponderRelease: (_) => {
+ // // console.log('end');
+ // // setZIndex(1);
+ // },
+ });
+ }, [
+ handleOnDrag,
+ onPanResponderGrant,
+ onPanResponderRelease,
+ shouldStartDrag,
+ ]);
+
+ // TODO Figure out a way to destroy and remove offsetFromStart entirely
+ React.useEffect(() => {
+ const curPan = pan.current; // Using an instance to avoid losing the pointer before the cleanup
+ if (!shouldReverse) {
+ curPan.addListener((c) => (offsetFromStart.current = c));
+ }
+ return () => {
+ // Typed incorrectly
+ curPan.removeAllListeners();
+ };
+ }, [shouldReverse]);
+
+ const positionCss: StyleProp<ViewStyle> = React.useMemo(() => {
+ const Window = Dimensions.get('window');
+
+ return {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: Window.width,
+ height: Window.height,
+ elevation: zIndex,
+ zIndex: zIndex,
+ };
+ }, [zIndex]);
+
+ const dragItemCss = React.useMemo(() => {
+ const style: StyleProp<ViewStyle> = {
+ top: y,
+ left: x,
+ elevation: zIndex,
+ zIndex: zIndex,
+ };
+ if (renderColor) {
+ style.backgroundColor = renderColor;
+ }
+ if (isCircle) {
+ style.borderRadius = renderSize;
+ }
+
+ if (children) {
+ return {
+ ...style,
+ alignSelf: 'baseline',
+ };
+ }
+ return {
+ ...style,
+ justifyContent: 'center',
+ width: renderSize,
+ height: renderSize,
+ };
+ }, [children, isCircle, renderColor, renderSize, x, y, zIndex]);
+
+ const touchableContent = React.useMemo(() => {
+ if (children) {
+ return children;
+ } else if (imageSource) {
+ return (
+ <Image
+ style={{width: renderSize, height: renderSize}}
+ source={imageSource}
+ />
+ );
+ } else {
+ return <Text style={styles.text}>{renderText}</Text>;
+ }
+ }, [children, imageSource, renderSize, renderText]);
+
+ const handleOnLayout = React.useCallback((event) => {
+ const {height, width} = event.nativeEvent.layout;
+ childSize.current = {x: width, y: height};
+ }, []);
+
+ // const handlePressOut = React.useCallback(
+ // (event: GestureResponderEvent) => {
+ // onPressOut(event);
+ // if (!isDragging.current) {
+ // onRelease(event, false);
+ // }
+ // },
+ // [onPressOut, onRelease],
+ // );
+
+ const getDebugView = React.useCallback(() => {
+ const {width, height} = Dimensions.get('window');
+ const far = 9999;
+ const constrained = minX || maxX || minY || maxY;
+ if (!constrained) {
+ return null;
+ } // could show other debug info here
+ const left = minX || -far;
+ const right = maxX ? width - maxX : -far;
+ const top = minY || -far;
+ const bottom = maxY ? height - maxY : -far;
+ return (
+ <View
+ pointerEvents="box-none"
+ style={{left, right, top, bottom, ...styles.debugView}}
+ />
+ );
+ }, [maxX, maxY, minX, minY]);
+
+ return (
+ <View pointerEvents="box-none" style={positionCss}>
+ {debug && getDebugView()}
+ <Animated.View
+ pointerEvents="box-none"
+ {...animatedViewProps}
+ {...panResponder.panHandlers}
+ style={pan.current.getLayout()}>
+ <TouchableOpacity
+ {...touchableOpacityProps}
+ onLayout={handleOnLayout}
+ style={dragItemCss}
+ disabled={true}
+ onPress={onShortPressRelease}
+ onLongPress={onLongPress}
+ onPressIn={onPressIn}
+ onPressOut={onPressOut}>
+ {touchableContent}
+ </TouchableOpacity>
+ </Animated.View>
+ </View>
+ );
+}
+
+/***** Default props and types */
+
+Draggable.defaultProps = {
+ renderText: '+',
+ renderSize: 36,
+ shouldReverse: false,
+ disabled: false,
+ debug: false,
+ onDrag: () => {},
+ onShortPressRelease: () => {},
+ onDragRelease: () => {},
+ onLongPress: () => {},
+ onPressIn: () => {},
+ onPressOut: () => {},
+ onRelease: () => {},
+ x: 0,
+ y: 0,
+ z: 1,
+};
+
+const styles = StyleSheet.create({
+ text: {color: '#fff', textAlign: 'center'},
+ debugView: {
+ backgroundColor: '#ff000044',
+ position: 'absolute',
+ borderColor: '#fced0ecc',
+ borderWidth: 4,
+ },
+});
diff --git a/src/components/common/MomentTags.tsx b/src/components/common/MomentTags.tsx
new file mode 100644
index 00000000..fb9ef5be
--- /dev/null
+++ b/src/components/common/MomentTags.tsx
@@ -0,0 +1,77 @@
+import React, {MutableRefObject, useEffect, useState} from 'react';
+import {MomentTagType, ProfilePreviewType} from '../../types';
+import TaggDraggable from '../taggs/TaggDraggable';
+import Draggable from './Draggable';
+
+interface MomentTagsProps {
+ editing: boolean;
+ tags: MomentTagType[];
+ imageRef: MutableRefObject<null>;
+ deleteFromList?: (user: ProfilePreviewType) => void;
+}
+
+const MomentTags: React.FC<MomentTagsProps> = ({
+ editing,
+ tags,
+ imageRef,
+ deleteFromList,
+}) => {
+ const [offset, setOffset] = useState([0, 0]);
+ const [curStart, setCurStart] = useState([0, 0]);
+ const [imageDimensions, setImageDimensions] = useState([0, 0]);
+
+ useEffect(() => {
+ imageRef.current.measure((fx, fy, width, height, px, py) => {
+ setOffset([px, py]);
+ setImageDimensions([width, height]);
+ });
+ }, []);
+
+ if (!tags) {
+ return null;
+ }
+
+ return editing && deleteFromList ? (
+ <>
+ {tags.map((tag) => (
+ <Draggable
+ x={imageDimensions[0] / 2 - curStart[0] / 2 + offset[0]}
+ y={offset[1] + imageDimensions[1] / 2 - curStart[1] / 2}
+ minX={offset[0]}
+ minY={offset[1]}
+ maxX={imageDimensions[0] + offset[0]}
+ maxY={imageDimensions[1] + offset[1]}
+ onDragStart={() => null}>
+ <TaggDraggable
+ taggedUser={tag.user}
+ editingView={true}
+ deleteFromList={() => deleteFromList(tag.user)}
+ setStart={setCurStart}
+ />
+ </Draggable>
+ ))}
+ </>
+ ) : (
+ <>
+ {tags.map((tag) => (
+ <Draggable
+ x={imageDimensions[0] / 2 - curStart[0] / 2 + tag.x}
+ y={imageDimensions[0] / 2 - curStart[0] / 2 + tag.y}
+ minX={offset[0]}
+ minY={offset[1]}
+ maxX={imageDimensions[0] + offset[0]}
+ maxY={imageDimensions[1] + offset[1]}
+ onDragStart={() => null}>
+ <TaggDraggable
+ taggedUser={tag.user}
+ editingView={editing}
+ deleteFromList={() => null}
+ setStart={setCurStart}
+ />
+ </Draggable>
+ ))}
+ </>
+ );
+};
+
+export default MomentTags;
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index 44edbe5f..4f5c0232 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -28,3 +28,4 @@ export {default as TaggTypeahead} from './TaggTypeahead';
export {default as TaggUserRowCell} from './TaggUserRowCell';
export {default as LikeButton} from './LikeButton';
export {default as TaggUserSelectionCell} from './TaggUserSelectionCell';
+export {default as MomentTags} from './MomentTags';