aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/comments/ZoomInCropper.tsx26
-rw-r--r--src/components/moments/Moment.tsx72
-rw-r--r--src/components/moments/legacy/MomentPostContent.tsx42
-rw-r--r--src/components/profile/MomentMoreInfoDrawer.tsx11
-rw-r--r--src/components/profile/ProfileBody.tsx1
-rw-r--r--src/constants/api.ts1
-rw-r--r--src/constants/regex.ts2
-rw-r--r--src/routes/main/MainStackNavigator.tsx14
-rw-r--r--src/screens/chat/ChatListScreen.tsx2
-rw-r--r--src/screens/moments/TagFriendsScreen.tsx150
-rw-r--r--src/screens/profile/CaptionScreen.tsx117
-rw-r--r--src/services/MomentService.ts124
-rw-r--r--src/types/types.ts16
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;
+};