From 1b7fef188ec2aee0706fc1204432315db3d4fec6 Mon Sep 17 00:00:00 2001 From: Shravya Ramesh <37447613+shravyaramesh@users.noreply.github.com> Date: Mon, 19 Oct 2020 12:42:15 -0700 Subject: Tma235/231 Individual view and horizontal view (#59) * Implemented modal stack navigation for moment view, created a rough UI for individual moment view [incl: title, image(not displayed)] * bare bones beginnning * Created individual moment screen, moment tile for horizontal view * Alert * Fix initial route Co-authored-by: Ashm Walia Co-authored-by: Ashm Walia <40498934+ashmgarv@users.noreply.github.com> --- src/components/profile/Content.tsx | 79 ++++++++++++-- src/components/profile/Moment.tsx | 41 +++++--- src/components/profile/MomentTile.tsx | 33 ++++++ src/constants/api.ts | 2 +- src/routes/authentication/AuthProvider.tsx | 9 ++ src/routes/profile/MomentStack.tsx | 11 ++ src/routes/profile/MomentStackScreen.tsx | 46 ++++++++ src/routes/profile/ProfileStack.tsx | 5 +- src/routes/profile/index.ts | 2 + src/routes/tabs/NavigationBar.tsx | 11 +- src/screens/profile/CaptionScreen.tsx | 14 ++- src/screens/profile/IndividualMoment.tsx | 162 +++++++++++++++++++++++++++++ src/screens/profile/index.ts | 1 + src/types/types.ts | 8 ++ 14 files changed, 384 insertions(+), 40 deletions(-) create mode 100644 src/components/profile/MomentTile.tsx create mode 100644 src/routes/profile/MomentStack.tsx create mode 100644 src/routes/profile/MomentStackScreen.tsx create mode 100644 src/screens/profile/IndividualMoment.tsx (limited to 'src') diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index 8d368747..d52696a7 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -1,8 +1,11 @@ -import React, {useState} from 'react'; -import {LayoutChangeEvent, StyleSheet, View} from 'react-native'; -import {Text} from 'react-native-animatable'; +import AsyncStorage from '@react-native-community/async-storage'; +import React, {useCallback, useEffect, useState} from 'react'; +import {Alert, LayoutChangeEvent, StyleSheet, View} from 'react-native'; + import Animated from 'react-native-reanimated'; -import {defaultMoments} from '../../constants'; +import {AuthContext} from '../../routes/authentication'; +import {MomentType, UserType} from 'src/types'; +import {defaultMoments, MOMENTS_ENDPOINT} from '../../constants'; import {SCREEN_HEIGHT} from '../../utils'; import TaggsBar from '../taggs/TaggsBar'; import Moment from './Moment'; @@ -14,12 +17,70 @@ interface ContentProps { y: Animated.Value; isProfileView: boolean; } + const Content: React.FC = ({y, isProfileView}) => { const [profileBodyHeight, setProfileBodyHeight] = useState(0); + const {newMomentsAvailable, updateMoments} = React.useContext(AuthContext); + + const [imagesList, setImagesList] = useState([]); + const [imagesMap, setImagesMap] = useState>( + new Map(), + ); const onLayout = (e: LayoutChangeEvent) => { const {height} = e.nativeEvent.layout; setProfileBodyHeight(height); }; + + const {userId, username} = user; + + const createImagesMap = useCallback(() => { + var map = new Map(); + imagesList.forEach(function (imageObject) { + var moment_category = imageObject.moment_category; + if (map.has(moment_category)) { + map.get(moment_category).push(imageObject); + } else { + map.set(moment_category, [imageObject]); + } + }); + + setImagesMap(map); + console.log(map); + }, [imagesList]); + + useEffect(() => { + if (!userId) { + return; + } + + const retrieveMoments = async () => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch(MOMENTS_ENDPOINT + `${userId}/`, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const status = response.status; + if (status === 200) { + const data = await response.json(); + setImagesList(data); + updateMoments(!newMomentsAvailable); + } else { + Alert.alert('Could not load moments!'); + } + } catch (err) { + Alert.alert('Could not load moments!'); + } + }; + + if (newMomentsAvailable) { + retrieveMoments(); + createImagesMap(); + } + }, [userId, createImagesMap, updateMoments, newMomentsAvailable]); + return ( = ({y, isProfileView}) => { + + {!isProfileView ? ( - {defaultMoments.map((title, index) => ( - - ))} - + {defaultMoments.map((title, index) => ( + + ))} + ) : ( )} diff --git a/src/components/profile/Moment.tsx b/src/components/profile/Moment.tsx index 6ae8d38e..be7cbfba 100644 --- a/src/components/profile/Moment.tsx +++ b/src/components/profile/Moment.tsx @@ -1,6 +1,6 @@ import {useNavigation} from '@react-navigation/native'; import React from 'react'; -import {StyleSheet, View} from 'react-native'; +import {Alert, StyleSheet, View} from 'react-native'; import {Text} from 'react-native-animatable'; import {ScrollView, TouchableOpacity} from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; @@ -9,10 +9,12 @@ import BigPlusIcon from '../../assets/icons/plus_icon-02.svg'; import {MOMENTS_TITLE_COLOR} from '../../constants'; import {SCREEN_WIDTH} from '../../utils'; import ImagePicker from 'react-native-image-crop-picker'; +import MomentTile from './MomentTile'; +import {MomentType} from 'src/types'; interface MomentProps { title: string; - images: Array; + images: MomentType[] | undefined; } const Moment: React.FC = ({title, images}) => { @@ -28,10 +30,13 @@ const Moment: React.FC = ({title, images}) => { }) .then((picture) => { if ('path' in picture) { - navigation.navigate('CaptionScreen', {title: title, image: picture}); + navigation.navigate('CaptionScreen', { + title: title, + image: picture, + }); } }) - .catch(() => {}); + .catch((err) => {Alert.alert('Unable to upload moment!');}); }; return ( @@ -47,17 +52,23 @@ const Moment: React.FC = ({title, images}) => { horizontal showsHorizontalScrollIndicator={false} style={styles.scrollContainer}> - navigateToImagePicker()}> - - - - - Add a moment of your {title.toLowerCase()}! - - - - + {images && + images.map((imageObj: MomentType) => ( + + ))} + {(images === undefined || images.length === 0) && ( + navigateToImagePicker()}> + + + + + Add a moment of your {title.toLowerCase()}! + + + + + )} ); diff --git a/src/components/profile/MomentTile.tsx b/src/components/profile/MomentTile.tsx new file mode 100644 index 00000000..70b20d40 --- /dev/null +++ b/src/components/profile/MomentTile.tsx @@ -0,0 +1,33 @@ +import {useNavigation} from '@react-navigation/native'; +import React from 'react'; +import {StyleSheet, View, Image, TouchableOpacity} from 'react-native'; +import {MomentType} from 'src/types'; + +interface MomentTileProps { + moment: MomentType; +} +const MomentTile: React.FC = ({moment}) => { + const navigation = useNavigation(); + const {path_hash} = moment; + return ( + { + navigation.navigate('IndividualMoment', {moment}); + }}> + + + + + ); +}; + +const styles = StyleSheet.create({ + image: { + aspectRatio: 1, + height: '100%', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + }, +}); +export default MomentTile; diff --git a/src/constants/api.ts b/src/constants/api.ts index 8e935714..93a68d65 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -12,7 +12,7 @@ export const COVER_PHOTO_ENDPOINT: string = API_URL + 'large-profile-pic/'; export const AVATAR_PHOTO_ENDPOINT: string = API_URL + 'small-profile-pic/'; export const GET_IG_POSTS_ENDPOINT: string = API_URL + 'posts-ig/'; export const SEARCH_ENDPOINT: string = API_URL + 'search/'; -export const MOMENTS_UPLOAD_ENDPOINT: string = API_URL + 'moments/'; +export const MOMENTS_ENDPOINT: string = API_URL + 'moments/'; export const VERIFY_INVITATION_CODE_ENDPOUNT: string = API_URL + 'verify-code/'; // Social Link diff --git a/src/routes/authentication/AuthProvider.tsx b/src/routes/authentication/AuthProvider.tsx index 6f577a73..8dd9fd73 100644 --- a/src/routes/authentication/AuthProvider.tsx +++ b/src/routes/authentication/AuthProvider.tsx @@ -24,6 +24,8 @@ interface AuthContextProps { cover: string | null; instaPosts: Array; recentSearches: Array; + newMomentsAvailable: boolean; + updateMoments: (value: boolean) => void; } const NO_USER: UserType = { userId: '', @@ -43,6 +45,8 @@ export const AuthContext = createContext({ cover: null, instaPosts: [], recentSearches: [], + newMomentsAvailable: true, + updateMoments: () => {}, }); /** @@ -57,6 +61,7 @@ const AuthProvider: React.FC = ({children}) => { const [recentSearches, setRecentSearches] = useState< Array >([]); + const [newMomentsAvailable, setNewMomentsAvailable] = useState(true); const {userId} = user; useEffect(() => { if (!userId) { @@ -90,6 +95,7 @@ const AuthProvider: React.FC = ({children}) => { avatar, cover, instaPosts, + newMomentsAvailable, login: (id, username) => { setUser({...user, userId: id, username}); }, @@ -105,6 +111,9 @@ const AuthProvider: React.FC = ({children}) => { } }, recentSearches, + updateMoments: (value) => { + setNewMomentsAvailable(value); + }, }}> {children} diff --git a/src/routes/profile/MomentStack.tsx b/src/routes/profile/MomentStack.tsx new file mode 100644 index 00000000..83853c99 --- /dev/null +++ b/src/routes/profile/MomentStack.tsx @@ -0,0 +1,11 @@ +import {createStackNavigator} from '@react-navigation/stack'; +import {MomentType} from '../../types'; + +export type MomentStackParams = { + Profile: undefined; + IndividualMoment: { + moment: MomentType; + }; +}; + +export const MomentStack = createStackNavigator(); diff --git a/src/routes/profile/MomentStackScreen.tsx b/src/routes/profile/MomentStackScreen.tsx new file mode 100644 index 00000000..8768199a --- /dev/null +++ b/src/routes/profile/MomentStackScreen.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import {IndividualMoment} from '../../screens'; +import {MomentStack} from './MomentStack'; +import Profile from './Profile'; + +const MomentStackScreen: React.FC = () => { + return ( + ({ + cardStyle: { + opacity: progress.interpolate({ + inputRange: [0, 0.5, 0.9, 1], + outputRange: [0, 0.25, 0.7, 1], + }), + }, + overlayStyle: { + backgroundColor: '#808080', + opacity: progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.9], + extrapolate: 'clamp', + }), + }, + }), + }} + initialRouteName="Profile" + mode="modal"> + + + + ); +}; + +export default MomentStackScreen; diff --git a/src/routes/profile/ProfileStack.tsx b/src/routes/profile/ProfileStack.tsx index 63ab9a10..df4d234f 100644 --- a/src/routes/profile/ProfileStack.tsx +++ b/src/routes/profile/ProfileStack.tsx @@ -10,7 +10,10 @@ export type ProfileStackParams = { socialMediaHandle: string; isProfileView: boolean; }; - CaptionScreen: {title: string; image: object}; + CaptionScreen: { + title: string; + image: object; + }; }; export const ProfileStack = createStackNavigator(); diff --git a/src/routes/profile/index.ts b/src/routes/profile/index.ts index 367f4cc6..1ab9cb7e 100644 --- a/src/routes/profile/index.ts +++ b/src/routes/profile/index.ts @@ -1,2 +1,4 @@ export * from './ProfileStack'; +export * from './MomentStack'; +export * from './MomentStackScreen'; export {default} from './Profile'; diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx index 2852b565..f05a512b 100644 --- a/src/routes/tabs/NavigationBar.tsx +++ b/src/routes/tabs/NavigationBar.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {NavigationIcon} from '../../components'; import {Home, Notifications, Upload} from '../../screens'; import Profile from '../profile'; +import MomentStackScreen from '../profile/MomentStackScreen'; const Tabs = createBottomTabNavigator(); @@ -35,7 +36,7 @@ const NavigationBar: React.FC = () => { ) : ( ); - } else if (route.name === 'Profile') { + } else if (route.name === 'MomentStackScreen') { return focused ? ( ) : ( @@ -44,7 +45,7 @@ const NavigationBar: React.FC = () => { } }, })} - initialRouteName="Profile" + initialRouteName="MomentStackScreen" tabBarOptions={{ showLabel: false, style: { @@ -64,11 +65,7 @@ const NavigationBar: React.FC = () => { /> - + ); }; diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index 53c47a6d..d65a8451 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -9,7 +9,7 @@ import {RouteProp} from '@react-navigation/native'; import {ProfileStackParams} from 'src/routes'; import {StackNavigationProp} from '@react-navigation/stack'; import {CaptionScreenHeader} from '../../components/profile'; -import {MOMENTS_UPLOAD_ENDPOINT} from '../../constants'; +import {MOMENTS_ENDPOINT} from '../../constants'; import {AuthContext} from '../../routes/authentication'; const NO_USER: UserType = { @@ -34,8 +34,8 @@ const CaptionScreen: React.FC = ({route, navigation}) => { const {title, image} = route.params; const { user: {userId}, + updateMoments, } = React.useContext(AuthContext); - const [user, setUser] = useState(NO_USER); const [caption, setCaption] = React.useState(''); const handleCaptionUpdate = (caption: string) => { @@ -53,11 +53,6 @@ const CaptionScreen: React.FC = ({route, navigation}) => { const handleShare = async () => { try { - const token = await AsyncStorage.getItem('token'); - if (!token) { - setUser(NO_USER); - return; - } const request = new FormData(); const uri = image.path; const name = image.filename; @@ -69,7 +64,8 @@ const CaptionScreen: React.FC = ({route, navigation}) => { request.append('moment', title); request.append('user_id', userId); request.append('captions', JSON.stringify({image: caption})); - let response = await fetch(MOMENTS_UPLOAD_ENDPOINT, { + const token = await AsyncStorage.getItem('token'); + let response = await fetch(MOMENTS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'multipart/form-data', @@ -81,6 +77,7 @@ const CaptionScreen: React.FC = ({route, navigation}) => { let data = await response.json(); if (statusCode === 200 && checkImageUploadStatus(data)) { Alert.alert('The picture was uploaded successfully!'); + updateMoments(true); navigation.navigate('Profile'); } else { Alert.alert('An error occured while uploading. Please try again!'); @@ -89,6 +86,7 @@ const CaptionScreen: React.FC = ({route, navigation}) => { Alert.alert('An error occured during authenticaion. Please login again!'); } }; + return ( diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx new file mode 100644 index 00000000..377898c1 --- /dev/null +++ b/src/screens/profile/IndividualMoment.tsx @@ -0,0 +1,162 @@ +import React, {useEffect, useState} from 'react'; +import {StyleSheet, View, Image} from 'react-native'; +import {Button} from 'react-native-elements'; +import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils'; +import {UserType} from '../../types'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {CaptionScreenHeader} from '../../components/profile'; +import {AuthContext} from '../../routes/authentication'; +import {MomentStackParams} from 'src/routes/profile/MomentStack'; +import moment from 'moment'; +import Animated from 'react-native-reanimated'; + +const NO_USER: UserType = { + userId: '', + username: '', +}; + +/** + * Individual moment view opened when user clicks on a moment tile + */ +type IndividualMomentRouteProp = RouteProp< + MomentStackParams, + 'IndividualMoment' +>; +type IndividualMomentNavigationProp = StackNavigationProp< + MomentStackParams, + 'IndividualMoment' +>; +interface IndividualMomentProps { + route: IndividualMomentRouteProp; + navigation: IndividualMomentNavigationProp; +} + +const IndividualMoment: React.FC = ({ + route, + navigation, +}) => { + const { + moment_category, + path_hash, + date_time, + moment_id, + } = route.params.moment; + const { + user: {userId}, + } = React.useContext(AuthContext); + const [user, setUser] = useState(NO_USER); + const [caption, setCaption] = React.useState(route.params.moment.caption); + const [elapsedTime, setElapsedTime] = React.useState(); + const handleCaptionUpdate = (caption: string) => { + setCaption(caption); + }; + + useEffect(() => { + if (!userId) { + setUser(NO_USER); + } + const timePeriod = async () => { + const datePosted = moment(date_time); + const now = moment(); + var time = date_time; + var difference = now.diff(datePosted, 'seconds'); + + //Creating elapsedTime string to display to user + // 0 to less than 1 minute + if (difference < 60) { + time = difference + 'seconds'; + } + // 1 minute to less than 1 hour + else if (difference >= 60 && difference < 60 * 60) { + difference = now.diff(datePosted, 'minutes'); + time = difference + (difference === 1 ? 'minute' : 'minutes'); + } + //1 hour to less than 1 day + else if (difference >= 60 * 60 && difference < 24 * 60 * 60) { + difference = now.diff(datePosted, 'hours'); + time = difference + (difference === 1 ? 'hour' : 'hours'); + } + //1 day to less than 7 days + else if (difference >= 24 * 60 * 60 && difference < 7 * 24 * 60 * 60) { + difference = now.diff(datePosted, 'days'); + time = difference + (difference === 1 ? 'day' : 'days'); + } + + setElapsedTime(time); + }; + timePeriod(); + }, [date_time, userId]); + + return ( + + +