aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIvan Chen <ivan@tagg.id>2021-07-23 18:52:28 -0400
committerGitHub <noreply@github.com>2021-07-23 18:52:28 -0400
commite39fcbd9e35f6a5e36afe248e24bea0dd3859202 (patch)
tree91509301e137497056886b053022c16ea81c4b0d /src
parentb06b93e77ca7ec1b1107c0a58dbc2dd370208ccf (diff)
parentd5eabf15913597fc61127d7b501d271cdeac683c (diff)
Merge pull request #521 from IvanIFChen/tma962-moment-upload-progress-bar
[TMA-962] Moment Upload Progress Bar
Diffstat (limited to 'src')
-rw-r--r--src/assets/images/green-check.pngbin0 -> 116479 bytes
-rw-r--r--src/assets/images/white-x.pngbin0 -> 111493 bytes
-rw-r--r--src/components/camera/GalleryIcon.tsx4
-rw-r--r--src/components/common/GradientProgressBar.tsx48
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/moments/MomentUploadProgressBar.tsx221
-rw-r--r--src/components/moments/TrimmerPlayer.tsx2
-rw-r--r--src/components/moments/index.ts1
-rw-r--r--src/components/profile/Content.tsx2
-rw-r--r--src/components/profile/ProfileBadges.tsx8
-rw-r--r--src/constants/api.ts1
-rw-r--r--src/constants/constants.ts1
-rw-r--r--src/constants/strings.ts13
-rw-r--r--src/routes/main/MainStackNavigator.tsx3
-rw-r--r--src/screens/profile/CaptionScreen.tsx55
-rw-r--r--src/screens/profile/ProfileScreen.tsx2
-rw-r--r--src/screens/upload/EditMedia.tsx55
-rw-r--r--src/services/MomentService.ts20
-rw-r--r--src/store/actions/user.ts98
-rw-r--r--src/store/initialStates.ts2
-rw-r--r--src/store/reducers/userReducer.ts5
-rw-r--r--src/types/types.ts13
-rw-r--r--src/utils/camera.ts2
23 files changed, 494 insertions, 63 deletions
diff --git a/src/assets/images/green-check.png b/src/assets/images/green-check.png
new file mode 100644
index 00000000..c8680c53
--- /dev/null
+++ b/src/assets/images/green-check.png
Binary files differ
diff --git a/src/assets/images/white-x.png b/src/assets/images/white-x.png
new file mode 100644
index 00000000..17f0c50a
--- /dev/null
+++ b/src/assets/images/white-x.png
Binary files differ
diff --git a/src/components/camera/GalleryIcon.tsx b/src/components/camera/GalleryIcon.tsx
index ca2d2559..44297d6d 100644
--- a/src/components/camera/GalleryIcon.tsx
+++ b/src/components/camera/GalleryIcon.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import {Image, Text, TouchableOpacity, View} from 'react-native';
-import {navigateToImagePicker} from '../../utils/camera';
+import {navigateToMediaPicker} from '../../utils/camera';
import {ImageOrVideo} from 'react-native-image-crop-picker';
import {styles} from './styles';
@@ -19,7 +19,7 @@ export const GalleryIcon: React.FC<GalleryIconProps> = ({
}) => {
return (
<TouchableOpacity
- onPress={() => navigateToImagePicker(callback)}
+ onPress={() => navigateToMediaPicker(callback)}
style={styles.saveButton}>
{mostRecentPhotoUri !== '' ? (
<Image
diff --git a/src/components/common/GradientProgressBar.tsx b/src/components/common/GradientProgressBar.tsx
new file mode 100644
index 00000000..fc62bd3c
--- /dev/null
+++ b/src/components/common/GradientProgressBar.tsx
@@ -0,0 +1,48 @@
+import React, {FC} from 'react';
+import {StyleSheet, ViewProps, ViewStyle} from 'react-native';
+import LinearGradient from 'react-native-linear-gradient';
+import Animated, {useAnimatedStyle} from 'react-native-reanimated';
+import {
+ TAGG_LIGHT_BLUE_2,
+ TAGG_LIGHT_BLUE_3,
+ TAGG_PURPLE,
+} from '../../constants';
+import {normalize} from '../../utils';
+
+interface GradientProgressBarProps extends ViewProps {
+ progress: Animated.SharedValue<number>;
+}
+
+const GradientProgressBar: FC<GradientProgressBarProps> = ({
+ style,
+ progress,
+}) => {
+ const animatedProgressStyle = useAnimatedStyle<ViewStyle>(() => ({
+ width: `${(1 - progress.value) * 100}%`,
+ }));
+ return (
+ <LinearGradient
+ style={[styles.bar, style]}
+ useAngle={true}
+ colors={[TAGG_PURPLE, TAGG_LIGHT_BLUE_2]}>
+ <Animated.View style={[styles.blank, animatedProgressStyle]} />
+ </LinearGradient>
+ );
+};
+const styles = StyleSheet.create({
+ container: {
+ borderRadius: 6.5,
+ },
+ bar: {
+ height: normalize(10),
+ borderRadius: 6.5,
+ },
+ blank: {
+ alignSelf: 'flex-end',
+ height: normalize(10),
+ width: '80%',
+ backgroundColor: TAGG_LIGHT_BLUE_3,
+ },
+});
+
+export default GradientProgressBar;
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index 4f5c0232..5edbb3ad 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -29,3 +29,4 @@ export {default as TaggUserRowCell} from './TaggUserRowCell';
export {default as LikeButton} from './LikeButton';
export {default as TaggUserSelectionCell} from './TaggUserSelectionCell';
export {default as MomentTags} from './MomentTags';
+export {default as GradientProgressBar} from './GradientProgressBar';
diff --git a/src/components/moments/MomentUploadProgressBar.tsx b/src/components/moments/MomentUploadProgressBar.tsx
new file mode 100644
index 00000000..d56a8337
--- /dev/null
+++ b/src/components/moments/MomentUploadProgressBar.tsx
@@ -0,0 +1,221 @@
+import React, {useEffect} from 'react';
+import {Image, StyleSheet, Text} from 'react-native';
+import {View} from 'react-native-animatable';
+import {
+ cancelAnimation,
+ Easing,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
+import {useDispatch, useSelector} from 'react-redux';
+import {checkMomentDoneProcessing} from '../../services';
+import {loadUserMoments} from '../../store/actions';
+import {setMomentUploadProgressBar} from '../../store/reducers';
+import {RootState} from '../../store/rootReducer';
+import {MomentUploadStatusType} from '../../types';
+import {normalize, SCREEN_WIDTH, StatusBarHeight} from '../../utils';
+import {GradientProgressBar} from '../common';
+
+interface MomentUploadProgressBarProps {}
+
+const MomentUploadProgressBar: React.FC<MomentUploadProgressBarProps> =
+ ({}) => {
+ const dispatch = useDispatch();
+ const {userId: loggedInUserId} = useSelector(
+ (state: RootState) => state.user.user,
+ );
+ const {momentUploadProgressBar} = useSelector(
+ (state: RootState) => state.user,
+ );
+ const progress = useSharedValue(0);
+ const showLoading =
+ momentUploadProgressBar?.status ===
+ MomentUploadStatusType.UploadingToS3 ||
+ momentUploadProgressBar?.status ===
+ MomentUploadStatusType.WaitingForDoneProcessing;
+
+ useEffect(() => {
+ let doneProcessing = false;
+ const checkDone = async () => {
+ if (
+ momentUploadProgressBar &&
+ (await checkMomentDoneProcessing(momentUploadProgressBar!.momentId))
+ ) {
+ doneProcessing = true;
+ cancelAnimation(progress);
+ // upload is done, but let's finish the progress bar animation in a velocity of 10%/s
+ const finishProgressBarDuration = (1 - progress.value) * 10 * 1000;
+ progress.value = withTiming(1, {
+ duration: finishProgressBarDuration,
+ easing: Easing.linear,
+ });
+ // change status to Done 1s after the progress bar animation is done
+ setTimeout(() => {
+ dispatch(loadUserMoments(loggedInUserId));
+ dispatch({
+ type: setMomentUploadProgressBar.type,
+ payload: {
+ momentUploadProgressBar: {
+ ...momentUploadProgressBar,
+ status: MomentUploadStatusType.Done,
+ },
+ },
+ });
+ }, finishProgressBarDuration);
+ }
+ };
+ if (
+ momentUploadProgressBar?.status ===
+ MomentUploadStatusType.WaitingForDoneProcessing
+ ) {
+ checkDone();
+ const timer = setInterval(async () => {
+ if (!doneProcessing) {
+ checkDone();
+ }
+ }, 5 * 1000);
+ // timeout if takes longer than 1 minute to process
+ setTimeout(() => {
+ clearInterval(timer);
+ if (!doneProcessing) {
+ console.error('Check for done processing timed out');
+ dispatch({
+ type: setMomentUploadProgressBar.type,
+ payload: {
+ momentUploadProgressBar: {
+ ...momentUploadProgressBar,
+ status: MomentUploadStatusType.Error,
+ },
+ },
+ });
+ }
+ }, 60 * 1000);
+ return () => clearInterval(timer);
+ }
+ }, [momentUploadProgressBar?.status]);
+
+ useEffect(() => {
+ if (
+ momentUploadProgressBar?.status === MomentUploadStatusType.UploadingToS3
+ ) {
+ // e.g. 30s video => 30 * 3 = 60s
+ const videoDuration =
+ momentUploadProgressBar.originalVideoDuration ?? 30;
+ const durationInSeconds = videoDuration * 3;
+ progress.value = withTiming(1, {
+ duration: durationInSeconds * 1000,
+ easing: Easing.out(Easing.quad),
+ });
+ }
+ }, [momentUploadProgressBar?.status]);
+
+ useEffect(() => {
+ if (
+ momentUploadProgressBar?.status === MomentUploadStatusType.Done ||
+ momentUploadProgressBar?.status === MomentUploadStatusType.Error
+ ) {
+ progress.value = 0;
+ // clear this component after a duration
+ setTimeout(() => {
+ dispatch({
+ type: setMomentUploadProgressBar.type,
+ payload: {
+ momentUploadProgressBar: undefined,
+ },
+ });
+ }, 5000);
+ }
+ }, [momentUploadProgressBar?.status]);
+
+ if (!momentUploadProgressBar) {
+ return null;
+ }
+
+ return (
+ <View
+ style={[
+ styles.background,
+ momentUploadProgressBar?.status === MomentUploadStatusType.Error
+ ? styles.redBackground
+ : {},
+ ]}>
+ <View style={styles.container}>
+ {showLoading && (
+ <>
+ <Text style={styles.text}>Uploading Moment...</Text>
+ <GradientProgressBar style={styles.bar} progress={progress} />
+ </>
+ )}
+ {momentUploadProgressBar.status === MomentUploadStatusType.Done && (
+ <View style={styles.row}>
+ <Image
+ source={require('../../assets/images/green-check.png')}
+ style={styles.x}
+ />
+ <Text style={styles.text}>
+ Beautiful, the Moment was uploaded successfully!
+ </Text>
+ </View>
+ )}
+ {momentUploadProgressBar.status === MomentUploadStatusType.Error && (
+ <View style={styles.row}>
+ <Image
+ source={require('../../assets/images/white-x.png')}
+ style={styles.x}
+ />
+ <Text style={styles.whiteText}>
+ Unable to upload Moment. Please retry
+ </Text>
+ </View>
+ )}
+ </View>
+ </View>
+ );
+ };
+
+const styles = StyleSheet.create({
+ background: {
+ position: 'absolute',
+ zIndex: 999,
+ height: StatusBarHeight + normalize(84),
+ backgroundColor: 'white',
+ width: '100%',
+ alignItems: 'center',
+ },
+ container: {
+ justifyContent: 'center',
+ marginTop: StatusBarHeight,
+ height: normalize(84),
+ },
+ text: {
+ fontSize: normalize(14),
+ fontWeight: 'bold',
+ lineHeight: 17,
+ marginVertical: 12,
+ width: '80%',
+ },
+ bar: {
+ width: SCREEN_WIDTH * 0.9,
+ },
+ redBackground: {
+ backgroundColor: '#EA574C',
+ },
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ whiteText: {
+ color: 'white',
+ fontSize: normalize(14),
+ fontWeight: 'bold',
+ lineHeight: 17,
+ marginVertical: 12,
+ },
+ x: {
+ width: normalize(26),
+ height: normalize(26),
+ marginRight: 10,
+ },
+});
+
+export default MomentUploadProgressBar;
diff --git a/src/components/moments/TrimmerPlayer.tsx b/src/components/moments/TrimmerPlayer.tsx
index 87b3a786..a7239d8b 100644
--- a/src/components/moments/TrimmerPlayer.tsx
+++ b/src/components/moments/TrimmerPlayer.tsx
@@ -73,7 +73,7 @@ const TrimmerPlayer: React.FC<TrimmerPlayerProps> = ({
repeat={true}
onLoad={(payload) => {
setEnd(payload.duration);
- handleLoad(payload.naturalSize);
+ handleLoad(payload.naturalSize, payload.duration);
}}
onProgress={(e) => {
if (!paused) {
diff --git a/src/components/moments/index.ts b/src/components/moments/index.ts
index 16c9aed2..3f33ec53 100644
--- a/src/components/moments/index.ts
+++ b/src/components/moments/index.ts
@@ -5,3 +5,4 @@ export {default as TagFriendsFooter} from './TagFriendsFoooter';
export {default as MomentPost} from './MomentPost';
export {default as TaggedUsersDrawer} from './TaggedUsersDrawer';
export {default as TrimmerPlayer} from './TrimmerPlayer';
+export {default as MomentUploadProgressBar} from './MomentUploadProgressBar';
diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx
index 2d1002dd..9edd890d 100644
--- a/src/components/profile/Content.tsx
+++ b/src/components/profile/Content.tsx
@@ -6,6 +6,7 @@ import Animated, {
useSharedValue,
} from 'react-native-reanimated';
import {useDispatch, useSelector, useStore} from 'react-redux';
+import {MomentUploadProgressBar} from '..';
import {
blockUnblockUser,
loadFriendsData,
@@ -140,6 +141,7 @@ const Content: React.FC<ContentProps> = ({userXId, screenType}) => {
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
+ {!userXId && <MomentUploadProgressBar />}
<Cover {...{userXId, screenType}} />
<ProfileCutout />
<ProfileHeader
diff --git a/src/components/profile/ProfileBadges.tsx b/src/components/profile/ProfileBadges.tsx
index 8e68dc46..c7d3b5ba 100644
--- a/src/components/profile/ProfileBadges.tsx
+++ b/src/components/profile/ProfileBadges.tsx
@@ -64,8 +64,8 @@ const ProfileBadges: React.FC<ProfileBadgesProps> = ({userXId, screenType}) => {
<PlusIcon />
{Array(BADGE_LIMIT)
.fill(0)
- .map(() => (
- <View style={[styles.grey, styles.circle]} />
+ .map((_item, index) => (
+ <View key={index} style={[styles.grey, styles.circle]} />
))}
</ScrollView>
)}
@@ -85,8 +85,8 @@ const ProfileBadges: React.FC<ProfileBadgesProps> = ({userXId, screenType}) => {
{Array(BADGE_LIMIT + 1)
.fill(0)
.splice(displayBadges.length + 1, BADGE_LIMIT)
- .map(() => (
- <View style={styles.circle} />
+ .map((_item, index) => (
+ <View key={index} style={styles.circle} />
))}
{/* X button */}
{displayBadges.length === BADGE_LIMIT && isOwnProfile && (
diff --git a/src/constants/api.ts b/src/constants/api.ts
index 6dab1153..b4548634 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -39,6 +39,7 @@ 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 CHECK_MOMENT_UPLOAD_DONE_PROCESSING_ENDPOINT: string = API_URL + 'moments/check_done_processing/';
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/constants.ts b/src/constants/constants.ts
index 13a73208..476e7af4 100644
--- a/src/constants/constants.ts
+++ b/src/constants/constants.ts
@@ -69,6 +69,7 @@ export const TAGG_DARK_BLUE = '#4E699C';
export const TAGG_DARK_PURPLEISH_BLUE = '#4755A1';
export const TAGG_LIGHT_BLUE: string = '#698DD3';
export const TAGG_LIGHT_BLUE_2: string = '#6EE7E7';
+export const TAGG_LIGHT_BLUE_3 = '#DDE8FE';
export const TAGG_LIGHT_PURPLE = '#F4DDFF';
export const RADIO_BUTTON_GREY: string = '#BEBEBE';
diff --git a/src/constants/strings.ts b/src/constants/strings.ts
index 071b3835..450d7e5c 100644
--- a/src/constants/strings.ts
+++ b/src/constants/strings.ts
@@ -2,8 +2,8 @@
// Below is the regex to convert this into a csv for the Google Sheet
// export const (.*) = .*?(['|"|`])(.*)\2;
// replace with: $1\t$3
-export const APP_STORE_LINK = 'https://apps.apple.com/us/app/tagg-discover-your-community/id1537853613'
export const ADD_COMMENT_TEXT = (username?: string) => username ? `Reply to ${username}` : 'Add a comment...'
+export const APP_STORE_LINK = 'https://apps.apple.com/us/app/tagg-discover-your-community/id1537853613'
export const COMING_SOON_MSG = 'Creating more fun things for you, surprises coming soon πŸ˜‰';
export const ERROR_ATTEMPT_EDIT_SP = 'Can\'t let you do that yet! Please onboard Suggested People first!';
export const ERROR_AUTHENTICATION = 'An error occurred during authentication. Please login again!';
@@ -31,8 +31,10 @@ export const ERROR_INVLAID_CODE = 'The code entered is not valid!';
export const ERROR_LINK = (str: string) => `Unable to link with ${str}, Please check your login and try again`;
export const ERROR_LOGIN = 'There was a problem logging you in, please refresh and try again';
export const ERROR_LOGIN_FAILED = 'Login failed. Check your username and password, and try again';
+export const ERROR_MOMENT_UPLOAD_IN_PROGRESS = 'Please wait, there is a Moment upload in progress.';
export const ERROR_NEXT_PAGE = 'There was a problem while loading the next page πŸ˜“, try again in a couple minutes';
export const ERROR_NO_CONTACT_INVITE_LEFT = 'You have no more invites left!'
+export const ERROR_NO_MOMENT_CATEGORY = 'Please select a category!';
export const ERROR_NOT_ONBOARDED = 'You are now on waitlist, please enter your invitation code if you have one';
export const ERROR_PHONE_IN_USE = 'Phone already in use, please try another one';
export const ERROR_PROFILE_CREATION_SHORT = 'Profile creation failed πŸ˜“';
@@ -45,7 +47,6 @@ export const ERROR_SELECT_GENDER = 'Please select your gender';
export const ERROR_SELECT_UNIVERSITY = 'Please select your University';
export const ERROR_SERVER_DOWN = 'mhm, looks like our servers are down, please refresh and try again in a few mins';
export const ERROR_SOMETHING_WENT_WRONG = 'Oh dear, don’t worry someone will be held responsible for this error, In the meantime refresh the app';
-export const ERROR_NO_MOMENT_CATEGORY = 'Please select a category!';
export const ERROR_SOMETHING_WENT_WRONG_REFRESH = "Ha, looks like this one's on us, please refresh and try again";
export const ERROR_SOMETHING_WENT_WRONG_RELOAD = "You broke it, Just kidding! we don't know what happened... Please reload the app and try again";
export const ERROR_T_AND_C_NOT_ACCEPTED = 'You must first agree to the terms and conditions.';
@@ -61,6 +62,7 @@ export const ERROR_UPLOAD_SMALL_PROFILE_PIC = "Can't have a profile without a pi
export const ERROR_UPLOAD_SP_PHOTO = 'Unable to update suggested people photo. Please retry!';
export const ERROR_VERIFICATION_FAILED_SHORT = 'Verification failed πŸ˜“';
export const FIRST_MESSAGE = 'How about sending your first message to your friend';
+export const INVITE_USER_SMS_BODY = (invitedUserName: string, invitee: string, inviteCode: string) => `Hey ${invitedUserName}!\nYou've been tagged by ${invitee}. Follow the instructions below to skip the line and join them on Tagg!\nSign up and use this code to get in: ${inviteCode}\n ${APP_STORE_LINK}`;
export const MARKED_AS_MSG = (str: string) => `Marked as ${str}`;
export const MOMENT_DELETED_MSG = 'Moment deleted....Some moments have to go, to create space for greater ones';
export const NO_NEW_NOTIFICATIONS = 'You have no new notifications';
@@ -69,13 +71,10 @@ export const PRIVATE_ACCOUNT = 'This account is private';
export const START_CHATTING = 'Let’s Start Chatting!';
export const SUCCESS_BADGES_UPDATE = 'Badges updated successfully!'
export const SUCCESS_CATEGORY_DELETE = 'Category successfully deleted, but its memory will live on';
+export const SUCCESS_CONFIRM_INVITE_CONTACT_MESSAGE = 'Use one now?';
+export const SUCCESS_CONFIRM_INVITE_CONTACT_TITLE = (str: string) => `You have ${str} invites left!`;
export const SUCCESS_INVITATION_CODE = 'Welcome to Tagg!';
export const SUCCESS_INVITE_CONTACT = (str: string) => `Success! You now have ${str} invites left!`;
-export const SUCCESS_CONFIRM_INVITE_CONTACT_TITLE = (str: string) => `You have ${str} invites left!`;
-export const SUCCESS_CONFIRM_INVITE_CONTACT_MESSAGE = 'Use one now?';
-export const INVITE_USER_SMS_BODY = (invitedUserName: string, invitee: string, inviteCode: string) => `Hey ${invitedUserName}!\n
-You've been tagged by ${invitee}. Follow the instructions below to skip the line and join them on Tagg!\n
-Sign up and use this code to get in: ${inviteCode}\n ${APP_STORE_LINK}`;
export const SUCCESS_LAST_CONTACT_INVITE = 'Done! That was your last invite, hope you used it wisely!';
export const SUCCESS_LINK = (str: string) => `Successfully linked ${str} πŸŽ‰`;
export const SUCCESS_PIC_UPLOAD = 'Beautiful, the Moment was uploaded successfully!';
diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx
index 11e9d08d..8def19e3 100644
--- a/src/routes/main/MainStackNavigator.tsx
+++ b/src/routes/main/MainStackNavigator.tsx
@@ -46,7 +46,7 @@ export type MainStackParams = {
};
CaptionScreen: {
screenType: ScreenType;
- media?: {uri: string; isVideo: boolean};
+ media?: {uri: string; isVideo: boolean; videoDuration: number | undefined};
selectedCategory?: string;
selectedTags?: MomentTagType[];
moment?: MomentType;
@@ -58,6 +58,7 @@ export type MainStackParams = {
media: {
uri: string;
isVideo: boolean;
+ videoDuration: number | undefined;
};
selectedTags?: MomentTagType[];
};
diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx
index 7f77bdca..6ba1791c 100644
--- a/src/screens/profile/CaptionScreen.tsx
+++ b/src/screens/profile/CaptionScreen.tsx
@@ -34,14 +34,9 @@ import {
} from '../../constants/strings';
import * as RootNavigation from '../../RootNavigation';
import {MainStackParams} from '../../routes';
+import {patchMoment, postMoment, postMomentTags} from '../../services';
import {
- handlePresignedURL,
- handleVideoUpload,
- patchMoment,
- postMoment,
- postMomentTags,
-} from '../../services';
-import {
+ handleVideoMomentUpload,
loadUserMoments,
updateProfileCompletionStage,
} from '../../store/actions';
@@ -76,6 +71,8 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
const [tags, setTags] = useState<MomentTagType[]>([]);
const [taggedUsersText, setTaggedUsersText] = useState('');
const [momentCategory, setMomentCategory] = useState<string | undefined>();
+ // only used for upload purposes, undefined for editing is fine
+ const videoDuration = moment ? undefined : route.params.media!.videoDuration;
const mediaUri = moment ? moment.moment_url : route.params.media!.uri;
// TODO: change this once moment refactor is done
const isMediaAVideo = moment
@@ -133,11 +130,7 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
navigation.popToTop();
RootNavigation.navigate('ProfileTab');
setTimeout(() => {
- if (isMediaAVideo) {
- Alert.alert(
- 'Beautiful, the Moment was uploaded successfully! Check back in a bit and refresh to see it!',
- );
- } else {
+ if (!isMediaAVideo) {
Alert.alert(SUCCESS_PIC_UPLOAD);
}
}, 500);
@@ -166,20 +159,20 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
return;
}
let profileCompletionStage;
- let momentId;
// separate upload logic for image/video
if (isMediaAVideo) {
- const presignedURLResponse = await handlePresignedURL(momentCategory);
- if (!presignedURLResponse) {
- handleFailed();
- return;
- }
- momentId = presignedURLResponse.moment_id;
- const fileHash = presignedURLResponse.response_url.fields.key;
- if (fileHash !== null && fileHash !== '' && fileHash !== undefined) {
- await handleVideoUpload(mediaUri, presignedURLResponse);
+ if (videoDuration) {
+ dispatch(
+ handleVideoMomentUpload(
+ mediaUri,
+ videoDuration,
+ momentCategory,
+ formattedTags(),
+ ),
+ );
} else {
handleFailed();
+ return;
}
} else {
const momentResponse = await postMoment(
@@ -193,13 +186,16 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
return;
}
profileCompletionStage = momentResponse.profile_completion_stage;
- momentId = momentResponse.moment_id;
- }
- if (momentId) {
- const momentTagResponse = await postMomentTags(momentId, formattedTags());
- if (!momentTagResponse) {
- handleFailed();
- return;
+ const momentId = momentResponse.moment_id;
+ if (momentId) {
+ const momentTagResponse = await postMomentTags(
+ momentId,
+ formattedTags(),
+ );
+ if (!momentTagResponse) {
+ handleFailed();
+ return;
+ }
}
}
if (!isMediaAVideo) {
@@ -325,6 +321,7 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
media: {
uri: mediaUri,
isVideo: isMediaAVideo,
+ videoDuration,
},
selectedTags: tags,
})
diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx
index 3dd142e1..09b70cd3 100644
--- a/src/screens/profile/ProfileScreen.tsx
+++ b/src/screens/profile/ProfileScreen.tsx
@@ -1,7 +1,7 @@
+import {RouteProp} from '@react-navigation/native';
import React, {useEffect} from 'react';
import {StatusBar} from 'react-native';
import {Content, TabsGradient} from '../../components';
-import {RouteProp} from '@react-navigation/native';
import {MainStackParams} from '../../routes/';
import {visitedUserProfile} from '../../services';
diff --git a/src/screens/upload/EditMedia.tsx b/src/screens/upload/EditMedia.tsx
index f8e7692d..07d20a7b 100644
--- a/src/screens/upload/EditMedia.tsx
+++ b/src/screens/upload/EditMedia.tsx
@@ -2,14 +2,24 @@ import ReactNativeZoomableView from '@dudigital/react-native-zoomable-view/src/R
import {RouteProp} from '@react-navigation/core';
import {StackNavigationProp} from '@react-navigation/stack';
import React, {useEffect, useRef, useState} from 'react';
-import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
+import {
+ Alert,
+ Image,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
import ImageZoom, {IOnMove} from 'react-native-image-pan-zoom';
import PhotoManipulator from 'react-native-photo-manipulator';
+import {useSelector} from 'react-redux';
import TrimIcon from '../../assets/icons/trim.svg';
import CloseIcon from '../../assets/ionicons/close-outline.svg';
import {SaveButton, TrimmerPlayer} from '../../components';
import {TaggLoadingIndicator, TaggSquareButton} from '../../components/common';
+import {ERROR_MOMENT_UPLOAD_IN_PROGRESS} from '../../constants/strings';
import {MainStackParams} from '../../routes';
+import {RootState} from '../../store/rootReducer';
import {
cropVideo,
HeaderHeight,
@@ -36,6 +46,9 @@ export const EditMedia: React.FC<EditMediaProps> = ({route, navigation}) => {
selectedCategory,
media: {isVideo},
} = route.params;
+ const {momentUploadProgressBar} = useSelector(
+ (state: RootState) => state.user,
+ );
const [aspectRatio, setAspectRatio] = useState<number>(1);
// width and height of video, if video
const [origDimensions, setOrigDimensions] = useState<number[]>([0, 0]);
@@ -43,6 +56,7 @@ export const EditMedia: React.FC<EditMediaProps> = ({route, navigation}) => {
const vidRef = useRef<View>(null);
const [cropLoading, setCropLoading] = useState<boolean>(false);
const [hideTrimmer, setHideTrimmer] = useState<boolean>(true);
+ const [videoDuration, setVideoDuration] = useState<number | undefined>();
// Stores the coordinates of the cropped image
const [x0, setX0] = useState<number>();
@@ -142,7 +156,7 @@ export const EditMedia: React.FC<EditMediaProps> = ({route, navigation}) => {
mediaUri,
(croppedURL: string) => {
setCropLoading(false);
- // Pass the trimmed/cropped video
+ // Pass the cropped video
callback(croppedURL);
},
videoCrop,
@@ -251,6 +265,24 @@ export const EditMedia: React.FC<EditMediaProps> = ({route, navigation}) => {
);
};
+ const handleNext = () => {
+ if (momentUploadProgressBar) {
+ Alert.alert(ERROR_MOMENT_UPLOAD_IN_PROGRESS);
+ } else {
+ processVideo((uri) =>
+ navigation.navigate('CaptionScreen', {
+ screenType,
+ media: {
+ uri,
+ isVideo,
+ videoDuration,
+ },
+ selectedCategory,
+ }),
+ );
+ }
+ };
+
return (
<View style={styles.container}>
{cropLoading && <TaggLoadingIndicator fullscreen />}
@@ -338,8 +370,12 @@ export const EditMedia: React.FC<EditMediaProps> = ({route, navigation}) => {
height: SCREEN_WIDTH / aspectRatio,
},
]}
- handleLoad={(response: {width: number; height: number}) => {
+ handleLoad={(
+ response: {width: number; height: number},
+ duration: number,
+ ) => {
const {width, height} = response;
+ setVideoDuration(duration);
setOrigDimensions([width, height]);
setAspectRatio(width / height);
}}
@@ -386,18 +422,7 @@ export const EditMedia: React.FC<EditMediaProps> = ({route, navigation}) => {
/>
<TaggSquareButton
style={styles.button}
- onPress={() =>
- processVideo((uri) =>
- navigation.navigate('CaptionScreen', {
- screenType,
- media: {
- uri: uri,
- isVideo: isVideo,
- },
- selectedCategory,
- }),
- )
- }
+ onPress={handleNext}
title={'Next'}
buttonStyle={'large'}
buttonColor={'blue'}
diff --git a/src/services/MomentService.ts b/src/services/MomentService.ts
index 25d44041..b67cd169 100644
--- a/src/services/MomentService.ts
+++ b/src/services/MomentService.ts
@@ -1,6 +1,7 @@
import AsyncStorage from '@react-native-community/async-storage';
import RNFetchBlob from 'rn-fetch-blob';
import {
+ CHECK_MOMENT_UPLOAD_DONE_PROCESSING_ENDPOINT,
MOMENTS_ENDPOINT,
MOMENTTAG_ENDPOINT,
MOMENT_TAGS_ENDPOINT,
@@ -320,3 +321,22 @@ export const handleVideoUpload = async (
}
return false;
};
+
+export const checkMomentDoneProcessing = async (momentId: string) => {
+ try {
+ const token = await AsyncStorage.getItem('token');
+ const response = await fetch(
+ CHECK_MOMENT_UPLOAD_DONE_PROCESSING_ENDPOINT + '?moment_id=' + momentId,
+ {
+ method: 'GET',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ },
+ );
+ return response.status === 200;
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+};
diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts
index b1cb8719..1acbb519 100644
--- a/src/store/actions/user.ts
+++ b/src/store/actions/user.ts
@@ -1,13 +1,21 @@
import AsyncStorage from '@react-native-community/async-storage';
-import {StreamChat} from 'stream-chat';
import {Action, ThunkAction} from '@reduxjs/toolkit';
+import {StreamChat} from 'stream-chat';
import {
getProfilePic,
+ handlePresignedURL,
+ handleVideoUpload,
loadProfileInfo,
+ postMomentTags,
removeBadgesService,
sendSuggestedPeopleLinked,
} from '../../services';
-import {UniversityBadge, UserType} from '../../types/types';
+import {
+ MomentUploadProgressBarType,
+ MomentUploadStatusType,
+ UniversityBadge,
+ UserType,
+} from '../../types/types';
import {getTokenOrLogout} from '../../utils';
import {
clearHeaderAndProfileImages,
@@ -15,6 +23,7 @@ import {
profileBadgesUpdated,
profileCompletionStageUpdated,
setIsOnboardedUser,
+ setMomentUploadProgressBar,
setNewNotificationReceived,
setNewVersionAvailable,
setReplyPosted,
@@ -275,3 +284,88 @@ export const suggestedPeopleAnimatedTutorialFinished =
);
}
};
+
+/**
+ * state is now UploadingToS3:
+ * - get presigned url (backend creates the moment object)
+ * - upload moment tags
+ * - upload video to s3
+ * state is now WaitingForDoneProcessing
+ */
+export const handleVideoMomentUpload =
+ (
+ videoUri: string,
+ videoLength: number,
+ momentCategory: string,
+ formattedTags: {
+ x: number;
+ y: number;
+ z: number;
+ user_id: string;
+ }[],
+ ): ThunkAction<Promise<void>, RootState, unknown, Action<string>> =>
+ async (dispatch) => {
+ try {
+ const handleError = (reason: string) => {
+ console.error('Moment video upload failed,', reason);
+ dispatch({
+ type: setMomentUploadProgressBar.type,
+ payload: {
+ momentUploadProgressBar: {
+ ...momentUploadProgressBar,
+ status: MomentUploadStatusType.Error,
+ },
+ },
+ });
+ };
+ let momentUploadProgressBar: MomentUploadProgressBarType = {
+ status: MomentUploadStatusType.UploadingToS3,
+ momentId: '',
+ originalVideoDuration: videoLength,
+ };
+ // set progress bar as loading
+ dispatch({
+ type: setMomentUploadProgressBar.type,
+ payload: {momentUploadProgressBar},
+ });
+ // get a presigned url for the video
+ const presignedURLResponse = await handlePresignedURL(momentCategory);
+ if (!presignedURLResponse) {
+ handleError('Presigned URL failed');
+ return;
+ }
+ const momentId = presignedURLResponse.moment_id;
+ const fileHash = presignedURLResponse.response_url.fields.key;
+ // upload moment tags, now that we have a moment id
+ const momentTagResponse = await postMomentTags(momentId, formattedTags);
+ if (!momentTagResponse) {
+ handleError('Upload moment tags failed');
+ return;
+ }
+ if (!fileHash) {
+ handleError('Unable to parse file hash from presigned response');
+ return;
+ }
+ // upload video to s3
+ const videoUploadResponse = await handleVideoUpload(
+ videoUri,
+ presignedURLResponse,
+ );
+ if (!videoUploadResponse) {
+ handleError('Video upload failed');
+ return;
+ }
+ dispatch({
+ type: setMomentUploadProgressBar.type,
+ payload: {
+ momentUploadProgressBar: {
+ ...momentUploadProgressBar,
+ status: MomentUploadStatusType.WaitingForDoneProcessing,
+ momentId,
+ },
+ },
+ });
+ } catch (error) {
+ console.log(error);
+ }
+ };
diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts
index 92a1e456..7d8cf439 100644
--- a/src/store/initialStates.ts
+++ b/src/store/initialStates.ts
@@ -10,6 +10,7 @@ import {
import {
CommentThreadType,
MomentPostType,
+ MomentUploadProgressBarType,
UniversityType,
} from './../types/types';
@@ -48,6 +49,7 @@ export const NO_USER_DATA = {
profile: <ProfileInfoType>NO_PROFILE,
avatar: <string | undefined>undefined,
cover: <string | undefined>undefined,
+ momentUploadProgressBar: <MomentUploadProgressBarType | undefined>undefined,
isOnboardedUser: false,
newVersionAvailable: false,
newNotificationReceived: false,
diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts
index 4692c5d3..617c60be 100644
--- a/src/store/reducers/userReducer.ts
+++ b/src/store/reducers/userReducer.ts
@@ -85,6 +85,10 @@ const userDataSlice = createSlice({
state.avatar = '';
state.cover = '';
},
+
+ setMomentUploadProgressBar: (state, action) => {
+ state.momentUploadProgressBar = action.payload.momentUploadProgressBar;
+ },
},
});
@@ -102,5 +106,6 @@ export const {
clearHeaderAndProfileImages,
profileBadgesUpdated,
profileBadgeRemoved,
+ setMomentUploadProgressBar,
} = userDataSlice.actions;
export const userDataReducer = userDataSlice.reducer;
diff --git a/src/types/types.ts b/src/types/types.ts
index 5f70d1f8..685e3784 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -61,6 +61,19 @@ export interface ProfileInfoType {
is_private: boolean;
}
+export interface MomentUploadProgressBarType {
+ status: MomentUploadStatusType;
+ momentId: string;
+ originalVideoDuration: number | undefined;
+}
+
+export enum MomentUploadStatusType {
+ UploadingToS3 = 'UploadingToS3',
+ WaitingForDoneProcessing = 'WaitingForDoneProcessing',
+ Done = 'Done',
+ Error = 'Error',
+}
+
export interface SocialAccountType {
handle?: string;
profile_pic?: string;
diff --git a/src/utils/camera.ts b/src/utils/camera.ts
index c3822858..8104ba74 100644
--- a/src/utils/camera.ts
+++ b/src/utils/camera.ts
@@ -57,7 +57,7 @@ export const saveImageToGallery = (
.catch((_err) => Alert.alert('Failed to save to device!'));
};
-export const navigateToImagePicker = (
+export const navigateToMediaPicker = (
callback: (media: ImageOrVideo) => void,
) => {
ImagePicker.openPicker({