diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/assets/icons/tag-icon.png | bin | 0 -> 1708 bytes | |||
-rw-r--r-- | src/assets/images/Tagg-Triangle.png | bin | 0 -> 561 bytes | |||
-rw-r--r-- | src/assets/images/draggableX.png | bin | 0 -> 1780 bytes | |||
-rw-r--r-- | src/assets/images/tagg-rectangle.png | bin | 0 -> 521 bytes | |||
-rw-r--r-- | src/assets/images/tagg-tip.png | bin | 0 -> 233 bytes | |||
-rw-r--r-- | src/assets/images/tagg-x-button.png | bin | 0 -> 549 bytes | |||
-rw-r--r-- | src/components/common/Draggable.tsx | 364 | ||||
-rw-r--r-- | src/components/common/MomentTags.tsx | 77 | ||||
-rw-r--r-- | src/components/common/index.ts | 1 | ||||
-rw-r--r-- | src/components/moments/MomentPostContent.tsx | 27 | ||||
-rw-r--r-- | src/components/notifications/Notification.tsx | 147 | ||||
-rw-r--r-- | src/components/taggs/TaggDraggable.tsx | 129 | ||||
-rw-r--r-- | src/constants/api.ts | 1 | ||||
-rw-r--r-- | src/screens/moments/TagFriendsScreen.tsx | 20 | ||||
-rw-r--r-- | src/screens/profile/CaptionScreen.tsx | 2 | ||||
-rw-r--r-- | src/screens/profile/IndividualMoment.tsx | 5 | ||||
-rw-r--r-- | src/services/MomentService.ts | 30 | ||||
-rw-r--r-- | src/types/types.ts | 10 |
18 files changed, 731 insertions, 82 deletions
diff --git a/src/assets/icons/tag-icon.png b/src/assets/icons/tag-icon.png Binary files differnew file mode 100644 index 00000000..9111b036 --- /dev/null +++ b/src/assets/icons/tag-icon.png diff --git a/src/assets/images/Tagg-Triangle.png b/src/assets/images/Tagg-Triangle.png Binary files differnew file mode 100644 index 00000000..dfe97282 --- /dev/null +++ b/src/assets/images/Tagg-Triangle.png diff --git a/src/assets/images/draggableX.png b/src/assets/images/draggableX.png Binary files differnew file mode 100644 index 00000000..b99c5dfc --- /dev/null +++ b/src/assets/images/draggableX.png diff --git a/src/assets/images/tagg-rectangle.png b/src/assets/images/tagg-rectangle.png Binary files differnew file mode 100644 index 00000000..24bfd67e --- /dev/null +++ b/src/assets/images/tagg-rectangle.png diff --git a/src/assets/images/tagg-tip.png b/src/assets/images/tagg-tip.png Binary files differnew file mode 100644 index 00000000..eb3c5bad --- /dev/null +++ b/src/assets/images/tagg-tip.png diff --git a/src/assets/images/tagg-x-button.png b/src/assets/images/tagg-x-button.png Binary files differnew file mode 100644 index 00000000..5ce3846d --- /dev/null +++ b/src/assets/images/tagg-x-button.png 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'; diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/MomentPostContent.tsx index 193bf40c..e702cb68 100644 --- a/src/components/moments/MomentPostContent.tsx +++ b/src/components/moments/MomentPostContent.tsx @@ -1,19 +1,20 @@ import {useNavigation} from '@react-navigation/native'; -import React, {useEffect} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {Image, StyleSheet, Text, View, ViewProps} from 'react-native'; import {useDispatch, useStore} from 'react-redux'; -import {getCommentsCount} from '../../services'; +import {getCommentsCount, loadMomentTags} from '../../services'; import {RootState} from '../../store/rootReducer'; -import {ScreenType, UserType} from '../../types'; +import {MomentTagType, ScreenType, UserType} from '../../types'; import { getTimePosted, navigateToProfile, + normalize, SCREEN_HEIGHT, SCREEN_WIDTH, - normalize, } from '../../utils'; import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments'; import {CommentsCount} from '../comments'; +import {MomentTags} from '../common'; interface MomentPostContentProps extends ViewProps { screenType: ScreenType; @@ -30,11 +31,23 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ dateTime, style, }) => { - const [elapsedTime, setElapsedTime] = React.useState<string>(); - const [comments_count, setCommentsCount] = React.useState(''); + const [elapsedTime, setElapsedTime] = useState(''); + const [comments_count, setCommentsCount] = useState(''); + const [tags, setTags] = useState<MomentTagType[]>([]); const state: RootState = useStore().getState(); const navigation = useNavigation(); const dispatch = useDispatch(); + const imageRef = useRef(null); + + useEffect(() => { + const loadTags = async () => { + const response = await loadMomentTags(momentId); + if (response) { + setTags(response); + } + }; + loadTags(); + }, []); useEffect(() => { const fetchCommentsCount = async () => { @@ -48,10 +61,12 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ return ( <View style={[styles.container, style]}> <Image + ref={imageRef} style={styles.image} source={{uri: pathHash}} resizeMode={'cover'} /> + <MomentTags editing={false} tags={tags} imageRef={imageRef} /> <View style={styles.footerContainer}> <CommentsCount commentsCount={comments_count} diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx index cb62047a..3f9cc56a 100644 --- a/src/components/notifications/Notification.tsx +++ b/src/components/notifications/Notification.tsx @@ -84,7 +84,8 @@ const Notification: React.FC<NotificationProps> = (props) => { let obj; if ( notification_type === 'MOM_3+' || - notification_type === 'MOM_FRIEND' + notification_type === 'MOM_FRIEND' || + notification_type === 'MOM_TAG' ) { obj = notification_object as MomentType; url = obj.thumbnail_url; @@ -185,6 +186,7 @@ const Notification: React.FC<NotificationProps> = (props) => { break; case 'MOM_3+': case 'MOM_FRIEND': + case 'MOM_TAG': const object = notification_object as MomentType; await fetchUserX( dispatch, @@ -236,83 +238,88 @@ const Notification: React.FC<NotificationProps> = (props) => { style={styles.avatarContainer}> <Avatar style={styles.avatar} uri={avatar} /> </TouchableWithoutFeedback> - {notification_type === 'SYSTEM_MSG' ? ( - // Only verbage + {notification_type === 'SYSTEM_MSG' || notification_type === 'MOM_TAG' ? ( + // Single-line body text with timestamp <View style={styles.contentContainer}> <View style={styles.textContainerStyles}> - <Text style={styles.actorName}>{verbage}</Text> - <Text style={styles.timeStampStyles}> - {getTimeInShorthand(timestamp)} - </Text> - </View> - </View> - ) : ( - <> - {/* Text content: Actor name and verbage*/} - <View style={styles.contentContainer}> - <TouchableWithoutFeedback onPress={navigateToProfile}> - <Text style={styles.actorName}>{notification_title}</Text> - </TouchableWithoutFeedback> - <TouchableWithoutFeedback - style={styles.textContainerStyles} - onPress={onNotificationTap}> - <Text style={styles.verbageStyles}> - {verbage} + {notification_type === 'SYSTEM_MSG' ? ( + <Text style={styles.actorName}>{verbage}</Text> + ) : ( + <Text> + <Text style={styles.actorName}>{notification_title} </Text> + <Text style={styles.verbageStyles}>{verbage} </Text> <Text style={styles.timeStampStyles}> - {' '} {getTimeInShorthand(timestamp)} </Text> </Text> - {/* <Text style={styles.verbageStyles}>{verbage}</Text> */} - </TouchableWithoutFeedback> - </View> - {/* Friend request accept/decline button */} - {notification_type === 'FRD_REQ' && ( - <View style={styles.buttonsContainer}> - <AcceptDeclineButtons - requester={{id, username, first_name, last_name}} - onAccept={handleAcceptRequest} - onReject={handleDeclineFriendRequest} - /> - </View> - )} - {notification_type === 'FRD_ACPT' && ( - <View style={styles.buttonsContainer}> - <MessageButton - userXId={id} - isBlocked={false} - friendship_status={'friends'} - externalStyles={{ - container: { - width: normalize(63), - height: normalize(21), - marginTop: '7%', - }, - buttonTitle: { - fontSize: normalize(11), - lineHeight: normalize(13.13), - letterSpacing: normalize(0.5), - fontWeight: '700', - textAlign: 'center', - }, - }} - solid - /> - </View> - )} - {/* Moment Image Preview */} - {(notification_type === 'CMT' || - notification_type === 'MOM_3+' || - notification_type === 'MOM_FRIEND') && - notification_object && ( - <TouchableWithoutFeedback - style={styles.moment} - onPress={onNotificationTap}> - <Image style={styles.imageFlex} source={{uri: momentURI}} /> - </TouchableWithoutFeedback> )} - </> + </View> + </View> + ) : ( + // Two-line title and body text with timestamp + <View style={styles.contentContainer}> + <TouchableWithoutFeedback onPress={navigateToProfile}> + <Text style={styles.actorName}>{notification_title}</Text> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback + style={styles.textContainerStyles} + onPress={onNotificationTap}> + <Text> + <Text style={styles.verbageStyles}>{verbage} </Text> + <Text style={styles.timeStampStyles}> + {getTimeInShorthand(timestamp)} + </Text> + </Text> + </TouchableWithoutFeedback> + </View> + )} + {/* Friend request accept/decline button */} + {notification_type === 'FRD_REQ' && ( + <View style={styles.buttonsContainer}> + <AcceptDeclineButtons + requester={{id, username, first_name, last_name}} + onAccept={handleAcceptRequest} + onReject={handleDeclineFriendRequest} + /> + </View> + )} + {/* Message button when user accepts friend request */} + {notification_type === 'FRD_ACPT' && ( + <View style={styles.buttonsContainer}> + <MessageButton + userXId={id} + isBlocked={false} + friendship_status={'friends'} + externalStyles={{ + container: { + width: normalize(63), + height: normalize(21), + marginTop: '7%', + }, + buttonTitle: { + fontSize: normalize(11), + lineHeight: normalize(13.13), + letterSpacing: normalize(0.5), + fontWeight: '700', + textAlign: 'center', + }, + }} + solid + /> + </View> )} + {/* Moment Image Preview */} + {(notification_type === 'CMT' || + notification_type === 'MOM_3+' || + notification_type === 'MOM_TAG' || + notification_type === 'MOM_FRIEND') && + notification_object && ( + <TouchableWithoutFeedback + style={styles.moment} + onPress={onNotificationTap}> + <Image style={styles.imageFlex} source={{uri: momentURI}} /> + </TouchableWithoutFeedback> + )} </View> ); diff --git a/src/components/taggs/TaggDraggable.tsx b/src/components/taggs/TaggDraggable.tsx new file mode 100644 index 00000000..e4448642 --- /dev/null +++ b/src/components/taggs/TaggDraggable.tsx @@ -0,0 +1,129 @@ +import {useNavigation} from '@react-navigation/native'; +import React, {useEffect, useRef} from 'react'; +import { + Image, + StyleSheet, + Text, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from 'react-native'; +import {useDispatch, useSelector} from 'react-redux'; +import Avatar from '../../components/common/Avatar'; +import {RootState} from '../../store/rootReducer'; +import {ProfilePreviewType, ScreenType, UserType} from '../../types'; +import {normalize} from '../../utils'; +import {navigateToProfile} from '../../utils/users'; + +interface TaggDraggableProps { + taggedUser: ProfilePreviewType; + editingView: boolean; + deleteFromList: () => void; + setStart: Function; +} + +const TaggDraggable: React.FC<TaggDraggableProps> = ( + props: TaggDraggableProps, +) => { + const navigation = useNavigation(); + const dispatch = useDispatch(); + const state = useSelector((rs: RootState) => rs); + const {taggedUser, editingView, deleteFromList, setStart} = props; + let uriX = require('../../assets/images/draggableX.png'); + let uriTip = require('../../assets/images/Tagg-Triangle.png'); + + const draggableRef = useRef(null); + + useEffect(() => { + draggableRef.current.measure((width: number, height: number) => { + setStart([width, height]); + }); + }, []); + + const user: UserType = { + userId: taggedUser.id, + username: taggedUser.username, + }; + + return ( + <TouchableWithoutFeedback> + <View style={styles.container}> + <Image style={styles.imageTip} source={uriTip} /> + <TouchableOpacity + onPressIn={() => + navigateToProfile( + state, + dispatch, + navigation, + ScreenType.Profile, + user, + ) + } + disabled={editingView} + style={styles.content} + ref={draggableRef}> + <Avatar style={styles.avatar} uri={taggedUser.thumbnail_url} /> + <Text style={editingView ? styles.buttonTitle : styles.buttonTitleX}> + @{taggedUser.username} + </Text> + {editingView && ( + <TouchableOpacity + disabled={false} + onPressIn={deleteFromList} + style={styles.imageX}> + <Image style={styles.imageX} source={uriX} /> + </TouchableOpacity> + )} + </TouchableOpacity> + </View> + </TouchableWithoutFeedback> + ); +}; + +const styles = StyleSheet.create({ + imageTip: { + height: normalize(12), + aspectRatio: 12 / 8, + }, + container: { + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + buttonTitle: { + color: 'white', + fontSize: normalize(11), + fontWeight: '700', + lineHeight: normalize(13.13), + letterSpacing: normalize(0.6), + paddingHorizontal: '1%', + }, + buttonTitleX: { + color: 'white', + fontSize: normalize(11), + fontWeight: '700', + lineHeight: normalize(13.13), + letterSpacing: normalize(0.6), + paddingHorizontal: '1%', + }, + avatar: { + height: normalize(20), + width: normalize(20), + borderRadius: normalize(20) / 2, + }, + imageX: { + width: normalize(15), + height: normalize(15), + }, + content: { + justifyContent: 'space-evenly', + alignItems: 'center', + flexDirection: 'row', + borderRadius: 20, + paddingVertical: normalize(5), + paddingHorizontal: normalize(8), + backgroundColor: 'rgba(0, 0, 0, 0.8)', + }, +}); + +export default TaggDraggable; diff --git a/src/constants/api.ts b/src/constants/api.ts index 6cc357f5..d52fc203 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -31,6 +31,7 @@ export const SEARCH_ENDPOINT: string = API_URL + 'search/'; export const SEARCH_ENDPOINT_MESSAGES: string = API_URL + 'search/messages/'; export const SEARCH_ENDPOINT_SUGGESTED: string = API_URL + 'search/suggested/'; export const MOMENTS_ENDPOINT: string = API_URL + 'moments/'; +export const MOMENT_TAGS_ENDPOINT: string = API_URL + 'moments/tags/'; export const MOMENT_THUMBNAIL_ENDPOINT: string = API_URL + 'moment-thumbnail/'; export const VERIFY_INVITATION_CODE_ENDPOUNT: string = API_URL + 'verify-code/'; export const COMMENTS_ENDPOINT: string = API_URL + 'comments/'; diff --git a/src/screens/moments/TagFriendsScreen.tsx b/src/screens/moments/TagFriendsScreen.tsx index 941fea3e..ba180921 100644 --- a/src/screens/moments/TagFriendsScreen.tsx +++ b/src/screens/moments/TagFriendsScreen.tsx @@ -1,6 +1,6 @@ import {RouteProp} from '@react-navigation/core'; import {useNavigation} from '@react-navigation/native'; -import React, {Fragment, useEffect, useState} from 'react'; +import React, {Fragment, useEffect, useRef, useState} from 'react'; import { Image, Keyboard, @@ -14,6 +14,7 @@ import {Button} from 'react-native-elements'; import {MainStackParams} from 'src/routes'; import { CaptionScreenHeader, + MomentTags, SearchBackground, TaggLoadingIndicator, } from '../../components'; @@ -32,7 +33,7 @@ interface TagFriendsScreenProps { const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { const {image, selectedUsers} = route.params; const navigation = useNavigation(); - const [loading, setLoading] = useState(false); + const imageRef = useRef(null); const [taggedUsers, setTaggedUsers] = useState<ProfilePreviewType[]>([]); /* @@ -54,7 +55,6 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { return ( <SearchBackground> - {loading ? <TaggLoadingIndicator fullscreen /> : <Fragment />} <TouchableWithoutFeedback onPress={Keyboard.dismiss}> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} @@ -78,10 +78,24 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { title={'Tap on photo to Tag friends!'} /> <Image + ref={imageRef} style={styles.image} source={{uri: image.path}} resizeMode={'cover'} /> + <MomentTags + editing={true} + tags={taggedUsers.map((user) => ({ + id: '', + x: 0, + y: 0, + user, + }))} + imageRef={imageRef} + deleteFromList={(user) => + setTaggedUsers(taggedUsers.filter((u) => u.id !== user.id)) + } + /> <View style={{marginHorizontal: '5%', marginTop: '3%'}}> <TagFriendsFooter taggedUsers={taggedUsers} diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index dbfb3a4a..2093a1f9 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -56,6 +56,7 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { const [loading, setLoading] = useState(false); const [taggedUsers, setTaggedUsers] = useState<ProfilePreviewType[]>([]); const [taggedList, setTaggedList] = useState<string>(''); + useEffect(() => { setTaggedUsers(selectedUsers ? selectedUsers : []); }, [route.params]); @@ -129,6 +130,7 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { /> </View> <CaptionScreenHeader style={styles.header} {...{title: title}} /> + {/* this is the image we want to center our tags' initial location within */} <Image style={styles.image} source={{uri: image.path}} diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx index 515cbacf..4baca5b2 100644 --- a/src/screens/profile/IndividualMoment.tsx +++ b/src/screens/profile/IndividualMoment.tsx @@ -1,14 +1,17 @@ +import AsyncStorage from '@react-native-community/async-storage'; import {BlurView} from '@react-native-community/blur'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import React from 'react'; -import {FlatList, StyleSheet, View} from 'react-native'; +import {Alert, FlatList, StyleSheet, View} from 'react-native'; import {useSelector} from 'react-redux'; import { IndividualMomentTitleBar, MomentPostContent, MomentPostHeader, } from '../../components'; +import {MOMENT_TAGS_ENDPOINT} from '../../constants'; +import {ERROR_SOMETHING_WENT_WRONG_REFRESH} from '../../constants/strings'; import {MainStackParams} from '../../routes'; import {RootState} from '../../store/rootreducer'; import {MomentType} from '../../types'; diff --git a/src/services/MomentService.ts b/src/services/MomentService.ts index 2354d18e..a26a1abb 100644 --- a/src/services/MomentService.ts +++ b/src/services/MomentService.ts @@ -1,7 +1,11 @@ import AsyncStorage from '@react-native-community/async-storage'; import RNFetchBlob from 'rn-fetch-blob'; -import {MOMENTS_ENDPOINT, MOMENT_THUMBNAIL_ENDPOINT} from '../constants'; -import {MomentType} from '../types'; +import { + MOMENTS_ENDPOINT, + MOMENT_TAGS_ENDPOINT, + MOMENT_THUMBNAIL_ENDPOINT, +} from '../constants'; +import {MomentTagType, MomentType} from '../types'; import {checkImageUploadStatus} from '../utils'; export const postMoment: ( @@ -116,3 +120,25 @@ export const loadMomentThumbnail = async (momentId: string) => { return undefined; } }; + +export const loadMomentTags = async (moment_id: string) => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch( + MOMENT_TAGS_ENDPOINT + `?moment_id=${moment_id}`, + { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }, + ); + if (response.status === 200) { + const tags: MomentTagType[] = await response.json(); + return tags; + } + } catch (error) { + console.error(error); + return []; + } +}; diff --git a/src/types/types.ts b/src/types/types.ts index 7bf5597f..e957483b 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -117,6 +117,14 @@ export interface MomentType { moment_url: string; thumbnail_url: string; } + +export interface MomentTagType { + id: string; + user: ProfilePreviewType; + x: number; + y: number; +} + export interface CommentBaseType { comment_id: string; comment: string; @@ -241,6 +249,8 @@ export type TypeOfNotification = | 'MOM_FRIEND' // notification_object is undefined | 'INVT_ONBRD' + // notification_object is MomentType + | 'MOM_TAG' // notification_object is undefined | 'SYSTEM_MSG'; |