diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/camera/GalleryIcon.tsx | 4 | ||||
-rw-r--r-- | src/components/common/GradientProgressBar.tsx | 48 | ||||
-rw-r--r-- | src/components/common/index.ts | 1 | ||||
-rw-r--r-- | src/components/moments/MomentPost.tsx | 28 | ||||
-rw-r--r-- | src/components/moments/MomentUploadProgressBar.tsx | 221 | ||||
-rw-r--r-- | src/components/moments/TrimmerPlayer.tsx | 7 | ||||
-rw-r--r-- | src/components/moments/index.ts | 1 | ||||
-rw-r--r-- | src/components/profile/Content.tsx | 2 | ||||
-rw-r--r-- | src/components/profile/ProfileBadges.tsx | 8 |
9 files changed, 310 insertions, 10 deletions
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/MomentPost.tsx b/src/components/moments/MomentPost.tsx index 2e5807f4..f6917784 100644 --- a/src/components/moments/MomentPost.tsx +++ b/src/components/moments/MomentPost.tsx @@ -46,12 +46,14 @@ interface MomentPostProps { moment: MomentPostType; userXId: string | undefined; screenType: ScreenType; + updateMomentViewCount: () => void; } const MomentPost: React.FC<MomentPostProps> = ({ moment, userXId, screenType, + updateMomentViewCount, }) => { const navigation = useNavigation(); const dispatch = useDispatch(); @@ -197,11 +199,18 @@ const MomentPost: React.FC<MomentPostProps> = ({ screenType={screenType} editable={false} /> - <Text style={styles.headerText}>{user.username}</Text> + <View style={styles.profilePreviewContainer}> + <Text style={styles.headerText}>{user.username}</Text> + <Text style={styles.viewCount}> + {moment.view_count <= 9999 + ? `${moment.view_count} Views` + : `${(moment.view_count / 1000).toFixed(1)}K Views`} + </Text> + </View> </TouchableOpacity> </View> ), - [user.username], + [user.username, moment.view_count], ); const momentMedia = isVideo ? ( @@ -227,6 +236,8 @@ const MomentPost: React.FC<MomentPostProps> = ({ setVideoProgress(localProgress); } }} + onEnd={updateMomentViewCount} + /> </View> ) : ( @@ -432,7 +443,17 @@ const styles = StyleSheet.create({ fontSize: 15, fontWeight: 'bold', color: 'white', - paddingHorizontal: '3%', + }, + viewCount: { + height: normalize(12), + left: 0, + top: '8%', + fontSize: 11, + fontWeight: '600', + lineHeight: 13, + letterSpacing: 0.08, + textAlign: 'left', + color: '#fff', }, header: { alignItems: 'center', @@ -500,6 +521,7 @@ const styles = StyleSheet.create({ position: 'absolute', top: isIPhoneX() ? 75 : 70, }, + profilePreviewContainer: {paddingHorizontal: '3%'}, }); export default MomentPost; 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..8d1cd156 100644 --- a/src/components/moments/TrimmerPlayer.tsx +++ b/src/components/moments/TrimmerPlayer.tsx @@ -73,7 +73,12 @@ const TrimmerPlayer: React.FC<TrimmerPlayerProps> = ({ repeat={true} onLoad={(payload) => { setEnd(payload.duration); - handleLoad(payload.naturalSize); + const {width, height} = payload.naturalSize; + if (payload.naturalSize.orientation === 'portrait') { + handleLoad(height, width, payload.duration); + } else { + handleLoad(width, height, 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 && ( |