aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMichael <michael.foiani@gmail.com>2021-08-02 11:41:51 -0400
committerMichael <michael.foiani@gmail.com>2021-08-02 11:41:51 -0400
commite3f8180e24b35eccdb49fe766b9e1fe10c33da3f (patch)
tree1841dda83317d0b3ac922691493197aacb036677 /src
parentcf2a5b7294ed3c51898febf393a50108e2b9825d (diff)
parent452f3fb44838c367f40e8aa57db2e274a357afd2 (diff)
Pull from master
Diffstat (limited to 'src')
-rw-r--r--src/components/common/GradientProgressBar.tsx25
-rw-r--r--src/components/moments/IndividualMomentTitleBar.tsx53
-rw-r--r--src/components/moments/MomentPost.tsx131
-rw-r--r--src/components/moments/MomentUploadProgressBar.tsx17
-rw-r--r--src/components/moments/index.ts1
-rw-r--r--src/components/profile/Content.tsx2
-rw-r--r--src/constants/constants.ts2
-rw-r--r--src/constants/strings.ts1
-rw-r--r--src/routes/main/MainStackScreen.tsx49
-rw-r--r--src/screens/main/NotificationsScreen.tsx4
-rw-r--r--src/screens/moments/CameraScreen.tsx48
-rw-r--r--src/screens/moments/TagFriendsScreen.tsx104
-rw-r--r--src/screens/profile/CaptionScreen.tsx73
-rw-r--r--src/screens/suggestedPeople/index.ts1
-rw-r--r--src/services/MomentService.ts6
-rw-r--r--src/store/actions/user.ts90
-rw-r--r--src/utils/camera.ts16
17 files changed, 340 insertions, 283 deletions
diff --git a/src/components/common/GradientProgressBar.tsx b/src/components/common/GradientProgressBar.tsx
index fc62bd3c..066295d0 100644
--- a/src/components/common/GradientProgressBar.tsx
+++ b/src/components/common/GradientProgressBar.tsx
@@ -2,20 +2,21 @@ 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>;
+ toColor: string;
+ fromColor: string;
+ unfilledColor: string;
}
const GradientProgressBar: FC<GradientProgressBarProps> = ({
style,
progress,
+ toColor,
+ fromColor,
+ unfilledColor,
}) => {
const animatedProgressStyle = useAnimatedStyle<ViewStyle>(() => ({
width: `${(1 - progress.value) * 100}%`,
@@ -24,8 +25,16 @@ const GradientProgressBar: FC<GradientProgressBarProps> = ({
<LinearGradient
style={[styles.bar, style]}
useAngle={true}
- colors={[TAGG_PURPLE, TAGG_LIGHT_BLUE_2]}>
- <Animated.View style={[styles.blank, animatedProgressStyle]} />
+ colors={[fromColor, toColor]}>
+ <Animated.View
+ style={[
+ styles.blank,
+ animatedProgressStyle,
+ {
+ backgroundColor: unfilledColor,
+ },
+ ]}
+ />
</LinearGradient>
);
};
@@ -36,12 +45,12 @@ const styles = StyleSheet.create({
bar: {
height: normalize(10),
borderRadius: 6.5,
+ opacity: 1,
},
blank: {
alignSelf: 'flex-end',
height: normalize(10),
width: '80%',
- backgroundColor: TAGG_LIGHT_BLUE_3,
},
});
diff --git a/src/components/moments/IndividualMomentTitleBar.tsx b/src/components/moments/IndividualMomentTitleBar.tsx
deleted file mode 100644
index c6bf1423..00000000
--- a/src/components/moments/IndividualMomentTitleBar.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-import {StyleSheet, Text, View, ViewProps} from 'react-native';
-import {normalize, SCREEN_WIDTH} from '../../utils';
-
-interface IndividualMomentTitleBarProps extends ViewProps {
- title: string;
-}
-const IndividualMomentTitleBar: React.FC<IndividualMomentTitleBarProps> = ({
- title,
-}) => {
- return (
- <View style={styles.mainContainer}>
- <View style={styles.titleContainer}>
- <Text
- style={[
- styles.title,
- {
- fontSize: title.length > 18 ? normalize(14) : normalize(16),
- },
- ]}>
- {title}
- </Text>
- </View>
- </View>
- );
-};
-
-const styles = StyleSheet.create({
- title: {
- textAlign: 'center',
- color: 'white',
- fontSize: normalize(18),
- fontWeight: '700',
- lineHeight: normalize(21.48),
- letterSpacing: normalize(1.3),
- },
- titleContainer: {
- width: '80%',
- position: 'absolute',
- left: '10%',
- right: '10%',
- height: normalize(70),
- },
- mainContainer: {
- flex: 1,
- width: SCREEN_WIDTH * 0.6,
- flexDirection: 'row',
- justifyContent: 'flex-end',
- marginVertical: '2%',
- },
-});
-
-export default IndividualMomentTitleBar;
diff --git a/src/components/moments/MomentPost.tsx b/src/components/moments/MomentPost.tsx
index c232986d..939c0cf6 100644
--- a/src/components/moments/MomentPost.tsx
+++ b/src/components/moments/MomentPost.tsx
@@ -1,4 +1,4 @@
-import {useNavigation} from '@react-navigation/native';
+import {useIsFocused, useNavigation} from '@react-navigation/native';
import React, {useContext, useEffect, useMemo, useRef, useState} from 'react';
import {
Image,
@@ -14,15 +14,18 @@ import {
} from 'react-native';
// @ts-ignore
import Pinchable from 'react-native-pinchable';
-import Animated, {EasingNode} from 'react-native-reanimated';
+import Animated, {
+ Easing,
+ EasingNode,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated';
import {SafeAreaView} from 'react-native-safe-area-context';
-import SimpleGradientProgressbarView from 'react-native-simple-gradient-progressbar-view';
import Video from 'react-native-video';
import {useDispatch, useSelector, useStore} from 'react-redux';
import {TaggedUsersDrawer} from '.';
import PauseIcon from '../../assets/icons/pause-icon.svg';
-import {TAGG_LIGHT_BLUE_2, TAGG_PURPLE} from '../../constants/constants';
-import {headerBarOptions} from '../../routes';
+import {headerBarOptions, multilineHeaderTitle} from '../../routes';
import {MomentContext} from '../../screens/profile/IndividualMoment';
import {deleteMomentTag, loadMomentTags} from '../../services';
import {loadUserMoments} from '../../store/actions';
@@ -30,18 +33,15 @@ import {RootState} from '../../store/rootReducer';
import {MomentPostType, MomentTagType, ScreenType, UserType} from '../../types';
import {
getTimePosted,
- isIPhoneX,
navigateToProfile,
normalize,
SCREEN_HEIGHT,
SCREEN_WIDTH,
} from '../../utils';
import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments';
-import {AddComment} from '../comments';
import CommentsCount from '../comments/CommentsCount';
-import {MomentTags} from '../common';
+import {GradientProgressBar, MomentTags} from '../common';
import {MomentMoreInfoDrawer, TaggAvatar} from '../profile';
-import IndividualMomentTitleBar from './IndividualMomentTitleBar';
interface MomentPostProps {
moment: MomentPostType;
userXId: string | undefined;
@@ -75,9 +75,6 @@ const MomentPost: React.FC<MomentPostProps> = ({
const [fadeValue, setFadeValue] = useState<Animated.Value<number>>(
new Animated.Value(0),
);
- const [commentCount, setCommentCount] = useState<number>(
- moment.comments_count,
- );
const [aspectRatio, setAspectRatio] = useState<number>(1);
const [momentTagId, setMomentTagId] = useState<string>('');
@@ -94,7 +91,14 @@ const MomentPost: React.FC<MomentPostProps> = ({
);
const mediaHeight = SCREEN_WIDTH / aspectRatio;
const [isVideoPaused, setIsVideoPaused] = useState<boolean>(false);
- const [videoProgress, setVideoProgress] = useState<number>(0);
+ const screenIsFocused = useIsFocused();
+ const videoProgress = useSharedValue(0);
+
+ // update play/pause icon based on video pause state
+ useEffect(() => {
+ setFadeValue(new Animated.Value(isVideoPaused ? 1 : 0));
+ }, [isVideoPaused]);
+
/*
* Load tags on initial render to pass tags data to moment header and content
*/
@@ -132,16 +136,18 @@ const MomentPost: React.FC<MomentPostProps> = ({
}
};
- useEffect(
- () =>
+ useEffect(() => {
+ if (moment.moment_category.length > 20) {
navigation.setOptions({
...headerBarOptions('white', ''),
- headerTitle: () => (
- <IndividualMomentTitleBar title={moment.moment_category} />
- ),
- }),
- [moment.moment_id],
- );
+ ...multilineHeaderTitle(moment.moment_category),
+ });
+ } else {
+ navigation.setOptions({
+ ...headerBarOptions('white', moment.moment_category),
+ });
+ }
+ }, [moment.moment_id]);
/*
* Determines if an image is 9:16 to set aspect ratio of current image and
@@ -229,14 +235,24 @@ const MomentPost: React.FC<MomentPostProps> = ({
const {width, height} = response.naturalSize;
setAspectRatio(width / height);
}}
- paused={moment.moment_id !== currentVisibleMomentId || isVideoPaused}
+ paused={
+ moment.moment_id !== currentVisibleMomentId ||
+ isVideoPaused ||
+ !screenIsFocused
+ }
onProgress={({currentTime, seekableDuration}) => {
const localProgress = currentTime / seekableDuration;
if (!isNaN(localProgress)) {
- setVideoProgress(localProgress);
+ videoProgress.value = withTiming(localProgress, {
+ duration: 250,
+ easing: Easing.linear,
+ });
}
}}
- onEnd={updateMomentViewCount}
+ onEnd={() => {
+ updateMomentViewCount();
+ videoProgress.value = 0;
+ }}
/>
{isVideoPaused && (
<Animated.View
@@ -260,20 +276,6 @@ const MomentPost: React.FC<MomentPostProps> = ({
/>
);
- const ProgressBar = () => (
- <View style={styles.progressBaContainer}>
- <SimpleGradientProgressbarView
- progress={videoProgress}
- style={styles.progressBar}
- fromColor={TAGG_PURPLE}
- toColor={TAGG_LIGHT_BLUE_2}
- />
- <View
- style={[styles.progressDot, {left: videoProgress * SCREEN_WIDTH - 5}]}
- />
- </View>
- );
-
return (
<>
<StatusBar barStyle={'light-content'} />
@@ -303,9 +305,6 @@ const MomentPost: React.FC<MomentPostProps> = ({
onPress={() => {
if (isVideo) {
setIsVideoPaused(!isVideoPaused);
- isVideoPaused
- ? setFadeValue(new Animated.Value(0))
- : setFadeValue(new Animated.Value(1));
} else {
setTagsVisible(!tagsVisible);
setFadeValue(new Animated.Value(0));
@@ -353,7 +352,7 @@ const MomentPost: React.FC<MomentPostProps> = ({
<View style={styles.commentsCountContainer}>
<CommentsCount
momentId={moment.moment_id}
- count={commentCount}
+ count={moment.comments_count}
screenType={screenType}
/>
</View>
@@ -374,22 +373,18 @@ const MomentPost: React.FC<MomentPostProps> = ({
),
})}
<View>
- <AddComment
- placeholderText={'Add a comment here!'}
- momentId={moment.moment_id}
- callback={() => {
- setCommentCount(commentCount + 1);
- }}
- onFocus={() => {
- setHideText(true);
- }}
- isKeyboardAvoiding={false}
- theme={'dark'}
- />
- {isVideo && <ProgressBar />}
<Text style={styles.text}>
{getTimePosted(moment.date_created)}
</Text>
+ {isVideo && (
+ <GradientProgressBar
+ style={styles.progressBar}
+ progress={videoProgress}
+ toColor={'#fff'}
+ fromColor={'#fff'}
+ unfilledColor={'#808080'}
+ />
+ )}
</View>
</KeyboardAvoidingView>
</View>
@@ -409,7 +404,8 @@ const styles = StyleSheet.create({
color: 'white',
fontWeight: '500',
textAlign: 'right',
- marginTop: 18,
+ marginTop: 10,
+ marginBottom: 10,
},
captionText: {
position: 'relative',
@@ -501,26 +497,13 @@ const styles = StyleSheet.create({
top: '40%',
},
progressBar: {
- position: 'absolute',
- top: 0,
- width: SCREEN_WIDTH,
- height: 5,
+ width: SCREEN_WIDTH * 0.99,
+ height: 3,
+ marginHorizontal: 2,
},
- progressDot: {
- backgroundColor: '#fff',
- width: 10,
- height: 10,
- borderRadius: 10,
- borderWidth: 0.3,
- borderColor: TAGG_PURPLE,
- position: 'absolute',
- top: -2.5,
- },
- progressBaContainer: {
- position: 'absolute',
- top: isIPhoneX() ? 75 : 70,
+ profilePreviewContainer: {
+ paddingHorizontal: '3%',
},
- profilePreviewContainer: {paddingHorizontal: '3%'},
});
export default MomentPost;
diff --git a/src/components/moments/MomentUploadProgressBar.tsx b/src/components/moments/MomentUploadProgressBar.tsx
index d56a8337..96f9fa27 100644
--- a/src/components/moments/MomentUploadProgressBar.tsx
+++ b/src/components/moments/MomentUploadProgressBar.tsx
@@ -8,6 +8,11 @@ import {
withTiming,
} from 'react-native-reanimated';
import {useDispatch, useSelector} from 'react-redux';
+import {
+ TAGG_LIGHT_BLUE_2,
+ TAGG_LIGHT_BLUE_3,
+ TAGG_PURPLE,
+} from '../../constants';
import {checkMomentDoneProcessing} from '../../services';
import {loadUserMoments} from '../../store/actions';
import {setMomentUploadProgressBar} from '../../store/reducers';
@@ -51,7 +56,6 @@ const MomentUploadProgressBar: React.FC<MomentUploadProgressBarProps> =
});
// change status to Done 1s after the progress bar animation is done
setTimeout(() => {
- dispatch(loadUserMoments(loggedInUserId));
dispatch({
type: setMomentUploadProgressBar.type,
payload: {
@@ -110,6 +114,9 @@ const MomentUploadProgressBar: React.FC<MomentUploadProgressBarProps> =
}, [momentUploadProgressBar?.status]);
useEffect(() => {
+ if (momentUploadProgressBar?.status === MomentUploadStatusType.Done) {
+ dispatch(loadUserMoments(loggedInUserId));
+ }
if (
momentUploadProgressBar?.status === MomentUploadStatusType.Done ||
momentUploadProgressBar?.status === MomentUploadStatusType.Error
@@ -143,7 +150,13 @@ const MomentUploadProgressBar: React.FC<MomentUploadProgressBarProps> =
{showLoading && (
<>
<Text style={styles.text}>Uploading Moment...</Text>
- <GradientProgressBar style={styles.bar} progress={progress} />
+ <GradientProgressBar
+ style={styles.bar}
+ progress={progress}
+ toColor={TAGG_LIGHT_BLUE_2}
+ fromColor={TAGG_PURPLE}
+ unfilledColor={TAGG_LIGHT_BLUE_3}
+ />
</>
)}
{momentUploadProgressBar.status === MomentUploadStatusType.Done && (
diff --git a/src/components/moments/index.ts b/src/components/moments/index.ts
index 3f33ec53..4c1c509e 100644
--- a/src/components/moments/index.ts
+++ b/src/components/moments/index.ts
@@ -1,4 +1,3 @@
-export {default as IndividualMomentTitleBar} from './IndividualMomentTitleBar';
export {default as CaptionScreenHeader} from './CaptionScreenHeader';
export {default as Moment} from './Moment';
export {default as TagFriendsFooter} from './TagFriendsFoooter';
diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx
index 9edd890d..df692a3f 100644
--- a/src/components/profile/Content.tsx
+++ b/src/components/profile/Content.tsx
@@ -136,7 +136,7 @@ const Content: React.FC<ContentProps> = ({userXId, screenType}) => {
onScroll={scrollHandler}
showsVerticalScrollIndicator={false}
scrollEventThrottle={1}
- stickyHeaderIndices={[4]}
+ stickyHeaderIndices={[5]}
scrollEnabled={scrollEnabled}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
diff --git a/src/constants/constants.ts b/src/constants/constants.ts
index 476e7af4..d25a5953 100644
--- a/src/constants/constants.ts
+++ b/src/constants/constants.ts
@@ -246,3 +246,5 @@ export const SETTINGS_DATA = {
},
],
};
+
+export const MAX_VIDEO_RECORDING_DURATION = 60;
diff --git a/src/constants/strings.ts b/src/constants/strings.ts
index 450d7e5c..47ad2464 100644
--- a/src/constants/strings.ts
+++ b/src/constants/strings.ts
@@ -56,6 +56,7 @@ export const ERROR_UNABLE_TO_FIND_PROFILE = 'We were unable to find this profile
export const ERROR_UNABLE_TO_VIEW_PROFILE = 'Unable to view this profile';
export const ERROR_UPLOAD = 'An error occurred while uploading. Please try again!';
export const ERROR_UPLOAD_BADGES = 'Unable to upload your badges. Please retry!';
+export const ERROR_UPLOAD_EXCEED_MAX_VIDEO_DURATION = 'Video can\'t be longer than 60 seconds!';
export const ERROR_UPLOAD_LARGE_PROFILE_PIC = "Can't have the first image seen on the profile be blank, please upload a large picture";
export const ERROR_UPLOAD_MOMENT = 'Unable to upload moment. Please retry';
export const ERROR_UPLOAD_SMALL_PROFILE_PIC = "Can't have a profile without a pic to represent you, please upload a small profile picture";
diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx
index 064e9725..64060554 100644
--- a/src/routes/main/MainStackScreen.tsx
+++ b/src/routes/main/MainStackScreen.tsx
@@ -2,25 +2,28 @@ import {RouteProp} from '@react-navigation/native';
import {StackNavigationOptions} from '@react-navigation/stack';
import React from 'react';
import {StyleSheet, Text} from 'react-native';
-import {normalize} from 'react-native-elements';
import BackIcon from '../../assets/icons/back-arrow.svg';
import {
AccountType,
AnimatedTutorial,
BadgeSelection,
+ CameraScreen,
CaptionScreen,
CategorySelection,
ChatListScreen,
ChatScreen,
+ ChoosingCategoryScreen,
CommentReactionScreen,
CreateCustomCategory,
DiscoverUsers,
+ EditMedia,
EditProfile,
FriendsListScreen,
IndividualMoment,
InviteFriendsScreen,
MomentCommentsScreen,
MomentUploadPromptScreen,
+ MutualBadgeHolders,
NewChatModal,
NotificationsScreen,
PrivacyScreen,
@@ -31,16 +34,17 @@ import {
SuggestedPeopleScreen,
SuggestedPeopleUploadPictureScreen,
SuggestedPeopleWelcomeScreen,
- TagSelectionScreen,
TagFriendsScreen,
- CameraScreen,
- EditMedia,
+ TagSelectionScreen,
} from '../../screens';
-import MutualBadgeHolders from '../../screens/suggestedPeople/MutualBadgeHolders';
import {ScreenType} from '../../types';
-import {AvatarHeaderHeight, ChatHeaderHeight, SCREEN_WIDTH} from '../../utils';
+import {
+ AvatarHeaderHeight,
+ ChatHeaderHeight,
+ normalize,
+ SCREEN_WIDTH,
+} from '../../utils';
import {MainStack, MainStackParams} from './MainStackNavigator';
-import ChoosingCategoryScreen from '../../screens/profile/ChoosingCategoryScreen';
/**
* Profile : To display the logged in user's profile when the userXId passed in to it is (undefined | null | empty string) else displays profile of the user being visited.
@@ -378,6 +382,7 @@ export const headerBarOptions: (
),
headerTitle: () => (
<Text
+ numberOfLines={1}
style={[
styles.headerTitle,
{color: color},
@@ -388,6 +393,23 @@ export const headerBarOptions: (
),
});
+export const multilineHeaderTitle: (title: string) => StackNavigationOptions = (
+ title,
+) => ({
+ headerTitle: () => (
+ <Text
+ numberOfLines={3}
+ style={[
+ styles.multilineHeaderTitle,
+ {
+ fontSize: title.length > 18 ? normalize(14) : normalize(16),
+ },
+ ]}>
+ {title}
+ </Text>
+ ),
+});
+
export const modalStyle: StackNavigationOptions = {
cardStyle: {backgroundColor: 'rgba(80,80,80,0.6)'},
gestureDirection: 'vertical',
@@ -413,8 +435,21 @@ const styles = StyleSheet.create({
shadowOffset: {width: 0, height: 0},
},
headerTitle: {
+ width: SCREEN_WIDTH * 0.7,
+ textAlign: 'center',
+ lineHeight: normalize(21.48),
+ letterSpacing: normalize(1.3),
+ fontWeight: '700',
+ },
+ multilineHeaderTitle: {
+ width: SCREEN_WIDTH * 0.7,
+ height: normalize(70),
+ marginTop: normalize(90) / 2,
+ textAlign: 'center',
+ lineHeight: normalize(21.48),
letterSpacing: normalize(1.3),
fontWeight: '700',
+ color: 'white',
},
});
diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx
index b19107a7..55dd9051 100644
--- a/src/screens/main/NotificationsScreen.tsx
+++ b/src/screens/main/NotificationsScreen.tsx
@@ -1,4 +1,5 @@
import AsyncStorage from '@react-native-community/async-storage';
+import PushNotificationIOS from '@react-native-community/push-notification-ios';
import {useFocusEffect, useNavigation} from '@react-navigation/native';
import moment from 'moment';
import React, {useCallback, useEffect, useState} from 'react';
@@ -21,7 +22,6 @@ import FindFriendsBlueIcon from '../../assets/icons/findFriends/find-friends-blu
import {TabsGradient} from '../../components';
import EmptyContentView from '../../components/common/EmptyContentView';
import {Notification} from '../../components/notifications';
-import {NewChatPrompt} from '../../components/notifications/NotificationPrompts';
import {
loadUserNotifications,
updateNewNotificationReceived,
@@ -29,7 +29,6 @@ import {
import {RootState} from '../../store/rootReducer';
import {NotificationType, ScreenType} from '../../types';
import {getDateAge, normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
-import PushNotificationIOS from '@react-native-community/push-notification-ios';
const NotificationsScreen: React.FC = () => {
const {newNotificationReceived} = useSelector(
@@ -299,7 +298,6 @@ const NotificationsScreen: React.FC = () => {
renderItem={renderNotification}
renderSectionHeader={renderSectionHeader}
renderSectionFooter={renderSectionFooter}
- ListHeaderComponent={<NewChatPrompt />}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
diff --git a/src/screens/moments/CameraScreen.tsx b/src/screens/moments/CameraScreen.tsx
index 27412486..07b697d0 100644
--- a/src/screens/moments/CameraScreen.tsx
+++ b/src/screens/moments/CameraScreen.tsx
@@ -4,14 +4,14 @@ import {RouteProp} from '@react-navigation/core';
import {useFocusEffect} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
import React, {createRef, useCallback, useEffect, useState} from 'react';
-import {StyleSheet, TouchableOpacity, View} from 'react-native';
+import {Modal, StyleSheet, TouchableOpacity, View} from 'react-native';
import {CameraType, FlashMode, RNCamera} from 'react-native-camera';
import {AnimatedCircularProgress} from 'react-native-circular-progress';
import CloseIcon from '../../assets/ionicons/close-outline.svg';
import {FlashButton, FlipButton, GalleryIcon} from '../../components';
-import {TAGG_PURPLE} from '../../constants';
+import {MAX_VIDEO_RECORDING_DURATION, TAGG_PURPLE} from '../../constants';
import {MainStackParams} from '../../routes';
-import {HeaderHeight, SCREEN_WIDTH} from '../../utils';
+import {HeaderHeight, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
import {showGIFFailureAlert, takePicture, takeVideo} from '../../utils/camera';
type CameraScreenRouteProps = RouteProp<MainStackParams, 'CameraScreen'>;
@@ -37,6 +37,7 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => {
navigation.dangerouslyGetParent()?.setOptions({
tabBarVisible: false,
});
+ return () => setIsRecording(false);
}, [navigation]),
);
@@ -84,6 +85,11 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => {
return (
<View style={styles.container}>
+ <Modal
+ transparent={true}
+ visible={isRecording && cameraType === 'front' && flashMode === 'on'}>
+ <View style={styles.flashView} />
+ </Modal>
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<CloseIcon height={25} width={25} color={'white'} />
</TouchableOpacity>
@@ -92,7 +98,11 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => {
ref={cameraRef}
style={styles.camera}
type={cameraType}
- flashMode={flashMode}
+ flashMode={
+ flashMode === 'on' && isRecording && cameraType === 'back'
+ ? 'torch'
+ : flashMode
+ }
onDoubleTap={() => {
setCameraType(cameraType === 'front' ? 'back' : 'front');
}}
@@ -111,10 +121,24 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => {
setIsRecording(true);
}}
onPressOut={async () => {
- if (await cameraRef.current?.isRecording()) {
- cameraRef.current?.stopRecording();
- setIsRecording(false);
- }
+ const cancelRecording = async () => {
+ if (await cameraRef.current?.isRecording()) {
+ cameraRef.current?.stopRecording();
+ setIsRecording(false);
+ }
+ };
+ cancelRecording();
+ // tmp fix for when the animation glitches during the beginning of
+ // recording causing onPressOut to not be detected.
+ setTimeout(() => {
+ cancelRecording();
+ }, 500);
+ setTimeout(() => {
+ cancelRecording();
+ }, 1000);
+ setTimeout(() => {
+ cancelRecording();
+ }, 1500);
}}
onPress={() => {
takePicture(cameraRef, (pic) => navigateToEditMedia(pic.uri));
@@ -127,7 +151,7 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => {
width={6}
fill={100}
rotation={0}
- duration={60000 + 1000} // an extra second for UI to load
+ duration={(MAX_VIDEO_RECORDING_DURATION + 1) * 1000} // an extra second for UI to load
tintColor={TAGG_PURPLE}
style={styles.bottomContainer}
lineCap={'round'}
@@ -164,6 +188,12 @@ const styles = StyleSheet.create({
flexDirection: 'column',
backgroundColor: 'black',
},
+ flashView: {
+ width: SCREEN_WIDTH,
+ height: SCREEN_HEIGHT,
+ backgroundColor: '#fff',
+ opacity: 0.5,
+ },
captureButtonVideoContainer: {
alignSelf: 'center',
backgroundColor: 'transparent',
diff --git a/src/screens/moments/TagFriendsScreen.tsx b/src/screens/moments/TagFriendsScreen.tsx
index d11f8049..c55721ed 100644
--- a/src/screens/moments/TagFriendsScreen.tsx
+++ b/src/screens/moments/TagFriendsScreen.tsx
@@ -10,15 +10,13 @@ import {
View,
} from 'react-native';
import Video from 'react-native-video';
-import {MainStackParams} from 'src/routes';
-import BackArrow from '../../assets/icons/back-arrow.svg';
import {MomentTags} from '../../components';
import {TagFriendsFooter} from '../../components/moments';
+import {headerBarOptions, MainStackParams} from '../../routes';
import {MomentTagType} from '../../types';
import {
HeaderHeight,
isIPhoneX,
- normalize,
SCREEN_HEIGHT,
SCREEN_WIDTH,
} from '../../utils';
@@ -48,6 +46,36 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => {
setTags(selectedTags ? selectedTags : []);
}, [selectedTags]);
+ useEffect(() => {
+ const title = media.isVideo
+ ? ''
+ : tags.length === 0
+ ? 'Tap on photo to tag friends!'
+ : 'Press and drag to move';
+ navigation.setOptions({
+ ...headerBarOptions('white', title),
+ headerRight: () => (
+ <TouchableOpacity
+ style={styles.buttonContainer}
+ // Altering the opacity style of TouchableOpacity doesn't work,
+ // so the next two lines are needed
+ disabled={tags.length === 0}
+ activeOpacity={tags.length === 0 ? 0 : 1}
+ onPress={handleDone}>
+ <Text
+ style={[
+ styles.shareButtonTitle,
+ // makes the Done buttomn invisible if there are no tags
+ // eslint-disable-next-line react-native/no-inline-styles
+ {opacity: tags.length !== 0 ? 1 : 0},
+ ]}>
+ Done
+ </Text>
+ </TouchableOpacity>
+ ),
+ });
+ }, [tags]);
+
/*
* Navigate back to Tag Users Screen, send selected users
*/
@@ -174,49 +202,8 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => {
onLayout={(event) => {
const {y, height} = event.nativeEvent.layout;
setTopHeight(y + height);
- }}>
- <TouchableOpacity
- onPress={() => {
- navigation.goBack();
- }}
- style={styles.backArrow}>
- <View style={styles.backArrowContainer}>
- <BackArrow
- height={normalize(18)}
- width={normalize(18)}
- color={'white'}
- />
- </View>
- </TouchableOpacity>
- {!media.isVideo ? (
- <TouchableWithoutFeedback style={styles.headerContainer}>
- {tags.length === 0 ? (
- <Text style={styles.header}>Tap on photo to tag friends!</Text>
- ) : (
- <Text style={styles.header}>Press and drag to move</Text>
- )}
- </TouchableWithoutFeedback>
- ) : (
- <View style={styles.headerPlaceholder} />
- )}
- <TouchableOpacity
- style={styles.buttonContainer}
- // Altering the opacity style of TouchableOpacity doesn't work,
- // so the next two lines are needed
- disabled={tags.length === 0}
- activeOpacity={tags.length === 0 ? 0 : 1}
- onPress={handleDone}>
- <Text
- style={[
- styles.shareButtonTitle,
- // makes the Done buttomn invisible if there are no tags
- // eslint-disable-next-line react-native/no-inline-styles
- {opacity: tags.length !== 0 ? 1 : 0},
- ]}>
- Done
- </Text>
- </TouchableOpacity>
- </View>
+ }}
+ />
{tags.length !== 0 && !media.isVideo && (
<MomentTags
tags={tags}
@@ -244,34 +231,11 @@ const styles = StyleSheet.create({
height: SCREEN_HEIGHT,
alignContent: 'center',
},
- backArrow: {
- marginTop: isIPhoneX() ? HeaderHeight : '10%',
- zIndex: 9999,
- },
- backArrowContainer: {
- flex: 1,
- flexDirection: 'column',
- justifyContent: 'center',
- alignContent: 'center',
- },
button: {
zIndex: 9999,
},
buttonContainer: {
- marginTop: isIPhoneX() ? HeaderHeight : '10%',
- right: 0,
- zIndex: 9999,
- flexDirection: 'row',
- justifyContent: 'flex-end',
- },
- headerContainer: {
- width: SCREEN_WIDTH,
- flexDirection: 'row',
- justifyContent: 'center',
- zIndex: 9999,
- },
- headerPlaceholder: {
- width: SCREEN_WIDTH * 0.5,
+ right: 20,
},
shareButtonTitle: {
fontWeight: 'bold',
diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx
index 3ee0bd5b..d329c589 100644
--- a/src/screens/profile/CaptionScreen.tsx
+++ b/src/screens/profile/CaptionScreen.tsx
@@ -30,15 +30,14 @@ import {
ERROR_NO_MOMENT_CATEGORY,
ERROR_SOMETHING_WENT_WRONG_REFRESH,
ERROR_UPLOAD,
- SUCCESS_PIC_UPLOAD,
} from '../../constants/strings';
import * as RootNavigation from '../../RootNavigation';
import {MainStackParams} from '../../routes';
-import {patchMoment, postMoment, postMomentTags} from '../../services';
+import {patchMoment} from '../../services';
import {
+ handleImageMomentUpload,
handleVideoMomentUpload,
loadUserMoments,
- updateProfileCompletionStage,
} from '../../store/actions';
import {RootState} from '../../store/rootReducer';
import {MomentTagType} from '../../types';
@@ -138,11 +137,6 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
// then switch to the profile tab
navigation.popToTop();
RootNavigation.navigate('ProfileTab');
- setTimeout(() => {
- if (!isMediaAVideo) {
- Alert.alert(SUCCESS_PIC_UPLOAD);
- }
- }, 500);
} else {
// if editing, simply go back to profile screen
navigation.navigate('Profile', {
@@ -167,53 +161,30 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
handleFailed(true);
return;
}
- let profileCompletionStage;
- // separate upload logic for image/video
if (isMediaAVideo) {
- if (videoDuration) {
- dispatch(
- handleVideoMomentUpload(
- mediaUri,
- videoDuration,
- momentCategory,
- formattedTags(),
- ),
- );
- } else {
- handleFailed();
- return;
- }
- } else {
- const momentResponse = await postMoment(
- mediaUri,
- caption,
- momentCategory,
- userId,
+ dispatch(
+ handleVideoMomentUpload(
+ mediaUri,
+ videoDuration ?? 30,
+ caption,
+ momentCategory,
+ formattedTags(),
+ ),
);
- if (!momentResponse) {
- handleFailed();
- return;
- }
- profileCompletionStage = momentResponse.profile_completion_stage;
- const momentId = momentResponse.moment_id;
- if (momentId) {
- const momentTagResponse = await postMomentTags(
- momentId,
+ } else {
+ dispatch(
+ handleImageMomentUpload(
+ mediaUri,
+ caption,
+ momentCategory,
+ userId,
formattedTags(),
- );
- if (!momentTagResponse) {
- handleFailed();
- return;
- }
- }
- }
- if (!isMediaAVideo) {
- dispatch(loadUserMoments(userId));
- }
- if (profileCompletionStage) {
- dispatch(updateProfileCompletionStage(profileCompletionStage));
+ ),
+ );
}
- handleSuccess();
+ setTimeout(() => {
+ handleSuccess();
+ }, 500);
};
const handleSubmitEditChanges = async () => {
diff --git a/src/screens/suggestedPeople/index.ts b/src/screens/suggestedPeople/index.ts
index 8c06d81e..be2393b5 100644
--- a/src/screens/suggestedPeople/index.ts
+++ b/src/screens/suggestedPeople/index.ts
@@ -1,2 +1,3 @@
export {default as SuggestedPeopleScreen} from './SuggestedPeopleScreen';
export {default as AnimatedTutorial} from './AnimatedTutorial';
+export {default as MutualBadgeHolders} from './MutualBadgeHolders';
diff --git a/src/services/MomentService.ts b/src/services/MomentService.ts
index 3a677ccc..0292f9ea 100644
--- a/src/services/MomentService.ts
+++ b/src/services/MomentService.ts
@@ -223,7 +223,10 @@ export const deleteMomentTag = async (moment_tag_id: string) => {
* @param value: string | undefined
* @returns a PresignedURLResponse object
*/
-export const handlePresignedURL = async (momentCategory: string) => {
+export const handlePresignedURL = async (
+ caption: string,
+ momentCategory: string,
+) => {
try {
// TODO: just a random filename for video poc, we should not need to once complete
const randHash = Math.random().toString(36).substring(7);
@@ -236,6 +239,7 @@ export const handlePresignedURL = async (momentCategory: string) => {
},
body: JSON.stringify({
filename,
+ caption: caption,
category: momentCategory,
}),
});
diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts
index 1acbb519..f01e2bac 100644
--- a/src/store/actions/user.ts
+++ b/src/store/actions/user.ts
@@ -6,6 +6,7 @@ import {
handlePresignedURL,
handleVideoUpload,
loadProfileInfo,
+ postMoment,
postMomentTags,
removeBadgesService,
sendSuggestedPeopleLinked,
@@ -285,6 +286,89 @@ export const suggestedPeopleAnimatedTutorialFinished =
}
};
+export const handleImageMomentUpload =
+ (
+ imageUri: string,
+ caption: string,
+ momentCategory: string,
+ userId: 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: 1, // assume upload time for an image is same as a 1s video
+ };
+ // set progress bar as loading
+ dispatch({
+ type: setMomentUploadProgressBar.type,
+ payload: {momentUploadProgressBar},
+ });
+ // upload image moment
+ const momentPostResponse = await postMoment(
+ imageUri,
+ caption,
+ momentCategory,
+ userId,
+ );
+ if (!momentPostResponse) {
+ handleError('Moment post failed');
+ return;
+ }
+ const profileCompletionStage =
+ momentPostResponse.profile_completion_stage;
+ const momentId = momentPostResponse.moment_id;
+ if (!momentId) {
+ handleError('Unable to parse moment id from moment post response');
+ return;
+ }
+ // upload moment tags
+ const momentTagResponse = await postMomentTags(momentId, formattedTags);
+ if (!momentTagResponse) {
+ handleError('Moment tag post failed');
+ return;
+ }
+ if (profileCompletionStage) {
+ dispatch(updateProfileCompletionStage(profileCompletionStage));
+ } else {
+ console.error(
+ 'failed to parse profile complete stage from moment post response',
+ );
+ }
+ // mark progress bar state as done
+ dispatch({
+ type: setMomentUploadProgressBar.type,
+ payload: {
+ momentUploadProgressBar: {
+ ...momentUploadProgressBar,
+ status: MomentUploadStatusType.Done,
+ },
+ },
+ });
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
/**
* state is now UploadingToS3:
* - get presigned url (backend creates the moment object)
@@ -296,6 +380,7 @@ export const handleVideoMomentUpload =
(
videoUri: string,
videoLength: number,
+ caption: string,
momentCategory: string,
formattedTags: {
x: number;
@@ -329,7 +414,10 @@ export const handleVideoMomentUpload =
payload: {momentUploadProgressBar},
});
// get a presigned url for the video
- const presignedURLResponse = await handlePresignedURL(momentCategory);
+ const presignedURLResponse = await handlePresignedURL(
+ caption,
+ momentCategory,
+ );
if (!presignedURLResponse) {
handleError('Presigned URL failed');
return;
diff --git a/src/utils/camera.ts b/src/utils/camera.ts
index f21ef133..ec2615de 100644
--- a/src/utils/camera.ts
+++ b/src/utils/camera.ts
@@ -10,7 +10,11 @@ import {
} from 'react-native-camera';
import {ProcessingManager} from 'react-native-video-processing';
import ImagePicker, {ImageOrVideo} from 'react-native-image-crop-picker';
-import {ERROR_UPLOAD} from '../constants/strings';
+import {
+ ERROR_UPLOAD,
+ ERROR_UPLOAD_EXCEED_MAX_VIDEO_DURATION,
+} from '../constants/strings';
+import {MAX_VIDEO_RECORDING_DURATION} from '../constants';
/*
* Captures a photo and pauses to show the preview of the picture taken
@@ -39,7 +43,7 @@ export const takeVideo = (
if (cameraRef !== null) {
const options: RecordOptions = {
orientation: 'portrait',
- maxDuration: 60,
+ maxDuration: MAX_VIDEO_RECORDING_DURATION,
quality: '1080p',
};
cameraRef.current?.recordAsync(options).then((vid) => {
@@ -73,6 +77,14 @@ export const navigateToMediaPicker = (
compressVideoPreset: 'Passthrough',
})
.then((media) => {
+ if (
+ 'duration' in media &&
+ media.duration !== null &&
+ media.duration > MAX_VIDEO_RECORDING_DURATION * 1000
+ ) {
+ Alert.alert(ERROR_UPLOAD_EXCEED_MAX_VIDEO_DURATION);
+ return;
+ }
callback(media);
})
.catch((err) => {