diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/comments/ZoomInCropper.tsx | 26 | ||||
-rw-r--r-- | src/components/moments/Moment.tsx | 72 | ||||
-rw-r--r-- | src/components/moments/legacy/MomentPostContent.tsx | 42 | ||||
-rw-r--r-- | src/components/profile/MomentMoreInfoDrawer.tsx | 11 | ||||
-rw-r--r-- | src/components/profile/ProfileBody.tsx | 1 | ||||
-rw-r--r-- | src/constants/api.ts | 1 | ||||
-rw-r--r-- | src/constants/regex.ts | 2 | ||||
-rw-r--r-- | src/routes/main/MainStackNavigator.tsx | 14 | ||||
-rw-r--r-- | src/screens/chat/ChatListScreen.tsx | 2 | ||||
-rw-r--r-- | src/screens/moments/TagFriendsScreen.tsx | 150 | ||||
-rw-r--r-- | src/screens/profile/CaptionScreen.tsx | 117 | ||||
-rw-r--r-- | src/services/MomentService.ts | 124 | ||||
-rw-r--r-- | src/types/types.ts | 16 |
13 files changed, 435 insertions, 143 deletions
diff --git a/src/components/comments/ZoomInCropper.tsx b/src/components/comments/ZoomInCropper.tsx index 94e772b6..bca4e599 100644 --- a/src/components/comments/ZoomInCropper.tsx +++ b/src/components/comments/ZoomInCropper.tsx @@ -25,7 +25,7 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({ route, navigation, }) => { - const {screenType, title, image} = route.params; + const {screenType, title, media} = route.params; const [aspectRatio, setAspectRatio] = useState<number>(1); // Stores the coordinates of the cropped image @@ -50,9 +50,9 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({ // Setting original aspect ratio of image useEffect(() => { - if (image.sourceURL) { + if (media.uri) { Image.getSize( - image.sourceURL, + media.uri, (w, h) => { setAspectRatio(w / h); }, @@ -67,10 +67,9 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({ x0 !== undefined && x1 !== undefined && y0 !== undefined && - y1 !== undefined && - image.sourceURL + y1 !== undefined ) { - PhotoManipulator.crop(image.sourceURL, { + PhotoManipulator.crop(media.uri, { x: x0, y: y1, width: Math.abs(x0 - x1), @@ -80,7 +79,11 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({ navigation.navigate('CaptionScreen', { screenType, title: title, - image: {filename: croppedURL, path: croppedURL}, + media: { + filename: media.filename, + uri: croppedURL, + isVideo: false, + }, }); }) .catch((err) => console.log('err: ', err)); @@ -88,13 +91,12 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({ x0 === undefined && x1 === undefined && y0 === undefined && - y1 === undefined && - image.sourceURL + y1 === undefined ) { navigation.navigate('CaptionScreen', { screenType, title: title, - image: {filename: image.sourceURL, path: image.sourceURL}, + media, }); } }; @@ -104,7 +106,7 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({ */ const onMove = (position: IOnMove) => { Image.getSize( - image.path, + media.uri, (w, h) => { const x = position.positionX; const y = position.positionY; @@ -154,7 +156,7 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({ <Image style={{width: SCREEN_WIDTH, height: SCREEN_WIDTH / aspectRatio}} source={{ - uri: image.sourceURL, + uri: media.uri, }} /> </ImageZoom> diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx index 81e23076..47de1c6a 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -43,6 +43,42 @@ const Moment: React.FC<MomentProps> = ({ }) => { const navigation = useNavigation(); + const navigateToCaptionScreenForVideo = (uri: string) => { + const randHash = Math.random().toString(36).substring(7); + navigation.navigate('CaptionScreen', { + screenType, + title, + media: { + filename: `poc_${randHash}.mov`, + uri, + isVideo: true, + }, + }); + }; + /** + * This function opens the ImagePicker, only lets you select video files, + * formats the file extension, then makes a call to the server to get the presigned URL, + * after which it makes a POST request to the returned URL to upload the file directly to S3. + * params: none + * @returns: none + */ + const navigateToVideoPicker = () => { + ImagePicker.openPicker({ + mediaType: 'video', + }) + .then(async (vid) => { + console.log(vid); + if (vid.path) { + navigateToCaptionScreenForVideo(vid.path); + } + }) + .catch((err) => { + if (err.code && err.code !== 'E_PICKER_CANCELLED') { + Alert.alert(ERROR_UPLOAD); + } + }); + }; + const navigateToImagePicker = () => { ImagePicker.openPicker({ smartAlbums: [ @@ -55,11 +91,15 @@ const Moment: React.FC<MomentProps> = ({ mediaType: 'any', }) .then((picture) => { - if ('path' in picture) { + if (picture.path && picture.filename) { navigation.navigate('ZoomInCropper', { screenType, - title: title, - image: picture, + title, + media: { + filename: picture.filename, + uri: picture.path, + isVideo: false, + }, }); } }) @@ -106,6 +146,32 @@ const Moment: React.FC<MomentProps> = ({ <PlusIcon width={23} height={23} + onPress={() => + Alert.alert('Video Upload', 'pick one', [ + { + text: 'gallery', + onPress: navigateToVideoPicker, + }, + { + text: 'camera (simulator will not work)', + onPress: () => + ImagePicker.openCamera({ + mediaType: 'video', + }).then((vid) => { + console.log(vid); + if (vid.path) { + navigateToCaptionScreenForVideo(vid.path); + } + }), + }, + ]) + } + color={'black'} + style={styles.horizontalMargin} + /> + <PlusIcon + width={23} + height={23} onPress={() => navigateToImagePicker()} color={TAGG_LIGHT_BLUE} style={styles.horizontalMargin} diff --git a/src/components/moments/legacy/MomentPostContent.tsx b/src/components/moments/legacy/MomentPostContent.tsx index 6388be27..d372a332 100644 --- a/src/components/moments/legacy/MomentPostContent.tsx +++ b/src/components/moments/legacy/MomentPostContent.tsx @@ -3,6 +3,7 @@ import React, {useContext, useEffect, useRef, useState} from 'react'; import {Image, StyleSheet, Text, View, ViewProps} from 'react-native'; import {TouchableWithoutFeedback} from 'react-native-gesture-handler'; import Animated, {EasingNode} from 'react-native-reanimated'; +import Video from 'react-native-video'; import {useDispatch, useStore} from 'react-redux'; import {MomentContext} from '../../../screens/profile/IndividualMoment'; import {RootState} from '../../../store/rootReducer'; @@ -57,6 +58,12 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ useState<MomentCommentPreviewType | null>(moment.comment_preview); const {keyboardVisible, scrollTo} = useContext(MomentContext); const [hideText, setHideText] = useState(false); + const isVideo = !( + moment.moment_url.endsWith('jpg') || + moment.moment_url.endsWith('JPG') || + moment.moment_url.endsWith('PNG') || + moment.moment_url.endsWith('png') + ); useEffect(() => { setTags(momentTags); @@ -78,7 +85,6 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ setHideText(false); } }, [keyboardVisible, hideText]); - return ( <View style={[styles.container, style]}> <TouchableWithoutFeedback @@ -86,12 +92,34 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ setVisible(!visible); setFadeValue(new Animated.Value(0)); }}> - <Image - ref={imageRef} - style={styles.image} - source={{uri: moment.moment_url}} - resizeMode={'cover'} - /> + {isVideo ? ( + <View ref={imageRef}> + <Video + // ref={imageRef} + source={{ + uri: moment.moment_url, + }} + // HLS m3u8 version + // source={{ + // uri: 'https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8', + // }} + // mp4 version + // source={{ + // uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + // }} + volume={1} + style={styles.image} + repeat={true} + /> + </View> + ) : ( + <Image + ref={imageRef} + style={styles.image} + source={{uri: moment.moment_url}} + resizeMode={'cover'} + /> + )} {tags.length > 0 && ( <Image source={require('../../assets/icons/tag_indicate.png')} diff --git a/src/components/profile/MomentMoreInfoDrawer.tsx b/src/components/profile/MomentMoreInfoDrawer.tsx index dc4ebe32..910aa095 100644 --- a/src/components/profile/MomentMoreInfoDrawer.tsx +++ b/src/components/profile/MomentMoreInfoDrawer.tsx @@ -31,6 +31,13 @@ interface MomentMoreInfoDrawerProps extends ViewProps { tags: MomentTagType[]; } +type DrawerButtonType = [ + string, + (event: GestureResponderEvent) => void, + JSX.Element?, + TextStyle?, +][]; + const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => { const { setIsOpen, @@ -45,9 +52,7 @@ const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => { const navigation = useNavigation(); - const [drawerButtons, setDrawerButtons] = useState< - [string, (event: GestureResponderEvent) => void, JSX.Element?, TextStyle?][] - >([]); + const [drawerButtons, setDrawerButtons] = useState<DrawerButtonType>([]); const handleDeleteMoment = async () => { setIsOpen(false); diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index c0ee508a..cc001516 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -80,7 +80,6 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({ ); }}>{`${website}`}</Text> )} - {userXId && isBlocked && ( <View style={styles.toggleButtonContainer}> <ToggleButton diff --git a/src/constants/api.ts b/src/constants/api.ts index b55489d9..6dab1153 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -38,6 +38,7 @@ export const VERIFY_INVITATION_CODE_ENDPOUNT: string = API_URL + 'verify-code/'; export const COMMENTS_ENDPOINT: string = API_URL + 'comments/'; export const COMMENT_REACTIONS_ENDPOINT: string = API_URL + 'reaction-comment/'; export const COMMENT_REACTIONS_REPLY_ENDPOINT: string = API_URL + 'reaction-reply/'; +export const PRESIGNED_URL_ENDPOINT: string = API_URL + 'presigned-url/'; export const FRIENDS_ENDPOINT: string = API_URL + 'friends/'; export const ALL_USERS_ENDPOINT: string = API_URL + 'users/'; export const REPORT_ISSUE_ENDPOINT: string = API_URL + 'report/'; diff --git a/src/constants/regex.ts b/src/constants/regex.ts index f934185d..61523203 100644 --- a/src/constants/regex.ts +++ b/src/constants/regex.ts @@ -36,7 +36,7 @@ export const nameRegex: RegExp = /^[A-Za-z'\-,. ]{2,20}$/; * - match alphanumerics, and special characters used in URLs */ export const websiteRegex: RegExp = - /^$|^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,50}\.[a-zA-Z0-9()]{2,6}\b([-a-zA-Z0-9()@:%_+.~#?&\/=]{0,35})$/; + /^$|^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,50}\.[a-zA-Z0-9()]{2,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]{0,35})$/; /** * The website regex has the following constraints diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index b58e03cc..c518d75e 100644 --- a/src/routes/main/MainStackNavigator.tsx +++ b/src/routes/main/MainStackNavigator.tsx @@ -2,7 +2,6 @@ * Note the name userXId here, it refers to the id of the user being visited */ import {createStackNavigator} from '@react-navigation/stack'; -import {Image} from 'react-native-image-crop-picker'; import { CommentBaseType, MomentTagType, @@ -38,16 +37,17 @@ export type MainStackParams = { }; CaptionScreen: { title?: string; - image?: { - filename: string; - path: string; - }; + media?: {filename: string; uri: string; isVideo: boolean}; screenType: ScreenType; selectedTags?: MomentTagType[]; moment?: MomentType; }; TagFriendsScreen: { - imagePath: string; + media: { + filename: string; + uri: string; + isVideo: boolean; + }; selectedTags?: MomentTagType[]; }; TagSelectionScreen: { @@ -111,7 +111,7 @@ export type MainStackParams = { Chat: undefined; NewChatModal: undefined; ZoomInCropper: { - image: Image; + media: {filename: string; uri: string; isVideo: boolean}; screenType: ScreenType; title: string; }; diff --git a/src/screens/chat/ChatListScreen.tsx b/src/screens/chat/ChatListScreen.tsx index 1df5c2da..0f5d8073 100644 --- a/src/screens/chat/ChatListScreen.tsx +++ b/src/screens/chat/ChatListScreen.tsx @@ -6,10 +6,10 @@ import {useStore} from 'react-redux'; import {ChannelList, Chat} from 'stream-chat-react-native'; import {ChatContext} from '../../App'; import {TabsGradient} from '../../components'; +import EmptyContentView from '../../components/common/EmptyContentView'; import {ChannelPreview, MessagesHeader} from '../../components/messages'; import {MainStackParams} from '../../routes'; import {RootState} from '../../store/rootReducer'; -import EmptyContentView from '../../components/common/EmptyContentView'; import { LocalAttachmentType, LocalChannelType, diff --git a/src/screens/moments/TagFriendsScreen.tsx b/src/screens/moments/TagFriendsScreen.tsx index 15926b5a..db7456bc 100644 --- a/src/screens/moments/TagFriendsScreen.tsx +++ b/src/screens/moments/TagFriendsScreen.tsx @@ -1,16 +1,9 @@ import {RouteProp} from '@react-navigation/core'; import {useNavigation} from '@react-navigation/native'; import React, {useEffect, useRef, useState} from 'react'; -import { - Image, - Keyboard, - KeyboardAvoidingView, - Platform, - StyleSheet, - TouchableWithoutFeedback, - View, -} from 'react-native'; +import {Image, StyleSheet, TouchableWithoutFeedback, View} from 'react-native'; import {Button} from 'react-native-elements'; +import Video from 'react-native-video'; import {MainStackParams} from 'src/routes'; import { CaptionScreenHeader, @@ -30,7 +23,7 @@ interface TagFriendsScreenProps { route: TagFriendsScreenRouteProps; } const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { - const {imagePath, selectedTags} = route.params; + const {media, selectedTags} = route.params; const navigation = useNavigation(); const imageRef = useRef(null); const [tags, setTags] = useState<MomentTagType[]>([]); @@ -60,7 +53,7 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { useEffect(() => { if (imageRef && imageRef.current) { Image.getSize( - imagePath, + media.uri, (w, h) => { const imageAspectRatio = w / h; @@ -82,65 +75,75 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { return ( <SearchBackground> - <TouchableWithoutFeedback onPress={Keyboard.dismiss}> - <KeyboardAvoidingView - behavior={Platform.OS === 'ios' ? 'padding' : 'height'} - style={styles.flex}> - <View style={styles.contentContainer}> - <View style={styles.buttonsContainer}> - <Button - title="Cancel" - buttonStyle={styles.button} - onPress={() => navigation.goBack()} - /> - <Button - title="Done" - titleStyle={styles.shareButtonTitle} - buttonStyle={styles.button} - onPress={handleDone} + <View style={styles.contentContainer}> + <View style={styles.buttonsContainer}> + <Button + title="Cancel" + buttonStyle={styles.button} + onPress={() => navigation.goBack()} + /> + <Button + title="Done" + titleStyle={styles.shareButtonTitle} + buttonStyle={styles.button} + onPress={handleDone} + /> + </View> + <CaptionScreenHeader + style={styles.header} + title={'Tap on photo to tag friends!'} + /> + <TouchableWithoutFeedback + onPress={() => + navigation.navigate('TagSelectionScreen', { + selectedTags: tags, + }) + }> + {media.isVideo ? ( + <View style={styles.media} ref={imageRef}> + <Video + style={styles.media} + source={{uri: media.uri}} + repeat={true} /> </View> - <CaptionScreenHeader - style={styles.header} - title={'Tap on photo to tag friends!'} + ) : ( + // <Image + // ref={imageRef} + // style={styles.media} + // source={{uri: media.uri}} + // resizeMode={'cover'} + // /> + <Image + ref={imageRef} + style={[ + { + width: imageWidth, + height: imageHeight, + marginVertical: (SCREEN_WIDTH - imageHeight) / 2, + marginHorizontal: (SCREEN_WIDTH - imageWidth) / 2, + }, + styles.media, + ]} + source={{uri: media.uri}} /> - <TouchableWithoutFeedback - onPress={() => - navigation.navigate('TagSelectionScreen', { - selectedTags: tags, - }) - }> - <Image - ref={imageRef} - style={[ - { - width: imageWidth, - height: imageHeight, - marginVertical: (SCREEN_WIDTH - imageHeight) / 2, - marginHorizontal: (SCREEN_WIDTH - imageWidth) / 2, - }, - styles.image, - ]} - source={{uri: imagePath}} - /> - </TouchableWithoutFeedback> - {tags.length !== 0 && ( - <MomentTags - tags={tags} - setTags={setTags} - editing={true} - imageRef={imageRef} - deleteFromList={(user) => - setTags(tags.filter((tag) => tag.user.id !== user.id)) - } - /> - )} - <View style={styles.footerContainer}> - <TagFriendsFooter tags={tags} setTags={setTags} /> - </View> - </View> - </KeyboardAvoidingView> - </TouchableWithoutFeedback> + )} + </TouchableWithoutFeedback> + {tags.length !== 0 && ( + <MomentTags + tags={tags} + setTags={setTags} + editing={true} + imageRef={imageRef} + deleteFromList={(user) => + setTags(tags.filter((tag) => tag.user.id !== user.id)) + } + /> + )} + <View style={styles.footerContainer}> + <TagFriendsFooter tags={tags} setTags={setTags} /> + </View> + </View> </SearchBackground> ); }; @@ -148,7 +151,6 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { const styles = StyleSheet.create({ contentContainer: { paddingTop: StatusBarHeight, - justifyContent: 'flex-end', }, buttonsContainer: { flexDirection: 'row', @@ -166,7 +168,17 @@ const styles = StyleSheet.create({ header: { marginVertical: 20, }, - image: {zIndex: 0, justifyContent: 'center', alignSelf: 'center'}, + // media: { + // position: 'relative', + // width: SCREEN_WIDTH, + // aspectRatio: 1, + // marginBottom: '3%', + // }, + media: { + zIndex: 0, + justifyContent: 'center', + alignSelf: 'center', + }, text: { position: 'relative', backgroundColor: 'white', diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index 75533a9b..364b81a3 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -15,6 +15,7 @@ import { } from 'react-native'; import {MentionInputControlled} from '../../components'; import {Button, normalize} from 'react-native-elements'; +import Video from 'react-native-video'; import {useDispatch, useSelector} from 'react-redux'; import FrontArrow from '../../assets/icons/front-arrow.svg'; import {SearchBackground} from '../../components'; @@ -27,7 +28,13 @@ import { SUCCESS_PIC_UPLOAD, } from '../../constants/strings'; import {MainStackParams} from '../../routes'; -import {patchMoment, postMoment, postMomentTags} from '../../services'; +import { + handlePresignedURL, + handleVideoUpload, + patchMoment, + postMoment, + postMomentTags, +} from '../../services'; import { loadUserMoments, updateProfileCompletionStage, @@ -51,7 +58,7 @@ interface CaptionScreenProps { } const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { - const {title, image, screenType, selectedTags, moment} = route.params; + const {title, screenType, selectedTags, moment} = route.params; const { user: {userId}, } = useSelector((state: RootState) => state.user); @@ -62,6 +69,17 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { selectedTags ? selectedTags : [], ); const [taggedList, setTaggedList] = useState<string>(''); + const mediaFilename = moment ? undefined : route.params.media!.filename; + const mediaUri = moment ? moment.moment_url : route.params.media!.uri; + // TODO: change this once moment refactor is done + const isMediaAVideo = moment + ? !( + moment.moment_url.endsWith('.jpg') || + moment.moment_url.endsWith('.JPG') || + moment.moment_url.endsWith('.png') || + moment.moment_url.endsWith('.PNG') + ) + : route.params.media?.isVideo ?? false; useEffect(() => { setTags(selectedTags ? selectedTags : []); @@ -120,36 +138,52 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { const handleShare = async () => { setLoading(true); - if (!image?.filename || !title) { - return; - } - const momentResponse = await postMoment( - image.filename, - image.path, - caption, - title, - userId, - ); - if (!momentResponse) { + if (moment || !mediaFilename || !title) { handleFailed(); return; } - const momentTagResponse = await postMomentTags( - momentResponse.moment_id, - formattedTags(), - ); - if (!momentTagResponse) { - handleFailed(); - return; + let profileCompletionStage; + let momentId; + // separate upload logic for image/video + if (isMediaAVideo) { + const presignedURL = await handlePresignedURL(mediaFilename, title); + if (!presignedURL) { + handleFailed(); + return; + } + momentId = presignedURL.moment_id; + // TODO: assume success for now + await handleVideoUpload(mediaFilename, mediaUri, presignedURL); + } else { + const momentResponse = await postMoment( + mediaFilename, + mediaUri, + caption, + title, + userId, + ); + if (!momentResponse) { + handleFailed(); + return; + } + profileCompletionStage = momentResponse.profile_completion_stage; + momentId = momentResponse.moment_id; + } + if (momentId) { + const momentTagResponse = await postMomentTags(momentId, formattedTags()); + if (!momentTagResponse) { + handleFailed(); + return; + } } dispatch(loadUserMoments(userId)); - dispatch( - updateProfileCompletionStage(momentResponse.profile_completion_stage), - ); + if (profileCompletionStage) { + dispatch(updateProfileCompletionStage(profileCompletionStage)); + } handleSuccess(); }; - const handleDone = async () => { + const handleDoneEditing = async () => { setLoading(true); if (moment?.moment_id) { const success = await patchMoment( @@ -186,19 +220,26 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { title={moment ? 'Done' : 'Share'} titleStyle={styles.shareButtonTitle} buttonStyle={styles.button} - onPress={moment ? handleDone : handleShare} + onPress={moment ? handleDoneEditing : handleShare} /> </View> <CaptionScreenHeader style={styles.header} - {...{title: moment ? moment.moment_category : title}} - /> - {/* this is the image we want to center our tags' initial location within */} - <Image - style={styles.image} - source={{uri: moment ? moment.moment_url : image?.path}} - resizeMode={'contain'} + {...{title: moment ? moment.moment_category : title ?? ''}} /> + {isMediaAVideo ? ( + <Video + style={styles.media} + source={{uri: mediaUri}} + repeat={true} + /> + ) : ( + <Image + style={styles.media} + source={{uri: mediaUri}} + resizeMode={'contain'} + /> + )} <MentionInputControlled containerStyle={styles.text} placeholder="Write something....." @@ -210,11 +251,11 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { <TouchableOpacity onPress={() => navigation.navigate('TagFriendsScreen', { - imagePath: moment - ? moment.moment_url - : image - ? image.path - : '', + media: { + filename: mediaFilename ?? '', + uri: mediaUri, + isVideo: isMediaAVideo, + }, selectedTags: tags, }) } @@ -257,7 +298,7 @@ const styles = StyleSheet.create({ header: { marginVertical: 20, }, - image: { + media: { position: 'relative', width: SCREEN_WIDTH, aspectRatio: 1, diff --git a/src/services/MomentService.ts b/src/services/MomentService.ts index b837585a..ca32a3f3 100644 --- a/src/services/MomentService.ts +++ b/src/services/MomentService.ts @@ -5,8 +5,14 @@ import { MOMENTTAG_ENDPOINT, MOMENT_TAGS_ENDPOINT, MOMENT_THUMBNAIL_ENDPOINT, + PRESIGNED_URL_ENDPOINT, + TAGG_CUSTOMER_SUPPORT, } from '../constants'; -import {MomentPostType, MomentTagType} from '../types'; +import { + ERROR_SOMETHING_WENT_WRONG, + ERROR_SOMETHING_WENT_WRONG_REFRESH, +} from '../constants/strings'; +import {MomentPostType, MomentTagType, PresignedURLResponse} from '../types'; import {checkImageUploadStatus} from '../utils'; export const postMoment = async ( @@ -18,6 +24,7 @@ export const postMoment = async ( ) => { try { const request = new FormData(); + //Manipulating filename to end with .jpg instead of .heic if (fileName.endsWith('.heic') || fileName.endsWith('.HEIC')) { fileName = fileName.split('.')[0] + '.jpg'; @@ -208,3 +215,118 @@ export const deleteMomentTag = async (moment_tag_id: string) => { return false; } }; +/** + * This function makes a request to the server in order to provide the client with a presigned URL. + * This is called first, in order for the client to directly upload a file to S3 + * @param value: string | undefined + * @param filename: string | undefined + * @returns a PresignedURLResponse object + */ +export const handlePresignedURL = async ( + filename: string | undefined, + momentCategory: string, +) => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch(PRESIGNED_URL_ENDPOINT, { + method: 'POST', + headers: { + Authorization: 'Token ' + token, + }, + body: JSON.stringify({ + filename, + category: momentCategory, + }), + }); + const status = response.status; + let data: PresignedURLResponse = await response.json(); + if (status === 200) { + console.log('done'); + return data; + } else { + if (status === 404) { + console.log( + `Please make sure that the email / username entered is registered with us. You may contact our customer support at ${TAGG_CUSTOMER_SUPPORT}`, + ); + } else { + console.log(ERROR_SOMETHING_WENT_WRONG_REFRESH); + } + console.log(response); + } + } catch (error) { + console.log(error); + console.log(ERROR_SOMETHING_WENT_WRONG); + } +}; +/** + * This util function takes in the file object and the PresignedURLResponse object, creates form data from the latter, + * and makes a post request to the presigned URL, sending the file object inside of the form data. + * @param filename: the filename + * @param filePath: the path to the file, including filename + * @param urlObj PresignedURLResponse | undefined + * @returns responseURL or boolean + */ +export const handleVideoUpload = async ( + filename: string, + filePath: string, + urlObj: PresignedURLResponse | undefined, +) => { + try { + if (urlObj === null || urlObj === undefined) { + console.log('Invalid urlObj'); + return false; + } + //build formData for POST request + // Could not get a forEach to work and could not assign directly, will look into cleaning this series of appends up later. + const form = new FormData(); + form.append('key', urlObj.response_url.fields.key); + form.append( + 'x-amz-algorithm', + urlObj.response_url.fields['x-amz-algorithm'], + ); + form.append( + 'x-amz-credential', + urlObj.response_url.fields['x-amz-credential'], + ); + form.append('x-amz-date', urlObj.response_url.fields['x-amz-date']); + form.append('policy', urlObj.response_url.fields.policy); + form.append( + 'x-amz-signature', + urlObj.response_url.fields['x-amz-signature'], + ); + form.append('file', { + uri: filePath, + // other types such as 'quicktime' 'image' etc exist, and we can programmatically type this, but for now sticking with simple 'video' + type: 'video', + name: filename, + }); + const response = await fetch(urlObj.response_url.url, { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + body: form, + }); + const status = response.status; + // let data = await response.json(); + if (status === 200 || status === 204) { + console.log('complete'); + return true; + } else { + if (status === 404) { + console.log( + `Please make sure that the email / username entered is registered with us. You may contact our customer support at ${TAGG_CUSTOMER_SUPPORT}`, + ); + } else { + console.log('FFFFFF \n'); + console.log(response); + console.log(ERROR_SOMETHING_WENT_WRONG_REFRESH); + } + console.log(response); + } + } catch (error) { + console.log(error); + console.log(ERROR_SOMETHING_WENT_WRONG); + } + return false; +}; diff --git a/src/types/types.ts b/src/types/types.ts index 171a9ff3..416d9146 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -365,3 +365,19 @@ export type ReactionType = { id: string; type: ReactionOptionsType; }; +// used to handle direct S3 uploads by packaging presigned_url info into one object +export type PresignedURLResponse = { + response_msg: string; + response_url: { + url: string; + fields: { + key: string | undefined; + 'x-amz-algorithm': string; + 'x-amz-credential': string; + 'x-amz-date': string; + policy: string; + 'x-amz-signature': string; + }; + }; + moment_id: string; +}; |