diff options
| author | Ivan Chen <ivan@tagg.id> | 2021-06-25 20:58:56 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-06-25 20:58:56 -0400 |
| commit | 5480267b285812c094246bb941c6deaf83f53ff5 (patch) | |
| tree | 17f2e23576c000bcc90d840d14b8abc3bb9bec24 /src/components/moments | |
| parent | 981051448fee6197544383e535fea7a72827d41d (diff) | |
| parent | dcf45600b6e2be7820ed2d8c0f44603624f1e719 (diff) | |
Merge pull request #475 from IvanIFChen/tma948-video-playback
[TMA-948] Viewing Videos
Diffstat (limited to 'src/components/moments')
| -rw-r--r-- | src/components/moments/IndividualMomentTitleBar.tsx | 58 | ||||
| -rw-r--r-- | src/components/moments/Moment.tsx | 22 | ||||
| -rw-r--r-- | src/components/moments/MomentCommentPreview.tsx | 8 | ||||
| -rw-r--r-- | src/components/moments/MomentPost.tsx | 396 | ||||
| -rw-r--r-- | src/components/moments/MomentPostHeader.tsx | 116 | ||||
| -rw-r--r-- | src/components/moments/index.ts | 2 | ||||
| -rw-r--r-- | src/components/moments/legacy/MomentPostContent.tsx (renamed from src/components/moments/MomentPostContent.tsx) | 26 |
7 files changed, 415 insertions, 213 deletions
diff --git a/src/components/moments/IndividualMomentTitleBar.tsx b/src/components/moments/IndividualMomentTitleBar.tsx index 4ae9471f..c6bf1423 100644 --- a/src/components/moments/IndividualMomentTitleBar.tsx +++ b/src/components/moments/IndividualMomentTitleBar.tsx @@ -1,45 +1,32 @@ import React from 'react'; -import { - StyleSheet, - Text, - TouchableOpacity, - View, - ViewProps, -} from 'react-native'; -import CloseIcon from '../../assets/ionicons/close-outline.svg'; -import {normalize} from '../../utils'; +import {StyleSheet, Text, View, ViewProps} from 'react-native'; +import {normalize, SCREEN_WIDTH} from '../../utils'; interface IndividualMomentTitleBarProps extends ViewProps { title: string; - close: () => void; } const IndividualMomentTitleBar: React.FC<IndividualMomentTitleBarProps> = ({ title, - close, - style, }) => { return ( - <View style={[styles.container, style]}> - <TouchableOpacity style={styles.closeButton} onPress={close}> - <CloseIcon height={'100%'} width={'100%'} color={'white'} /> - </TouchableOpacity> - <View style={styles.headerContainer}> - <Text style={styles.header}>{title}</Text> + <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({ - container: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'flex-start', - }, - headerContainer: { - width: '80%', - }, - header: { + title: { textAlign: 'center', color: 'white', fontSize: normalize(18), @@ -47,10 +34,19 @@ const styles = StyleSheet.create({ lineHeight: normalize(21.48), letterSpacing: normalize(1.3), }, - closeButton: { - height: '50%', - aspectRatio: 1, - left: '8%', + 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%', }, }); diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx index eab4b7e3..a43a2830 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -88,15 +88,11 @@ const Moment: React.FC<MomentProps> = ({ 'Screenshots', 'UserLibrary', ], - width: 580, - height: 580, - cropping: true, - cropperToolbarTitle: 'Upload a moment', - mediaType: 'photo', + mediaType: 'any', }) .then((picture) => { if (picture.path && picture.filename) { - navigation.navigate('CaptionScreen', { + navigation.navigate('ZoomInCropper', { screenType, title, media: { @@ -161,12 +157,14 @@ const Moment: React.FC<MomentProps> = ({ onPress: () => ImagePicker.openCamera({ mediaType: 'video', - }).then((vid) => { - console.log(vid); - if (vid.path) { - navigateToCaptionScreenForVideo(vid.path); - } - }), + }) + .then((vid) => { + console.log(vid); + if (vid.path) { + navigateToCaptionScreenForVideo(vid.path); + } + }) + .catch((err) => console.error(err)), }, ]) } diff --git a/src/components/moments/MomentCommentPreview.tsx b/src/components/moments/MomentCommentPreview.tsx index e53ed258..232568f1 100644 --- a/src/components/moments/MomentCommentPreview.tsx +++ b/src/components/moments/MomentCommentPreview.tsx @@ -23,7 +23,9 @@ const MomentCommentPreview: React.FC<MomentCommentPreviewProps> = ({ const navigation = useNavigation(); const state = useStore().getState(); const commentCountText = - commentsCount === 0 ? 'No Comments' : commentsCount + ' comments'; + !commentsCount || commentsCount === 0 + ? 'No Comments' + : commentsCount + ' comments'; return ( <TouchableOpacity @@ -35,7 +37,7 @@ const MomentCommentPreview: React.FC<MomentCommentPreviewProps> = ({ }) }> <Text style={styles.whiteBold}>{commentCountText}</Text> - {commentPreview !== null && ( + {commentPreview && ( <View style={styles.previewContainer}> <Image source={{ @@ -50,7 +52,7 @@ const MomentCommentPreview: React.FC<MomentCommentPreviewProps> = ({ {renderTextWithMentions({ value: commentPreview.comment, styles: styles.normalFont, - partTypes: mentionPartTypes('white'), + partTypes: mentionPartTypes('white', 'comment'), onPress: (user: UserType) => navigateToProfile( state, diff --git a/src/components/moments/MomentPost.tsx b/src/components/moments/MomentPost.tsx index d87028e3..cb3a138b 100644 --- a/src/components/moments/MomentPost.tsx +++ b/src/components/moments/MomentPost.tsx @@ -1,38 +1,85 @@ -import React, {useEffect, useState} from 'react'; -import {StyleSheet} from 'react-native'; -import {useSelector} from 'react-redux'; -import {MomentPostContent, MomentPostHeader} from '.'; +import {useNavigation} from '@react-navigation/native'; +import React, {useContext, useEffect, useRef, useState} from 'react'; +import { + Image, + KeyboardAvoidingView, + Platform, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from 'react-native'; +import Animated, {EasingNode} from 'react-native-reanimated'; +import Video from 'react-native-video'; +import {useDispatch, useSelector, useStore} from 'react-redux'; +import {headerBarOptions} from '../../routes'; +import {MomentContext} from '../../screens/profile/IndividualMoment'; import {deleteMomentTag, loadMomentTags} from '../../services'; +import {loadUserMoments} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; -import {MomentPostType, MomentTagType, ScreenType} from '../../types'; -import {normalize, SCREEN_HEIGHT} from '../../utils'; - +import {MomentPostType, MomentTagType, ScreenType, UserType} from '../../types'; +import { + getTimePosted, + HeaderHeight, + 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 {MomentMoreInfoDrawer, TaggAvatar} from '../profile'; +import IndividualMomentTitleBar from './IndividualMomentTitleBar'; interface MomentPostProps { moment: MomentPostType; userXId: string | undefined; screenType: ScreenType; - index: number; } const MomentPost: React.FC<MomentPostProps> = ({ moment, userXId, screenType, - index, }) => { + const navigation = useNavigation(); + const dispatch = useDispatch(); const {userId: loggedInUserId, username: loggedInUsername} = useSelector( (state: RootState) => state.user.user, ); - - const { - user: {username}, - } = useSelector((state: RootState) => + const {user} = useSelector((state: RootState) => userXId ? state.userX[screenType][userXId] : state.user, ); + const state: RootState = useStore().getState(); + const isOwnProfile = user.username === loggedInUsername; + const [tags, setTags] = useState<MomentTagType[]>([]); + const [visible, setVisible] = useState(false); + const [drawerVisible, setDrawerVisible] = useState(false); + const [hideText, setHideText] = useState(false); + + 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>(''); - const isOwnProfile = username === loggedInUsername; + const imageRef = useRef(null); + const videoRef = useRef<Video>(null); + const {keyboardVisible, currentVisibleMomentId} = useContext(MomentContext); + const isVideo = !( + moment.moment_url.endsWith('jpg') || + moment.moment_url.endsWith('JPG') || + moment.moment_url.endsWith('PNG') || + moment.moment_url.endsWith('png') + ); /* * Load tags on initial render to pass tags data to moment header and content @@ -41,7 +88,7 @@ const MomentPost: React.FC<MomentPostProps> = ({ loadMomentTags(moment.moment_id).then((response) => { setTags(response ? response : []); }); - }, []); + }, [moment]); /* * Check if loggedInUser has been tagged in the picture and set the id @@ -71,34 +118,311 @@ const MomentPost: React.FC<MomentPostProps> = ({ } }; + useEffect( + () => + navigation.setOptions({ + ...headerBarOptions('white', ''), + headerTitle: () => ( + <IndividualMomentTitleBar title={moment.moment_category} /> + ), + }), + [moment.moment_id], + ); + + /* + * Determines if an image is 9:16 to set aspect ratio of current image and + * determine if image must be displayed in full screen or not + */ + useEffect(() => { + if (!isVideo) { + Image.getSize( + moment.moment_url, + (w, h) => { + setAspectRatio(w / h); + }, + (err) => console.log(err), + ); + } + }, []); + + /* + * To animate tags display + */ + useEffect(() => { + const fade = async () => { + Animated.timing(fadeValue, { + toValue: 1, + duration: 250, + easing: EasingNode.linear, + }).start(); + }; + fade(); + }, [fadeValue]); + + useEffect(() => { + if (!keyboardVisible && hideText) { + setHideText(false); + } + }, [keyboardVisible, hideText]); + + useEffect(() => { + if (moment.moment_id !== currentVisibleMomentId) { + videoRef.current?.seek(0); + } + }, [currentVisibleMomentId]); + + const MomentPosterPreview = () => ( + <View style={styles.momentPosterContainer}> + <TouchableOpacity + onPress={() => + navigateToProfile(state, dispatch, navigation, screenType, user) + } + style={styles.header}> + <TaggAvatar + style={styles.avatar} + userXId={userXId} + screenType={screenType} + editable={false} + /> + <Text style={styles.headerText}>{user.username}</Text> + </TouchableOpacity> + </View> + ); + return ( <> - <MomentPostHeader - style={styles.postHeader} - userXId={userXId} - screenType={screenType} - username={isOwnProfile ? loggedInUsername : username} - momentTagId={momentTagId} - removeTag={removeTag} - moment={moment} - tags={tags} - /> - <MomentPostContent - style={styles.postContent} - moment={moment} - screenType={screenType} - momentTags={tags} - index={index} - /> + <StatusBar barStyle={'light-content'} /> + <View style={styles.mainContainer}> + <View style={styles.imageContainer}> + {isVideo ? ( + <View + ref={imageRef} + style={[ + styles.media, + { + height: SCREEN_WIDTH / aspectRatio, + }, + ]}> + <Video + ref={videoRef} + source={{ + uri: moment.moment_url, + }} + volume={1} + style={[ + styles.media, + { + height: SCREEN_WIDTH / aspectRatio, + }, + ]} + repeat={true} + resizeMode={'contain'} + onLoad={(response) => { + const {width, height} = response.naturalSize; + setAspectRatio(width / height); + }} + paused={moment.moment_id !== currentVisibleMomentId} + /> + </View> + ) : ( + <Image + source={{uri: moment.moment_url}} + style={styles.media} + resizeMode={'contain'} + ref={imageRef} + /> + )} + </View> + {visible && ( + <Animated.View style={[styles.tagsContainer, {opacity: fadeValue}]}> + <MomentTags + editing={false} + tags={tags} + setTags={() => null} + imageRef={imageRef} + /> + </Animated.View> + )} + <TouchableWithoutFeedback + onPress={() => { + setVisible(!visible); + setFadeValue(new Animated.Value(0)); + }}> + <View style={styles.contentContainer}> + <View style={styles.topContainer}> + <MomentMoreInfoDrawer + isOpen={drawerVisible} + setIsOpen={setDrawerVisible} + isOwnProfile={isOwnProfile} + momentTagId={momentTagId} + removeTag={removeTag} + dismissScreenAndUpdate={() => { + dispatch(loadUserMoments(loggedInUserId)); + navigation.goBack(); + }} + screenType={screenType} + moment={moment} + tags={tags} + /> + </View> + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + keyboardVerticalOffset={-20}> + <View style={styles.bottomContainer}> + {tags.length > 0 && ( + <Image + source={require('../../assets/icons/tag_indicate.png')} + style={styles.tagIcon} + /> + )} + <View style={styles.commentsCountContainer}> + <CommentsCount + momentId={moment.moment_id} + count={commentCount} + screenType={screenType} + /> + </View> + <MomentPosterPreview /> + {!hideText && ( + <> + {moment.caption !== '' && + renderTextWithMentions({ + value: moment.caption, + styles: styles.captionText, + partTypes: mentionPartTypes('white', 'caption'), + onPress: (userLocal: UserType) => + navigateToProfile( + state, + dispatch, + navigation, + screenType, + userLocal, + ), + })} + </> + )} + <View> + <AddComment + placeholderText={'Add a comment here!'} + momentId={moment.moment_id} + callback={() => { + setCommentCount(commentCount + 1); + }} + onFocus={() => { + setHideText(true); + }} + isKeyboardAvoiding={false} + theme={'dark'} + /> + <Text style={styles.text}> + {getTimePosted(moment.date_created)} + </Text> + </View> + </View> + </KeyboardAvoidingView> + </View> + </TouchableWithoutFeedback> + </View> </> ); }; const styles = StyleSheet.create({ - postHeader: {}, - postContent: { - minHeight: SCREEN_HEIGHT * 0.8, - paddingBottom: normalize(20), + media: { + zIndex: 0, + flex: 1, + }, + imageContainer: { + height: SCREEN_HEIGHT, + width: SCREEN_WIDTH, + flexDirection: 'column', + justifyContent: 'center', + overflow: 'hidden', + }, + text: { + marginHorizontal: '5%', + color: 'white', + fontWeight: '500', + textAlign: 'right', + marginTop: 5, + }, + captionText: { + position: 'relative', + marginHorizontal: '5%', + color: '#ffffff', + fontWeight: '500', + fontSize: normalize(13), + lineHeight: normalize(15.51), + letterSpacing: normalize(0.6), + marginBottom: normalize(5), + width: SCREEN_WIDTH * 0.79, + }, + tagIcon: { + width: normalize(30), + height: normalize(30), + bottom: normalize(20), + left: '5%', + }, + avatar: { + width: 48, + aspectRatio: 1, + borderRadius: 100, + marginLeft: '3%', + }, + headerText: { + fontSize: 15, + fontWeight: 'bold', + color: 'white', + paddingHorizontal: '3%', + }, + header: { + alignItems: 'center', + flexDirection: 'row', + marginBottom: normalize(15), + alignSelf: 'flex-start', + }, + momentPosterContainer: { + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + commentsCountContainer: { + position: 'absolute', + zIndex: 3, + right: '2%', + bottom: SCREEN_HEIGHT * 0.12, + }, + bottomContainer: { + flexDirection: 'column', + justifyContent: 'flex-end', + }, + topContainer: { + paddingTop: isIPhoneX() ? HeaderHeight : '6%', + alignSelf: 'flex-end', + paddingRight: '8%', + }, + contentContainer: { + position: 'absolute', + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + flexDirection: 'column', + justifyContent: 'space-between', + paddingBottom: '24%', + }, + tagsContainer: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + marginBottom: '3%', + }, + mainContainer: { + backgroundColor: 'black', + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + flexDirection: 'column', + justifyContent: 'center', }, }); diff --git a/src/components/moments/MomentPostHeader.tsx b/src/components/moments/MomentPostHeader.tsx deleted file mode 100644 index 5f26951a..00000000 --- a/src/components/moments/MomentPostHeader.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import {useNavigation} from '@react-navigation/native'; -import React, {useState} from 'react'; -import { - StyleSheet, - Text, - TouchableOpacity, - View, - ViewProps, -} from 'react-native'; -import {useDispatch, useSelector, useStore} from 'react-redux'; -import {loadUserMoments} from '../../store/actions'; -import {RootState} from '../../store/rootReducer'; -import {MomentTagType, MomentType, ScreenType} from '../../types'; -import {fetchUserX, userXInStore} from '../../utils'; -import {MomentMoreInfoDrawer} from '../profile'; -import TaggAvatar from '../profile/TaggAvatar'; - -interface MomentPostHeaderProps extends ViewProps { - userXId?: string; - screenType: ScreenType; - username: string; - momentTagId: string; - removeTag: () => Promise<void>; - moment: MomentType; - tags: MomentTagType[]; -} - -const MomentPostHeader: React.FC<MomentPostHeaderProps> = ({ - userXId, - screenType, - username, - style, - momentTagId, - removeTag, - moment, - tags, -}) => { - const [drawerVisible, setDrawerVisible] = useState(false); - const dispatch = useDispatch(); - const navigation = useNavigation(); - const {userId: loggedInUserId, username: loggedInUserName} = useSelector( - (state: RootState) => state.user.user, - ); - const state: RootState = useStore().getState(); - const isOwnProfile = loggedInUserName === username; - const navigateToProfile = async () => { - if (userXId && !userXInStore(state, screenType, userXId)) { - await fetchUserX( - dispatch, - {userId: userXId, username: username}, - screenType, - ); - } - navigation.navigate('Profile', { - userXId: isOwnProfile ? undefined : userXId, - screenType, - }); - }; - - return ( - <View style={[styles.container, style]}> - <TouchableOpacity onPress={navigateToProfile} style={styles.header}> - <TaggAvatar - style={styles.avatar} - userXId={userXId} - screenType={screenType} - editable={false} - /> - <Text style={styles.headerText}>{username}</Text> - </TouchableOpacity> - <MomentMoreInfoDrawer - isOpen={drawerVisible} - setIsOpen={setDrawerVisible} - isOwnProfile={isOwnProfile} - momentTagId={momentTagId} - removeTag={removeTag} - dismissScreenAndUpdate={() => { - dispatch(loadUserMoments(loggedInUserId)); - navigation.goBack(); - }} - screenType={screenType} - moment={moment} - tags={tags} - /> - </View> - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'space-around', - flexDirection: 'row', - alignItems: 'center', - marginVertical: '2%', - }, - header: { - alignItems: 'center', - flexDirection: 'row', - flex: 1, - }, - avatar: { - flex: 0.2, - aspectRatio: 1, - borderRadius: 999999, - marginLeft: '3%', - }, - headerText: { - fontSize: 15, - fontWeight: 'bold', - color: 'white', - paddingHorizontal: '3%', - flex: 1, - }, -}); -export default MomentPostHeader; diff --git a/src/components/moments/index.ts b/src/components/moments/index.ts index c1419cfd..cac2da2e 100644 --- a/src/components/moments/index.ts +++ b/src/components/moments/index.ts @@ -1,7 +1,5 @@ export {default as IndividualMomentTitleBar} from './IndividualMomentTitleBar'; export {default as CaptionScreenHeader} from './CaptionScreenHeader'; -export {default as MomentPostHeader} from './MomentPostHeader'; -export {default as MomentPostContent} from './MomentPostContent'; export {default as Moment} from './Moment'; export {default as TagFriendsFooter} from './TagFriendsFoooter'; export {default as MomentPost} from './MomentPost'; diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/legacy/MomentPostContent.tsx index 4d2554d8..0e6e5eed 100644 --- a/src/components/moments/MomentPostContent.tsx +++ b/src/components/moments/legacy/MomentPostContent.tsx @@ -5,39 +5,40 @@ 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'; +import {MomentContext} from '../../../screens/profile/IndividualMoment'; +import {RootState} from '../../../store/rootReducer'; import { MomentCommentPreviewType, MomentPostType, MomentTagType, ScreenType, UserType, -} from '../../types'; +} from '../../../types'; import { getLoggedInUserAsProfilePreview, getTimePosted, navigateToProfile, normalize, SCREEN_WIDTH, -} from '../../utils'; -import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments'; -import {AddComment} from '../comments'; -import {MomentTags} from '../common'; -import MomentCommentPreview from './MomentCommentPreview'; +} from '../../../utils'; +import { + mentionPartTypes, + renderTextWithMentions, +} from '../../../utils/comments'; +import {AddComment} from '../../comments'; +import {MomentTags} from '../../common'; +import MomentCommentPreview from '../MomentCommentPreview'; interface MomentPostContentProps extends ViewProps { screenType: ScreenType; moment: MomentPostType; momentTags: MomentTagType[]; - index: number; } const MomentPostContent: React.FC<MomentPostContentProps> = ({ screenType, moment, style, momentTags, - index, }) => { const [tags, setTags] = useState<MomentTagType[]>(momentTags); const state: RootState = useStore().getState(); @@ -53,7 +54,7 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ ); const [commentPreview, setCommentPreview] = useState<MomentCommentPreviewType | null>(moment.comment_preview); - const {keyboardVisible, scrollTo} = useContext(MomentContext); + const {keyboardVisible} = useContext(MomentContext); const [hideText, setHideText] = useState(false); const isVideo = !( moment.moment_url.endsWith('jpg') || @@ -140,7 +141,7 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ renderTextWithMentions({ value: moment.caption, styles: styles.captionText, - partTypes: mentionPartTypes('white'), + partTypes: mentionPartTypes('white', 'caption'), onPress: (user: UserType) => navigateToProfile( state, @@ -170,7 +171,6 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ }} onFocus={() => { setHideText(true); - scrollTo(index); }} isKeyboardAvoiding={false} theme={'dark'} |
