diff options
author | Ivan Chen <ivan@tagg.id> | 2021-07-01 17:32:12 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-01 17:32:12 -0400 |
commit | fa9c527f85d23a38b45c7efc41ec4590597fa7a1 (patch) | |
tree | 164852b257ab961fb8d4a067189b85e0aadcc180 /src/screens | |
parent | 66c974161b59f1e3570e2a4a42334fabc16c2129 (diff) | |
parent | ad2f052c6d2cd1b50cc01200597b5b79cb33082d (diff) |
Merge pull request #472 from TaggiD-Inc/poc-video
[POC] PoC Video
Diffstat (limited to 'src/screens')
-rw-r--r-- | src/screens/chat/ChatListScreen.tsx | 2 | ||||
-rw-r--r-- | src/screens/moments/CameraScreen.tsx | 55 | ||||
-rw-r--r-- | src/screens/moments/TagFriendsScreen.tsx | 195 | ||||
-rw-r--r-- | src/screens/profile/CaptionScreen.tsx | 113 | ||||
-rw-r--r-- | src/screens/profile/IndividualMoment.tsx | 55 | ||||
-rw-r--r-- | src/screens/suggestedPeople/SuggestedPeopleScreen.tsx | 25 |
6 files changed, 257 insertions, 188 deletions
diff --git a/src/screens/chat/ChatListScreen.tsx b/src/screens/chat/ChatListScreen.tsx index 1df5c2da..0f5d8073 100644 --- a/src/screens/chat/ChatListScreen.tsx +++ b/src/screens/chat/ChatListScreen.tsx @@ -6,10 +6,10 @@ import {useStore} from 'react-redux'; import {ChannelList, Chat} from 'stream-chat-react-native'; import {ChatContext} from '../../App'; import {TabsGradient} from '../../components'; +import EmptyContentView from '../../components/common/EmptyContentView'; import {ChannelPreview, MessagesHeader} from '../../components/messages'; import {MainStackParams} from '../../routes'; import {RootState} from '../../store/rootReducer'; -import EmptyContentView from '../../components/common/EmptyContentView'; import { LocalAttachmentType, LocalChannelType, diff --git a/src/screens/moments/CameraScreen.tsx b/src/screens/moments/CameraScreen.tsx index c6ed1116..37b37264 100644 --- a/src/screens/moments/CameraScreen.tsx +++ b/src/screens/moments/CameraScreen.tsx @@ -1,6 +1,7 @@ import CameraRoll from '@react-native-community/cameraroll'; import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; -import {RouteProp, useFocusEffect} from '@react-navigation/core'; +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'; @@ -15,7 +16,7 @@ import { } from '../../components'; import {MainStackParams} from '../../routes'; import {HeaderHeight, normalize, SCREEN_WIDTH} from '../../utils'; -import {takePicture} from '../../utils/camera'; +import {showGIFFailureAlert, takePicture} from '../../utils/camera'; type CameraScreenRouteProps = RouteProp<MainStackParams, 'CameraScreen'>; export type CameraScreenNavigationProps = StackNavigationProp< @@ -36,9 +37,6 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { const [mostRecentPhoto, setMostRecentPhoto] = useState<string>(''); const [showSaveButton, setShowSaveButton] = useState<boolean>(false); - /* - * Removes bottom navigation bar on current screen and add it back when navigating away - */ useFocusEffect( useCallback(() => { navigation.dangerouslyGetParent()?.setOptions({ @@ -68,16 +66,24 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { ); }, [capturedImage]); - /* - * Appears once a picture has been captured to navigate to the caption screen - */ - const handleNext = () => { + const navigateToCropper = (uri: string) => { + navigation.navigate('ZoomInCropper', { + screenType, + title, + media: { + uri, + isVideo: false, + }, + }); + }; + + const navigateToCaptionScreen = () => { navigation.navigate('CaptionScreen', { screenType, title, - image: { - filename: capturedImage, - path: capturedImage, + media: { + uri: capturedImage, + isVideo: false, }, }); }; @@ -116,7 +122,10 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { )} <TouchableOpacity onPress={() => - takePicture(cameraRef, setShowSaveButton, setCapturedImage) + takePicture(cameraRef, (pic) => { + setShowSaveButton(true); + setCapturedImage(pic.uri); + }) } style={styles.captureButtonContainer}> <View style={styles.captureButton} /> @@ -124,7 +133,7 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { <View style={styles.bottomRightContainer}> {capturedImage ? ( <TaggSquareButton - onPress={handleNext} + onPress={navigateToCaptionScreen} title={'Next'} buttonStyle={'large'} buttonColor={'blue'} @@ -135,8 +144,17 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { ) : ( <GalleryIcon mostRecentPhotoUri={mostRecentPhoto} - screenType={screenType} - title={title} + callback={(pic) => { + const filename = pic.filename; + if ( + filename && + (filename.endsWith('gif') || filename.endsWith('GIF')) + ) { + showGIFFailureAlert(() => navigateToCropper(pic.path)); + } else { + navigateToCropper(pic.path); + } + }} /> )} </View> @@ -155,11 +173,6 @@ const styles = StyleSheet.create({ flexDirection: 'column', backgroundColor: 'black', }, - preview: { - flex: 1, - justifyContent: 'flex-end', - alignItems: 'center', - }, captureButtonContainer: { alignSelf: 'center', backgroundColor: 'transparent', diff --git a/src/screens/moments/TagFriendsScreen.tsx b/src/screens/moments/TagFriendsScreen.tsx index 15926b5a..201caf49 100644 --- a/src/screens/moments/TagFriendsScreen.tsx +++ b/src/screens/moments/TagFriendsScreen.tsx @@ -1,16 +1,9 @@ import {RouteProp} from '@react-navigation/core'; import {useNavigation} from '@react-navigation/native'; import React, {useEffect, useRef, useState} from 'react'; -import { - Image, - Keyboard, - KeyboardAvoidingView, - Platform, - StyleSheet, - TouchableWithoutFeedback, - View, -} from 'react-native'; +import {Image, StyleSheet, TouchableWithoutFeedback, View} from 'react-native'; import {Button} from 'react-native-elements'; +import Video from 'react-native-video'; import {MainStackParams} from 'src/routes'; import { CaptionScreenHeader, @@ -30,7 +23,7 @@ interface TagFriendsScreenProps { route: TagFriendsScreenRouteProps; } const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { - const {imagePath, selectedTags} = route.params; + const {media, selectedTags} = route.params; const navigation = useNavigation(); const imageRef = useRef(null); const [tags, setTags] = useState<MomentTagType[]>([]); @@ -54,26 +47,30 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { }); }; + const setMediaDimensions = (width: number, height: number) => { + const imageAspectRatio = width / height; + + // aspectRatio: >= 1 [Landscape] [1:1] + if (imageAspectRatio >= 1) { + setImageWidth(SCREEN_WIDTH); + setImageHeight(SCREEN_WIDTH / imageAspectRatio); + } + // aspectRatio: < 1 [Portrait] + if (imageAspectRatio < 1) { + setImageHeight(SCREEN_WIDTH); + setImageWidth(SCREEN_WIDTH * imageAspectRatio); + } + }; + /* - * Calculating image width and height with respect to it's enclosing view's dimensions + * Calculating image width and height with respect to it's enclosing view's dimensions. Only works for images. */ useEffect(() => { - if (imageRef && imageRef.current) { + if (imageRef && imageRef.current && !media.isVideo) { Image.getSize( - imagePath, + media.uri, (w, h) => { - const imageAspectRatio = w / h; - - // aspectRatio: >= 1 [Landscape] [1:1] - if (imageAspectRatio >= 1) { - setImageWidth(SCREEN_WIDTH); - setImageHeight(SCREEN_WIDTH / imageAspectRatio); - } - // aspectRatio: < 1 [Portrait] - else if (imageAspectRatio < 1) { - setImageHeight(SCREEN_WIDTH); - setImageWidth(SCREEN_WIDTH * imageAspectRatio); - } + setMediaDimensions(w, h); }, (err) => console.log(err), ); @@ -82,65 +79,85 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { return ( <SearchBackground> - <TouchableWithoutFeedback onPress={Keyboard.dismiss}> - <KeyboardAvoidingView - behavior={Platform.OS === 'ios' ? 'padding' : 'height'} - style={styles.flex}> - <View style={styles.contentContainer}> - <View style={styles.buttonsContainer}> - <Button - title="Cancel" - buttonStyle={styles.button} - onPress={() => navigation.goBack()} - /> - <Button - title="Done" - titleStyle={styles.shareButtonTitle} - buttonStyle={styles.button} - onPress={handleDone} + <View style={styles.contentContainer}> + <View style={styles.buttonsContainer}> + <Button + title="Cancel" + buttonStyle={styles.button} + onPress={() => navigation.goBack()} + /> + <Button + title="Done" + titleStyle={styles.shareButtonTitle} + buttonStyle={styles.button} + onPress={handleDone} + /> + </View> + <CaptionScreenHeader + style={styles.header} + title={'Tap on photo to tag friends!'} + /> + <TouchableWithoutFeedback + onPress={() => + navigation.navigate('TagSelectionScreen', { + selectedTags: tags, + }) + }> + {media.isVideo ? ( + <View + style={{ + width: imageWidth, + height: imageHeight, + marginVertical: (SCREEN_WIDTH - imageHeight) / 2, + marginHorizontal: (SCREEN_WIDTH - imageWidth) / 2, + }} + ref={imageRef}> + <Video + style={{ + width: imageWidth, + height: imageHeight, + }} + source={{uri: media.uri}} + repeat={true} + onLoad={(response) => { + const {width, height, orientation} = response.naturalSize; + // portrait will flip width and height + if (orientation === 'portrait') { + setMediaDimensions(height, width); + } else { + setMediaDimensions(width, height); + } + }} /> </View> - <CaptionScreenHeader - style={styles.header} - title={'Tap on photo to tag friends!'} + ) : ( + <Image + ref={imageRef} + style={{ + width: imageWidth, + height: imageHeight, + marginVertical: (SCREEN_WIDTH - imageHeight) / 2, + marginHorizontal: (SCREEN_WIDTH - imageWidth) / 2, + }} + source={{uri: media.uri}} /> - <TouchableWithoutFeedback - onPress={() => - navigation.navigate('TagSelectionScreen', { - selectedTags: tags, - }) - }> - <Image - ref={imageRef} - style={[ - { - width: imageWidth, - height: imageHeight, - marginVertical: (SCREEN_WIDTH - imageHeight) / 2, - marginHorizontal: (SCREEN_WIDTH - imageWidth) / 2, - }, - styles.image, - ]} - source={{uri: imagePath}} - /> - </TouchableWithoutFeedback> - {tags.length !== 0 && ( - <MomentTags - tags={tags} - setTags={setTags} - editing={true} - imageRef={imageRef} - deleteFromList={(user) => - setTags(tags.filter((tag) => tag.user.id !== user.id)) - } - /> - )} - <View style={styles.footerContainer}> - <TagFriendsFooter tags={tags} setTags={setTags} /> - </View> - </View> - </KeyboardAvoidingView> - </TouchableWithoutFeedback> + )} + </TouchableWithoutFeedback> + {tags.length !== 0 && ( + <MomentTags + tags={tags} + setTags={setTags} + editing={true} + imageRef={imageRef} + deleteFromList={(user) => + setTags(tags.filter((tag) => tag.user.id !== user.id)) + } + /> + )} + <View style={styles.footerContainer}> + <TagFriendsFooter tags={tags} setTags={setTags} /> + </View> + </View> </SearchBackground> ); }; @@ -148,7 +165,6 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => { const styles = StyleSheet.create({ contentContainer: { paddingTop: StatusBarHeight, - justifyContent: 'flex-end', }, buttonsContainer: { flexDirection: 'row', @@ -166,19 +182,10 @@ const styles = StyleSheet.create({ header: { marginVertical: 20, }, - image: {zIndex: 0, justifyContent: 'center', alignSelf: 'center'}, - text: { - position: 'relative', - backgroundColor: 'white', - width: '100%', - paddingHorizontal: '2%', - paddingVertical: '1%', - height: 60, - }, - flex: { - flex: 1, + footerContainer: { + marginHorizontal: '5%', + marginTop: '3%', }, - footerContainer: {marginHorizontal: '5%', marginTop: '3%'}, }); export default TagFriendsScreen; diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index 75533a9b..05db8ed7 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -15,6 +15,7 @@ import { } from 'react-native'; import {MentionInputControlled} from '../../components'; import {Button, normalize} from 'react-native-elements'; +import Video from 'react-native-video'; import {useDispatch, useSelector} from 'react-redux'; import FrontArrow from '../../assets/icons/front-arrow.svg'; import {SearchBackground} from '../../components'; @@ -27,7 +28,13 @@ import { SUCCESS_PIC_UPLOAD, } from '../../constants/strings'; import {MainStackParams} from '../../routes'; -import {patchMoment, postMoment, postMomentTags} from '../../services'; +import { + handlePresignedURL, + handleVideoUpload, + patchMoment, + postMoment, + postMomentTags, +} from '../../services'; import { loadUserMoments, updateProfileCompletionStage, @@ -51,7 +58,7 @@ interface CaptionScreenProps { } const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { - const {title, image, screenType, selectedTags, moment} = route.params; + const {title, screenType, selectedTags, moment} = route.params; const { user: {userId}, } = useSelector((state: RootState) => state.user); @@ -62,6 +69,16 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { selectedTags ? selectedTags : [], ); const [taggedList, setTaggedList] = useState<string>(''); + const mediaUri = moment ? moment.moment_url : route.params.media!.uri; + // TODO: change this once moment refactor is done + const isMediaAVideo = moment + ? !( + moment.moment_url.endsWith('.jpg') || + moment.moment_url.endsWith('.JPG') || + moment.moment_url.endsWith('.png') || + moment.moment_url.endsWith('.PNG') + ) + : route.params.media?.isVideo ?? false; useEffect(() => { setTags(selectedTags ? selectedTags : []); @@ -120,36 +137,50 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { const handleShare = async () => { setLoading(true); - if (!image?.filename || !title) { - return; - } - const momentResponse = await postMoment( - image.filename, - image.path, - caption, - title, - userId, - ); - if (!momentResponse) { + if (moment || !title) { handleFailed(); return; } - const momentTagResponse = await postMomentTags( - momentResponse.moment_id, - formattedTags(), - ); - if (!momentTagResponse) { - handleFailed(); - return; + let profileCompletionStage; + let momentId; + // separate upload logic for image/video + if (isMediaAVideo) { + const presignedURLResponse = await handlePresignedURL(title); + 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); + } else { + handleFailed(); + } + } else { + const momentResponse = await postMoment(mediaUri, caption, title, userId); + if (!momentResponse) { + handleFailed(); + return; + } + profileCompletionStage = momentResponse.profile_completion_stage; + momentId = momentResponse.moment_id; + } + if (momentId) { + const momentTagResponse = await postMomentTags(momentId, formattedTags()); + if (!momentTagResponse) { + handleFailed(); + return; + } } dispatch(loadUserMoments(userId)); - dispatch( - updateProfileCompletionStage(momentResponse.profile_completion_stage), - ); + if (profileCompletionStage) { + dispatch(updateProfileCompletionStage(profileCompletionStage)); + } handleSuccess(); }; - const handleDone = async () => { + const handleDoneEditing = async () => { setLoading(true); if (moment?.moment_id) { const success = await patchMoment( @@ -186,19 +217,26 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { title={moment ? 'Done' : 'Share'} titleStyle={styles.shareButtonTitle} buttonStyle={styles.button} - onPress={moment ? handleDone : handleShare} + onPress={moment ? handleDoneEditing : handleShare} /> </View> <CaptionScreenHeader style={styles.header} - {...{title: moment ? moment.moment_category : title}} - /> - {/* this is the image we want to center our tags' initial location within */} - <Image - style={styles.image} - source={{uri: moment ? moment.moment_url : image?.path}} - resizeMode={'contain'} + {...{title: moment ? moment.moment_category : title ?? ''}} /> + {isMediaAVideo ? ( + <Video + style={styles.media} + source={{uri: mediaUri}} + repeat={true} + /> + ) : ( + <Image + style={styles.media} + source={{uri: mediaUri}} + resizeMode={'contain'} + /> + )} <MentionInputControlled containerStyle={styles.text} placeholder="Write something....." @@ -210,11 +248,10 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { <TouchableOpacity onPress={() => navigation.navigate('TagFriendsScreen', { - imagePath: moment - ? moment.moment_url - : image - ? image.path - : '', + media: { + uri: mediaUri, + isVideo: isMediaAVideo, + }, selectedTags: tags, }) } @@ -257,7 +294,7 @@ const styles = StyleSheet.create({ header: { marginVertical: 20, }, - image: { + media: { position: 'relative', width: SCREEN_WIDTH, aspectRatio: 1, diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx index ca31ad5b..7d231312 100644 --- a/src/screens/profile/IndividualMoment.tsx +++ b/src/screens/profile/IndividualMoment.tsx @@ -1,22 +1,17 @@ import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import React, {useEffect, useRef, useState} from 'react'; -import {FlatList, Keyboard} from 'react-native'; +import {FlatList, Keyboard, ViewToken} from 'react-native'; import {useSelector} from 'react-redux'; import {MomentPost, TabsGradient} from '../../components'; -import {AVATAR_DIM} from '../../constants'; import {MainStackParams} from '../../routes'; import {RootState} from '../../store/rootreducer'; import {MomentPostType} from '../../types'; -import {isIPhoneX} from '../../utils'; - -/** - * Individual moment view opened when user clicks on a moment tile - */ +import {SCREEN_HEIGHT} from '../../utils'; type MomentContextType = { keyboardVisible: boolean; - scrollTo: (index: number) => void; + currentVisibleMomentId: string | undefined; }; export const MomentContext = React.createContext({} as MomentContextType); @@ -48,6 +43,26 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({route}) => { ); const initialIndex = momentData.findIndex((m) => m.moment_id === moment_id); const [keyboardVisible, setKeyboardVisible] = useState(false); + const [currentVisibleMomentId, setCurrentVisibleMomentId] = useState< + string | undefined + >(); + const [viewableItems, setViewableItems] = useState<ViewToken[]>([]); + + // https://stackoverflow.com/a/57502343 + const viewabilityConfigCallback = useRef( + (info: {viewableItems: ViewToken[]}) => { + setViewableItems(info.viewableItems); + }, + ); + + useEffect(() => { + if (viewableItems.length > 0) { + const index = viewableItems[0].index; + if (index !== null && momentData.length > 0) { + setCurrentVisibleMomentId(momentData[index].moment_id); + } + } + }, [viewableItems]); useEffect(() => { const showKeyboard = () => setKeyboardVisible(true); @@ -60,20 +75,11 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({route}) => { }; }, []); - const scrollTo = (index: number) => { - // TODO: make this dynamic - const offset = isIPhoneX() ? -(AVATAR_DIM + 100) : -(AVATAR_DIM + 160); - scrollRef.current?.scrollToIndex({ - index: index, - viewOffset: offset, - }); - }; - return ( <MomentContext.Provider value={{ keyboardVisible, - scrollTo, + currentVisibleMomentId, }}> <FlatList ref={scrollRef} @@ -89,13 +95,12 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({route}) => { keyExtractor={(item, _) => item.moment_id} showsVerticalScrollIndicator={false} initialScrollIndex={initialIndex} - onScrollToIndexFailed={(info) => { - setTimeout(() => { - scrollRef.current?.scrollToIndex({ - index: info.index, - }); - }, 500); - }} + onViewableItemsChanged={viewabilityConfigCallback.current} + getItemLayout={(data, index) => ({ + length: SCREEN_HEIGHT, + offset: SCREEN_HEIGHT * index, + index, + })} pagingEnabled /> <TabsGradient /> diff --git a/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx b/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx index 39d98bcc..6156e950 100644 --- a/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx +++ b/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx @@ -44,6 +44,7 @@ const SuggestedPeopleScreen: React.FC = () => { const [hideStatusBar, setHideStatusBar] = useState(false); // boolean for showing/hiding loading indicator const [loading, setLoading] = useState(true); + const [viewableItems, setViewableItems] = useState<ViewToken[]>([]); // set loading to false once there are people to display useEffect(() => { @@ -59,6 +60,20 @@ const SuggestedPeopleScreen: React.FC = () => { const stausBarRef = useRef(hideStatusBar); + // https://stackoverflow.com/a/57502343 + const viewabilityConfigCallback = useRef( + (info: {viewableItems: ViewToken[]}) => { + setViewableItems(info.viewableItems); + }, + ); + + useEffect(() => { + if (viewableItems.length > 0) { + setHideStatusBar(viewableItems[0].index !== 0); + stausBarRef.current = viewableItems[0].index !== 0; + } + }, [viewableItems]); + useEffect(() => { const handlePageChange = async () => { const checkAsync = await AsyncStorage.getItem( @@ -208,14 +223,6 @@ const SuggestedPeopleScreen: React.FC = () => { updateDisplayedUser(user, 'no_record', ''); }; - const onViewableItemsChanged = useCallback( - ({viewableItems}: {viewableItems: ViewToken[]}) => { - setHideStatusBar(viewableItems[0].index !== 0); - stausBarRef.current = viewableItems[0].index !== 0; - }, - [], - ); - useFocusEffect(() => { if (suggested_people_linked === 0) { navigation.navigate('SPWelcomeScreen'); @@ -244,7 +251,7 @@ const SuggestedPeopleScreen: React.FC = () => { }} keyExtractor={(_, index) => index.toString()} showsVerticalScrollIndicator={false} - onViewableItemsChanged={onViewableItemsChanged} + onViewableItemsChanged={viewabilityConfigCallback.current} onEndReached={() => setPage(page + 1)} onEndReachedThreshold={3} refreshControl={ |