aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/assets/navigationIcons/new-upload.pngbin0 -> 454951 bytes
-rw-r--r--src/components/camera/GalleryIcon.tsx12
-rw-r--r--src/components/camera/SaveButton.tsx4
-rw-r--r--src/components/comments/AddComment.tsx23
-rw-r--r--src/components/comments/CommentTextField.tsx45
-rw-r--r--src/components/comments/CommentsCount.tsx15
-rw-r--r--src/components/comments/ZoomInCropper.tsx28
-rw-r--r--src/components/common/MomentTags.tsx14
-rw-r--r--src/components/common/NavigationIcon.tsx19
-rw-r--r--src/components/moments/Moment.tsx52
-rw-r--r--src/components/moments/MomentPost.tsx132
-rw-r--r--src/components/moments/legacy/MomentPostContent.tsx49
-rw-r--r--src/components/profile/MomentMoreInfoDrawer.tsx11
-rw-r--r--src/components/profile/ProfileBody.tsx1
-rw-r--r--src/components/taggs/TaggDraggable.tsx4
-rw-r--r--src/constants/api.ts1
-rw-r--r--src/constants/regex.ts2
-rw-r--r--src/constants/strings.ts2
-rw-r--r--src/routes/main/MainStackNavigator.tsx17
-rw-r--r--src/routes/main/MainStackScreen.tsx88
-rw-r--r--src/routes/tabs/NavigationBar.tsx51
-rw-r--r--src/screens/chat/ChatListScreen.tsx2
-rw-r--r--src/screens/moments/CameraScreen.tsx55
-rw-r--r--src/screens/moments/TagFriendsScreen.tsx195
-rw-r--r--src/screens/profile/CaptionScreen.tsx147
-rw-r--r--src/screens/profile/IndividualMoment.tsx55
-rw-r--r--src/screens/suggestedPeople/SuggestedPeopleScreen.tsx25
-rw-r--r--src/services/MomentService.ts121
-rw-r--r--src/types/types.ts46
-rw-r--r--src/utils/camera.ts81
30 files changed, 813 insertions, 484 deletions
diff --git a/src/assets/navigationIcons/new-upload.png b/src/assets/navigationIcons/new-upload.png
new file mode 100644
index 00000000..f6a5487c
--- /dev/null
+++ b/src/assets/navigationIcons/new-upload.png
Binary files differ
diff --git a/src/components/camera/GalleryIcon.tsx b/src/components/camera/GalleryIcon.tsx
index c49ace7d..8d396550 100644
--- a/src/components/camera/GalleryIcon.tsx
+++ b/src/components/camera/GalleryIcon.tsx
@@ -1,14 +1,12 @@
-import {useNavigation} from '@react-navigation/native';
import React from 'react';
import {Image, Text, TouchableOpacity, View} from 'react-native';
-import {ScreenType} from '../../types';
import {navigateToImagePicker} from '../../utils/camera';
+import {Image as ImageType} from 'react-native-image-crop-picker';
import {styles} from './styles';
interface GalleryIconProps {
- screenType: ScreenType;
- title: string;
mostRecentPhotoUri: string;
+ callback: (pic: ImageType) => void;
}
/*
@@ -16,14 +14,12 @@ interface GalleryIconProps {
* On click, navigates to the image picker
*/
export const GalleryIcon: React.FC<GalleryIconProps> = ({
- screenType,
- title,
mostRecentPhotoUri,
+ callback,
}) => {
- const navigation = useNavigation();
return (
<TouchableOpacity
- onPress={() => navigateToImagePicker(navigation, screenType, title)}
+ onPress={() => navigateToImagePicker(callback)}
style={styles.saveButton}>
{mostRecentPhotoUri !== '' ? (
<Image
diff --git a/src/components/camera/SaveButton.tsx b/src/components/camera/SaveButton.tsx
index 840cc804..0e220497 100644
--- a/src/components/camera/SaveButton.tsx
+++ b/src/components/camera/SaveButton.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import {Text, TouchableOpacity} from 'react-native';
import SaveIcon from '../../assets/icons/camera/save.svg';
-import {downloadImage} from '../../utils/camera';
+import {saveImageToGallery} from '../../utils/camera';
import {styles} from './styles';
interface SaveButtonProps {
@@ -15,7 +15,7 @@ interface SaveButtonProps {
export const SaveButton: React.FC<SaveButtonProps> = ({capturedImageURI}) => (
<TouchableOpacity
onPress={() => {
- downloadImage(capturedImageURI);
+ saveImageToGallery(capturedImageURI);
}}
style={styles.saveButton}>
<SaveIcon width={40} height={40} />
diff --git a/src/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx
index 8a4ec082..33707d94 100644
--- a/src/components/comments/AddComment.tsx
+++ b/src/components/comments/AddComment.tsx
@@ -8,12 +8,11 @@ import {
View,
} from 'react-native';
import {useDispatch} from 'react-redux';
-import {TAGG_LIGHT_BLUE} from '../../constants';
import {CommentContext} from '../../screens/profile/MomentCommentsScreen';
import {postComment} from '../../services';
import {updateReplyPosted} from '../../store/actions';
import {CommentThreadType, CommentType} from '../../types';
-import {SCREEN_HEIGHT, SCREEN_WIDTH, normalize} from '../../utils';
+import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
import {mentionPartTypes} from '../../utils/comments';
import {CommentTextField} from './CommentTextField';
import MentionInputControlled from './MentionInputControlled';
@@ -174,26 +173,6 @@ const styles = StyleSheet.create({
flex: 1,
maxHeight: 100,
},
- avatar: {
- height: 35,
- width: 35,
- borderRadius: 30,
- marginRight: 10,
- marginLeft: '3%',
- marginVertical: '2%',
- alignSelf: 'flex-end',
- },
- submitButton: {
- height: 35,
- width: 35,
- backgroundColor: TAGG_LIGHT_BLUE,
- borderRadius: 999,
- justifyContent: 'center',
- alignItems: 'center',
- marginRight: '3%',
- marginVertical: '2%',
- alignSelf: 'flex-end',
- },
whiteBackround: {
backgroundColor: '#fff',
},
diff --git a/src/components/comments/CommentTextField.tsx b/src/components/comments/CommentTextField.tsx
index 6e92329c..6d86eb3f 100644
--- a/src/components/comments/CommentTextField.tsx
+++ b/src/components/comments/CommentTextField.tsx
@@ -1,8 +1,8 @@
import React, {FC, ReactFragment} from 'react';
import {
NativeSyntheticEvent,
- StyleSheet,
StyleProp,
+ StyleSheet,
Text,
TextInput,
TextInputSelectionChangeEventData,
@@ -10,22 +10,21 @@ import {
View,
ViewStyle,
} from 'react-native';
-import {useSelector} from 'react-redux';
-import {TAGG_LIGHT_BLUE} from '../../constants';
-import {RootState} from '../../store/rootReducer';
import {
+ MentionPartType,
Part,
PartType,
- MentionPartType,
} from 'react-native-controlled-mentions/dist/types';
import {
defaultMentionTextStyle,
isMentionPartType,
} from 'react-native-controlled-mentions/dist/utils';
-import {Avatar} from '../common';
-import {normalize} from '../../utils';
-
+import {useSelector} from 'react-redux';
import UpArrowIcon from '../../assets/icons/up_arrow.svg';
+import {TAGG_LIGHT_BLUE} from '../../constants';
+import {RootState} from '../../store/rootReducer';
+import {normalize} from '../../utils';
+import {Avatar} from '../common';
type CommentTextFieldProps = {
containerStyle: StyleProp<ViewStyle>;
@@ -40,8 +39,6 @@ type CommentTextFieldProps = {
) => null;
parts: Part[];
addComment: () => any;
- theme?: 'dark' | 'white';
- keyboardVisible?: boolean;
comment?: string;
};
@@ -56,8 +53,6 @@ const CommentTextField: FC<CommentTextFieldProps> = ({
handleSelectionChange,
parts,
addComment,
- theme = 'white',
- keyboardVisible = true,
comment = '',
...textInputProps
}) => {
@@ -99,20 +94,18 @@ const CommentTextField: FC<CommentTextFieldProps> = ({
)}
</Text>
</TextInput>
- {(theme === 'white' || (theme === 'dark' && keyboardVisible)) && (
- <View style={styles.submitButton}>
- <TouchableOpacity
- style={
- comment === ''
- ? [styles.submitButton, styles.greyButton]
- : styles.submitButton
- }
- disabled={comment === ''}
- onPress={addComment}>
- <UpArrowIcon width={35} height={35} color={'white'} />
- </TouchableOpacity>
- </View>
- )}
+ <View style={styles.submitButton}>
+ <TouchableOpacity
+ style={
+ comment === ''
+ ? [styles.submitButton, styles.greyButton]
+ : styles.submitButton
+ }
+ disabled={comment === ''}
+ onPress={addComment}>
+ <UpArrowIcon width={35} height={35} color={'white'} />
+ </TouchableOpacity>
+ </View>
</View>
{validateInput(keyboardText) &&
diff --git a/src/components/comments/CommentsCount.tsx b/src/components/comments/CommentsCount.tsx
index 90514193..d4a93bdd 100644
--- a/src/components/comments/CommentsCount.tsx
+++ b/src/components/comments/CommentsCount.tsx
@@ -3,27 +3,32 @@ import React from 'react';
import {StyleSheet, Text} from 'react-native';
import {TouchableOpacity} from 'react-native-gesture-handler';
import CommentsIcon from '../../assets/icons/moment-comment-icon.svg';
-import {MomentPostType, ScreenType} from '../../types';
+import {ScreenType} from '../../types';
import {normalize} from '../../utils';
interface CommentsCountProps {
- moment: MomentPostType;
+ momentId: string;
+ count: number;
screenType: ScreenType;
}
-const CommentsCount: React.FC<CommentsCountProps> = ({moment, screenType}) => {
+const CommentsCount: React.FC<CommentsCountProps> = ({
+ momentId,
+ count,
+ screenType,
+}) => {
const navigation = useNavigation();
return (
<TouchableOpacity
style={styles.countContainer}
onPress={() =>
navigation.navigate('MomentCommentsScreen', {
- moment_id: moment.moment_id,
+ moment_id: momentId,
screenType,
})
}>
<CommentsIcon width={25} height={25} />
- <Text style={styles.count}>{moment.comments_count}</Text>
+ <Text style={styles.count}>{count}</Text>
</TouchableOpacity>
);
};
diff --git a/src/components/comments/ZoomInCropper.tsx b/src/components/comments/ZoomInCropper.tsx
index 94e772b6..7fa88f6e 100644
--- a/src/components/comments/ZoomInCropper.tsx
+++ b/src/components/comments/ZoomInCropper.tsx
@@ -1,7 +1,7 @@
import {RouteProp} from '@react-navigation/core';
import {useFocusEffect} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
-import {default as React, useCallback, useEffect, useState} from 'react';
+import React, {useCallback, useEffect, useState} from 'react';
import {Image, StyleSheet, TouchableOpacity} from 'react-native';
import {normalize} from 'react-native-elements';
import ImageZoom, {IOnMove} from 'react-native-image-pan-zoom';
@@ -25,7 +25,7 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({
route,
navigation,
}) => {
- const {screenType, title, image} = route.params;
+ const {screenType, title, media} = route.params;
const [aspectRatio, setAspectRatio] = useState<number>(1);
// Stores the coordinates of the cropped image
@@ -34,7 +34,6 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({
const [y0, setY0] = useState<number>();
const [y1, setY1] = useState<number>();
- // Removes bottom navigation bar on current screen and add it back when navigating away
useFocusEffect(
useCallback(() => {
navigation.dangerouslyGetParent()?.setOptions({
@@ -50,9 +49,9 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({
// Setting original aspect ratio of image
useEffect(() => {
- if (image.sourceURL) {
+ if (media.uri) {
Image.getSize(
- image.sourceURL,
+ media.uri,
(w, h) => {
setAspectRatio(w / h);
},
@@ -67,10 +66,9 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({
x0 !== undefined &&
x1 !== undefined &&
y0 !== undefined &&
- y1 !== undefined &&
- image.sourceURL
+ y1 !== undefined
) {
- PhotoManipulator.crop(image.sourceURL, {
+ PhotoManipulator.crop(media.uri, {
x: x0,
y: y1,
width: Math.abs(x0 - x1),
@@ -80,7 +78,10 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({
navigation.navigate('CaptionScreen', {
screenType,
title: title,
- image: {filename: croppedURL, path: croppedURL},
+ media: {
+ uri: croppedURL,
+ isVideo: false,
+ },
});
})
.catch((err) => console.log('err: ', err));
@@ -88,13 +89,12 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({
x0 === undefined &&
x1 === undefined &&
y0 === undefined &&
- y1 === undefined &&
- image.sourceURL
+ y1 === undefined
) {
navigation.navigate('CaptionScreen', {
screenType,
title: title,
- image: {filename: image.sourceURL, path: image.sourceURL},
+ media,
});
}
};
@@ -104,7 +104,7 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({
*/
const onMove = (position: IOnMove) => {
Image.getSize(
- image.path,
+ media.uri,
(w, h) => {
const x = position.positionX;
const y = position.positionY;
@@ -154,7 +154,7 @@ export const ZoomInCropper: React.FC<ZoomInCropperProps> = ({
<Image
style={{width: SCREEN_WIDTH, height: SCREEN_WIDTH / aspectRatio}}
source={{
- uri: image.sourceURL,
+ uri: media.uri,
}}
/>
</ImageZoom>
diff --git a/src/components/common/MomentTags.tsx b/src/components/common/MomentTags.tsx
index 4afacddb..d8a70353 100644
--- a/src/components/common/MomentTags.tsx
+++ b/src/components/common/MomentTags.tsx
@@ -1,4 +1,5 @@
-import React, {createRef, MutableRefObject, useEffect, useState} from 'react';
+import React, {createRef, RefObject, useEffect, useState} from 'react';
+import {Image, View} from 'react-native';
import {MomentTagType, ProfilePreviewType} from '../../types';
import TaggDraggable from '../taggs/TaggDraggable';
import Draggable from './Draggable';
@@ -7,7 +8,7 @@ interface MomentTagsProps {
editing: boolean;
tags: MomentTagType[];
setTags: (tag: MomentTagType[]) => void;
- imageRef: MutableRefObject<null>;
+ imageRef: RefObject<Image>;
deleteFromList?: (user: ProfilePreviewType) => void;
}
@@ -21,14 +22,9 @@ const MomentTags: React.FC<MomentTagsProps> = ({
const [offset, setOffset] = useState([0, 0]);
const [imageDimensions, setImageDimensions] = useState([0, 0]);
const [maxZIndex, setMaxZIndex] = useState(1);
- const [draggableRefs, setDraggableRefs] = useState<
- React.MutableRefObject<null>[]
- >([]);
+ const [draggableRefs, setDraggableRefs] = useState<RefObject<View>[]>([]);
- const updateTagPosition = (
- ref: React.MutableRefObject<null>,
- userId: string,
- ) => {
+ const updateTagPosition = (ref: RefObject<Image>, userId: string) => {
if (ref !== null && ref.current !== null) {
ref.current.measure(
(
diff --git a/src/components/common/NavigationIcon.tsx b/src/components/common/NavigationIcon.tsx
index 5128f3da..f97bb861 100644
--- a/src/components/common/NavigationIcon.tsx
+++ b/src/components/common/NavigationIcon.tsx
@@ -18,6 +18,7 @@ interface NavigationIconProps extends TouchableOpacityProps {
| 'Chat';
disabled?: boolean;
newIcon?: boolean;
+ isBigger?: boolean;
}
const NavigationIcon = (props: NavigationIconProps) => {
@@ -35,7 +36,7 @@ const NavigationIcon = (props: NavigationIconProps) => {
break;
case 'Upload':
imgSrc = props.disabled
- ? require('../../assets/navigationIcons/upload.png')
+ ? require('../../assets/navigationIcons/new-upload.png')
: require('../../assets/navigationIcons/upload-clicked.png');
break;
case 'Notifications':
@@ -68,12 +69,22 @@ const NavigationIcon = (props: NavigationIconProps) => {
return (
<View style={styles.container}>
<TouchableOpacity {...props}>
- <Image source={imgSrc} style={styles.icon} />
+ <Image source={imgSrc} style={getStyles(props.isBigger ?? false)} />
</TouchableOpacity>
</View>
);
};
+const getStyles = (isBigger: boolean) =>
+ isBigger ? biggerIconStyles.icon : styles.icon;
+
+const biggerIconStyles = StyleSheet.create({
+ icon: {
+ height: 44,
+ width: 44,
+ },
+});
+
const styles = StyleSheet.create({
container: {
flex: 1,
@@ -87,8 +98,8 @@ const styles = StyleSheet.create({
shadowOpacity: 0.4,
},
icon: {
- height: 30,
- width: 30,
+ height: 28,
+ width: 28,
},
});
diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx
index 9449271b..1e1cadce 100644
--- a/src/components/moments/Moment.tsx
+++ b/src/components/moments/Moment.tsx
@@ -1,6 +1,6 @@
import {useNavigation} from '@react-navigation/native';
import React from 'react';
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
+import {Alert, StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
import {Text} from 'react-native-animatable';
import {ScrollView, TouchableOpacity} from 'react-native-gesture-handler';
import LinearGradient from 'react-native-linear-gradient';
@@ -12,6 +12,8 @@ import UpIcon from '../../assets/icons/up_icon.svg';
import {TAGG_LIGHT_BLUE} from '../../constants';
import {MomentType, ScreenType} from '../../types';
import {normalize, SCREEN_WIDTH} from '../../utils';
+import {navigateToVideoPicker} from '../../utils/camera';
+import ImagePicker from 'react-native-image-crop-picker';
import MomentTile from './MomentTile';
interface MomentProps {
@@ -41,6 +43,17 @@ const Moment: React.FC<MomentProps> = ({
}) => {
const navigation = useNavigation();
+ const navigateToCaptionScreenForVideo = (uri: string) => {
+ navigation.navigate('CaptionScreen', {
+ screenType,
+ title,
+ media: {
+ uri,
+ isVideo: true,
+ },
+ });
+ };
+
const navigateToCameraScreen = () => {
navigation.navigate('CameraScreen', {
title,
@@ -84,7 +97,37 @@ const Moment: React.FC<MomentProps> = ({
<PlusIcon
width={23}
height={23}
- onPress={() => navigateToCameraScreen()}
+ onPress={() =>
+ Alert.alert('Video Upload', 'pick one', [
+ {
+ text: 'gallery',
+ onPress: () =>
+ navigateToVideoPicker((vid) =>
+ navigateToCaptionScreenForVideo(vid.path),
+ ),
+ },
+ {
+ text: 'camera (simulator will not work)',
+ onPress: () =>
+ ImagePicker.openCamera({
+ mediaType: 'video',
+ })
+ .then((vid) => {
+ if (vid.path) {
+ navigateToCaptionScreenForVideo(vid.path);
+ }
+ })
+ .catch((err) => console.error(err)),
+ },
+ ])
+ }
+ color={'black'}
+ style={styles.horizontalMargin}
+ />
+ <PlusIcon
+ width={23}
+ height={23}
+ onPress={navigateToCameraScreen}
color={TAGG_LIGHT_BLUE}
style={styles.horizontalMargin}
/>
@@ -114,7 +157,7 @@ const Moment: React.FC<MomentProps> = ({
/>
))}
{(images === undefined || images.length === 0) && !userXId && (
- <TouchableOpacity onPress={() => navigateToCameraScreen()}>
+ <TouchableOpacity onPress={navigateToCameraScreen}>
<LinearGradient
colors={['rgba(105, 141, 211, 1)', 'rgba(105, 141, 211, 0.3)']}>
<View style={styles.defaultImage}>
@@ -150,9 +193,6 @@ const styles = StyleSheet.create({
color: TAGG_LIGHT_BLUE,
maxWidth: '70%',
},
- flexer: {
- flex: 1,
- },
scrollContainer: {
height: SCREEN_WIDTH / 3.25,
backgroundColor: '#eee',
diff --git a/src/components/moments/MomentPost.tsx b/src/components/moments/MomentPost.tsx
index 6eccf5ab..319542f9 100644
--- a/src/components/moments/MomentPost.tsx
+++ b/src/components/moments/MomentPost.tsx
@@ -1,5 +1,5 @@
import {useNavigation} from '@react-navigation/native';
-import React, {useContext, useEffect, useRef, useState} from 'react';
+import React, {useContext, useEffect, useMemo, useRef, useState} from 'react';
import {
Image,
KeyboardAvoidingView,
@@ -12,6 +12,7 @@ import {
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';
@@ -71,7 +72,16 @@ const MomentPost: React.FC<MomentPostProps> = ({
const [momentTagId, setMomentTagId] = useState<string>('');
const imageRef = useRef(null);
- const {keyboardVisible} = useContext(MomentContext);
+ 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') ||
+ moment.moment_url.endsWith('GIF') ||
+ moment.moment_url.endsWith('gif')
+ );
/*
* Load tags on initial render to pass tags data to moment header and content
@@ -126,13 +136,15 @@ const MomentPost: React.FC<MomentPostProps> = ({
* determine if image must be displayed in full screen or not
*/
useEffect(() => {
- Image.getSize(
- moment.moment_url,
- (w, h) => {
- setAspectRatio(w / h);
- },
- (err) => console.log(err),
- );
+ if (!isVideo) {
+ Image.getSize(
+ moment.moment_url,
+ (w, h) => {
+ setAspectRatio(w / h);
+ },
+ (err) => console.log(err),
+ );
+ }
}, []);
/*
@@ -155,22 +167,31 @@ const MomentPost: React.FC<MomentPostProps> = ({
}
}, [keyboardVisible, hideText]);
- 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>
+ useEffect(() => {
+ if (moment.moment_id !== currentVisibleMomentId) {
+ videoRef.current?.seek(0);
+ }
+ }, [currentVisibleMomentId]);
+
+ const momentPosterPreview = useMemo(
+ () => (
+ <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>
+ ),
+ [user.username],
);
return (
@@ -178,17 +199,44 @@ const MomentPost: React.FC<MomentPostProps> = ({
<StatusBar barStyle={'light-content'} />
<View style={styles.mainContainer}>
<View style={styles.imageContainer}>
- <Image
- source={{uri: moment.moment_url}}
- style={[
- styles.image,
- {
- height: SCREEN_WIDTH / aspectRatio,
- },
- ]}
- resizeMode={'contain'}
- ref={imageRef}
- />
+ {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}]}>
@@ -233,9 +281,13 @@ const MomentPost: React.FC<MomentPostProps> = ({
/>
)}
<View style={styles.commentsCountContainer}>
- <CommentsCount moment={moment} screenType={screenType} />
+ <CommentsCount
+ momentId={moment.moment_id}
+ count={commentCount}
+ screenType={screenType}
+ />
</View>
- <MomentPosterPreview />
+ {momentPosterPreview}
{!hideText && (
<>
{moment.caption !== '' &&
@@ -281,8 +333,9 @@ const MomentPost: React.FC<MomentPostProps> = ({
};
const styles = StyleSheet.create({
- image: {
+ media: {
zIndex: 0,
+ flex: 1,
},
imageContainer: {
height: SCREEN_HEIGHT,
@@ -340,6 +393,7 @@ const styles = StyleSheet.create({
},
commentsCountContainer: {
position: 'absolute',
+ zIndex: 3,
right: '2%',
bottom: SCREEN_HEIGHT * 0.12,
},
diff --git a/src/components/moments/legacy/MomentPostContent.tsx b/src/components/moments/legacy/MomentPostContent.tsx
index 6388be27..0e6e5eed 100644
--- a/src/components/moments/legacy/MomentPostContent.tsx
+++ b/src/components/moments/legacy/MomentPostContent.tsx
@@ -3,6 +3,7 @@ import React, {useContext, useEffect, useRef, useState} from 'react';
import {Image, StyleSheet, Text, View, ViewProps} from 'react-native';
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';
@@ -32,14 +33,12 @@ 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();
@@ -55,8 +54,14 @@ 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') ||
+ moment.moment_url.endsWith('JPG') ||
+ moment.moment_url.endsWith('PNG') ||
+ moment.moment_url.endsWith('png')
+ );
useEffect(() => {
setTags(momentTags);
@@ -78,7 +83,6 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({
setHideText(false);
}
}, [keyboardVisible, hideText]);
-
return (
<View style={[styles.container, style]}>
<TouchableWithoutFeedback
@@ -86,12 +90,34 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({
setVisible(!visible);
setFadeValue(new Animated.Value(0));
}}>
- <Image
- ref={imageRef}
- style={styles.image}
- source={{uri: moment.moment_url}}
- resizeMode={'cover'}
- />
+ {isVideo ? (
+ <View ref={imageRef}>
+ <Video
+ // ref={imageRef}
+ source={{
+ uri: moment.moment_url,
+ }}
+ // HLS m3u8 version
+ // source={{
+ // uri: 'https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8',
+ // }}
+ // mp4 version
+ // source={{
+ // uri: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
+ // }}
+ volume={1}
+ style={styles.image}
+ repeat={true}
+ />
+ </View>
+ ) : (
+ <Image
+ ref={imageRef}
+ style={styles.image}
+ source={{uri: moment.moment_url}}
+ resizeMode={'cover'}
+ />
+ )}
{tags.length > 0 && (
<Image
source={require('../../assets/icons/tag_indicate.png')}
@@ -115,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,
@@ -145,7 +171,6 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({
}}
onFocus={() => {
setHideText(true);
- scrollTo(index);
}}
isKeyboardAvoiding={false}
theme={'dark'}
diff --git a/src/components/profile/MomentMoreInfoDrawer.tsx b/src/components/profile/MomentMoreInfoDrawer.tsx
index dc4ebe32..910aa095 100644
--- a/src/components/profile/MomentMoreInfoDrawer.tsx
+++ b/src/components/profile/MomentMoreInfoDrawer.tsx
@@ -31,6 +31,13 @@ interface MomentMoreInfoDrawerProps extends ViewProps {
tags: MomentTagType[];
}
+type DrawerButtonType = [
+ string,
+ (event: GestureResponderEvent) => void,
+ JSX.Element?,
+ TextStyle?,
+][];
+
const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => {
const {
setIsOpen,
@@ -45,9 +52,7 @@ const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => {
const navigation = useNavigation();
- const [drawerButtons, setDrawerButtons] = useState<
- [string, (event: GestureResponderEvent) => void, JSX.Element?, TextStyle?][]
- >([]);
+ const [drawerButtons, setDrawerButtons] = useState<DrawerButtonType>([]);
const handleDeleteMoment = async () => {
setIsOpen(false);
diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx
index c0ee508a..cc001516 100644
--- a/src/components/profile/ProfileBody.tsx
+++ b/src/components/profile/ProfileBody.tsx
@@ -80,7 +80,6 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
);
}}>{`${website}`}</Text>
)}
-
{userXId && isBlocked && (
<View style={styles.toggleButtonContainer}>
<ToggleButton
diff --git a/src/components/taggs/TaggDraggable.tsx b/src/components/taggs/TaggDraggable.tsx
index d458fab6..ea19591d 100644
--- a/src/components/taggs/TaggDraggable.tsx
+++ b/src/components/taggs/TaggDraggable.tsx
@@ -1,5 +1,5 @@
import {useNavigation} from '@react-navigation/native';
-import React from 'react';
+import React, {RefObject} from 'react';
import {
Image,
StyleSheet,
@@ -17,7 +17,7 @@ import {normalize} from '../../utils';
import {navigateToProfile} from '../../utils/users';
interface TaggDraggableProps extends ViewProps {
- draggableRef: React.MutableRefObject<null>;
+ draggableRef: RefObject<View>;
taggedUser: ProfilePreviewType;
editingView: boolean;
deleteFromList: () => void;
diff --git a/src/constants/api.ts b/src/constants/api.ts
index b55489d9..6dab1153 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -38,6 +38,7 @@ export const VERIFY_INVITATION_CODE_ENDPOUNT: string = API_URL + 'verify-code/';
export const COMMENTS_ENDPOINT: string = API_URL + 'comments/';
export const COMMENT_REACTIONS_ENDPOINT: string = API_URL + 'reaction-comment/';
export const COMMENT_REACTIONS_REPLY_ENDPOINT: string = API_URL + 'reaction-reply/';
+export const PRESIGNED_URL_ENDPOINT: string = API_URL + 'presigned-url/';
export const FRIENDS_ENDPOINT: string = API_URL + 'friends/';
export const ALL_USERS_ENDPOINT: string = API_URL + 'users/';
export const REPORT_ISSUE_ENDPOINT: string = API_URL + 'report/';
diff --git a/src/constants/regex.ts b/src/constants/regex.ts
index f934185d..61523203 100644
--- a/src/constants/regex.ts
+++ b/src/constants/regex.ts
@@ -36,7 +36,7 @@ export const nameRegex: RegExp = /^[A-Za-z'\-,. ]{2,20}$/;
* - match alphanumerics, and special characters used in URLs
*/
export const websiteRegex: RegExp =
- /^$|^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,50}\.[a-zA-Z0-9()]{2,6}\b([-a-zA-Z0-9()@:%_+.~#?&\/=]{0,35})$/;
+ /^$|^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,50}\.[a-zA-Z0-9()]{2,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]{0,35})$/;
/**
* The website regex has the following constraints
diff --git a/src/constants/strings.ts b/src/constants/strings.ts
index a1064f49..112bc546 100644
--- a/src/constants/strings.ts
+++ b/src/constants/strings.ts
@@ -77,7 +77,7 @@ You've been tagged by ${invitee}. Follow the instructions below to skip the line
Sign up and use this code to get in: ${inviteCode}\n ${APP_STORE_LINK}`;
export const SUCCESS_LAST_CONTACT_INVITE = 'Done! That was your last invite, hope you used it wisely!';
export const SUCCESS_LINK = (str: string) => `Successfully linked ${str} 🎉`;
-export const SUCCESS_PIC_UPLOAD = 'Beautiful, the picture was uploaded successfully!';
+export const SUCCESS_PIC_UPLOAD = 'Beautiful, the Moment was uploaded successfully!';
export const SUCCESS_PWD_RESET = 'Your password was reset successfully!';
export const SUCCESS_VERIFICATION_CODE_SENT = 'New verification code sent! Check your phone messages for your code';
export const UP_TO_DATE = 'Up-to-Date!';
diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx
index ed3863b3..f12072e6 100644
--- a/src/routes/main/MainStackNavigator.tsx
+++ b/src/routes/main/MainStackNavigator.tsx
@@ -1,8 +1,7 @@
/**
* Note the name userXId here, it refers to the id of the user being visited
*/
-import { createStackNavigator } from '@react-navigation/stack';
-import { Image } from 'react-native-image-crop-picker';
+import {createStackNavigator} from '@react-navigation/stack';
import {
CommentBaseType,
MomentTagType,
@@ -21,7 +20,7 @@ export type MainStackParams = {
}; */
Upload: {
screenType: ScreenType;
- }
+ };
RequestContactsAccess: {
screenType: ScreenType;
};
@@ -42,16 +41,16 @@ export type MainStackParams = {
};
CaptionScreen: {
title?: string;
- image?: {
- filename: string;
- path: string;
- };
+ media?: {uri: string; isVideo: boolean};
screenType: ScreenType;
selectedTags?: MomentTagType[];
moment?: MomentType;
};
TagFriendsScreen: {
- imagePath: string;
+ media: {
+ uri: string;
+ isVideo: boolean;
+ };
selectedTags?: MomentTagType[];
};
TagSelectionScreen: {
@@ -115,7 +114,7 @@ export type MainStackParams = {
Chat: undefined;
NewChatModal: undefined;
ZoomInCropper: {
- image: Image;
+ media: {uri: string; isVideo: boolean};
screenType: ScreenType;
title: string;
};
diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx
index e1ad8aa9..e19df2c2 100644
--- a/src/routes/main/MainStackScreen.tsx
+++ b/src/routes/main/MainStackScreen.tsx
@@ -1,8 +1,8 @@
-import { RouteProp } from '@react-navigation/native';
-import { StackNavigationOptions } from '@react-navigation/stack';
+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 {StyleSheet, Text} from 'react-native';
+import {normalize} from 'react-native-elements';
import BackIcon from '../../assets/icons/back-arrow.svg';
import {
AccountType,
@@ -37,10 +37,10 @@ import {
CameraScreen,
} from '../../screens';
import MutualBadgeHolders from '../../screens/suggestedPeople/MutualBadgeHolders';
-import { ScreenType } from '../../types';
-import { AvatarHeaderHeight, ChatHeaderHeight, SCREEN_WIDTH } from '../../utils';
-import { MainStack, MainStackParams } from './MainStackNavigator';
-import { ZoomInCropper } from '../../components/comments/ZoomInCropper';
+import {ScreenType} from '../../types';
+import {AvatarHeaderHeight, ChatHeaderHeight, SCREEN_WIDTH} from '../../utils';
+import {MainStack, MainStackParams} from './MainStackNavigator';
+import {ZoomInCropper} from '../../components/comments/ZoomInCropper';
/**
* 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.
@@ -57,8 +57,8 @@ type MainStackRouteProps = RouteProp<MainStackParams, 'Profile'>;
interface MainStackProps {
route: MainStackRouteProps;
}
-const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
- const { screenType } = route.params;
+const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
+ const {screenType} = route.params;
// const isSearchTab = screenType === ScreenType.Search;
const isNotificationsTab = screenType === ScreenType.Notifications;
@@ -84,10 +84,10 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
})();
const tutorialModalStyle: StackNavigationOptions = {
- cardStyle: { backgroundColor: 'rgba(0, 0, 0, 0.5)' },
+ cardStyle: {backgroundColor: 'rgba(0, 0, 0, 0.5)'},
gestureDirection: 'vertical',
cardOverlayEnabled: true,
- cardStyleInterpolator: ({ current: { progress } }) => ({
+ cardStyleInterpolator: ({current: {progress}}) => ({
cardStyle: {
opacity: progress.interpolate({
inputRange: [0, 0.5, 0.9, 1],
@@ -98,7 +98,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
};
const newChatModalStyle: StackNavigationOptions = {
- cardStyle: { backgroundColor: 'rgba(0, 0, 0, 0.5)' },
+ cardStyle: {backgroundColor: 'rgba(0, 0, 0, 0.5)'},
cardOverlayEnabled: true,
animationEnabled: false,
};
@@ -121,14 +121,14 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Navigator
screenOptions={{
headerShown: false,
- gestureResponseDistance: { horizontal: SCREEN_WIDTH * 0.6 },
+ gestureResponseDistance: {horizontal: SCREEN_WIDTH * 0.6},
}}
mode="card"
initialRouteName={initialRouteName}>
<MainStack.Screen
name="Profile"
component={ProfileScreen}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
options={{
...headerBarOptions('white', ''),
}}
@@ -137,21 +137,21 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="SuggestedPeople"
component={SuggestedPeopleScreen}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
/>
)}
{isNotificationsTab && (
<MainStack.Screen
name="Notifications"
component={NotificationsScreen}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
/>
)}
{isUploadTab && (
<MainStack.Screen
name="Upload"
component={CameraScreen}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
/>
)}
<MainStack.Screen
@@ -188,7 +188,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
options={{
...tutorialModalStyle,
}}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
/>
<MainStack.Screen
name="CaptionScreen"
@@ -201,10 +201,10 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="SocialMediaTaggs"
component={SocialMediaTaggs}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
options={{
...headerBarOptions('white', ''),
- headerStyle: { height: AvatarHeaderHeight },
+ headerStyle: {height: AvatarHeaderHeight},
}}
/>
<MainStack.Screen
@@ -224,7 +224,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="IndividualMoment"
component={IndividualMoment}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
options={{
...modalStyle,
gestureEnabled: false,
@@ -234,7 +234,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="MomentCommentsScreen"
component={MomentCommentsScreen}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
options={{
...headerBarOptions('black', 'Comments'),
}}
@@ -249,7 +249,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="MomentUploadPrompt"
component={MomentUploadPromptScreen}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
options={{
...modalStyle,
}}
@@ -257,7 +257,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="FriendsListScreen"
component={FriendsListScreen}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
options={{
...headerBarOptions('black', 'Friends'),
}}
@@ -272,7 +272,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="RequestContactsAccess"
component={RequestContactsAccess}
- initialParams={{ screenType }}
+ initialParams={{screenType}}
options={{
...modalStyle,
gestureEnabled: false,
@@ -288,7 +288,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="UpdateSPPicture"
component={SuggestedPeopleUploadPictureScreen}
- initialParams={{ editing: true }}
+ initialParams={{editing: true}}
options={{
...headerBarOptions('white', ''),
}}
@@ -296,7 +296,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="BadgeSelection"
component={BadgeSelection}
- initialParams={{ editing: true }}
+ initialParams={{editing: true}}
options={{
...headerBarOptions('white', ''),
}}
@@ -304,7 +304,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="MutualBadgeHolders"
component={MutualBadgeHolders}
- options={{ ...modalStyle }}
+ options={{...modalStyle}}
/>
<MainStack.Screen
name="SPWelcomeScreen"
@@ -316,20 +316,20 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
<MainStack.Screen
name="ChatList"
component={ChatListScreen}
- options={{ headerTitle: 'Chats' }}
+ options={{headerTitle: 'Chats'}}
/>
<MainStack.Screen
name="Chat"
component={ChatScreen}
options={{
...headerBarOptions('black', ''),
- headerStyle: { height: ChatHeaderHeight },
+ headerStyle: {height: ChatHeaderHeight},
}}
/>
<MainStack.Screen
name="NewChatModal"
component={NewChatModal}
- options={{ headerShown: false, ...newChatModalStyle }}
+ options={{headerShown: false, ...newChatModalStyle}}
/>
<MainStack.Screen
name="TagSelectionScreen"
@@ -349,6 +349,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
name="ZoomInCropper"
component={ZoomInCropper}
options={{
+ ...modalStyle,
gestureEnabled: false,
}}
/>
@@ -356,6 +357,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({ route }) => {
name="CameraScreen"
component={CameraScreen}
options={{
+ ...modalStyle,
gestureEnabled: false,
}}
/>
@@ -388,8 +390,8 @@ export const headerBarOptions: (
<Text
style={[
styles.headerTitle,
- { color: color },
- { fontSize: title.length > 18 ? normalize(14) : normalize(16) },
+ {color: color},
+ {fontSize: title.length > 18 ? normalize(14) : normalize(16)},
]}>
{title}
</Text>
@@ -397,10 +399,10 @@ export const headerBarOptions: (
});
export const modalStyle: StackNavigationOptions = {
- cardStyle: { backgroundColor: 'rgba(80,80,80,0.6)' },
+ cardStyle: {backgroundColor: 'rgba(80,80,80,0.6)'},
gestureDirection: 'vertical',
cardOverlayEnabled: true,
- cardStyleInterpolator: ({ current: { progress } }) => ({
+ cardStyleInterpolator: ({current: {progress}}) => ({
cardStyle: {
opacity: progress.interpolate({
inputRange: [0, 0.5, 0.9, 1],
@@ -418,24 +420,12 @@ const styles = StyleSheet.create({
shadowColor: 'black',
shadowRadius: 3,
shadowOpacity: 0.7,
- shadowOffset: { width: 0, height: 0 },
+ shadowOffset: {width: 0, height: 0},
},
headerTitle: {
letterSpacing: normalize(1.3),
fontWeight: '700',
},
- whiteHeaderTitle: {
- fontSize: normalize(16),
- letterSpacing: normalize(1.3),
- fontWeight: '700',
- color: 'white',
- },
- blackHeaderTitle: {
- fontSize: normalize(16),
- letterSpacing: normalize(1.3),
- fontWeight: '700',
- color: 'black',
- },
});
export default MainStackScreen;
diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx
index f8b94470..789cbcac 100644
--- a/src/routes/tabs/NavigationBar.tsx
+++ b/src/routes/tabs/NavigationBar.tsx
@@ -1,23 +1,23 @@
-import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
-import React, { Fragment, useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
-import { NavigationIcon } from '../../components';
-import { NO_NOTIFICATIONS } from '../../store/initialStates';
-import { RootState } from '../../store/rootReducer';
-import { setNotificationsReadDate } from '../../services';
-import { ScreenType } from '../../types';
-import { haveUnreadNotifications } from '../../utils';
+import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
+import React, {Fragment, useEffect, useState} from 'react';
+import {useSelector} from 'react-redux';
+import {NavigationIcon} from '../../components';
+import {NO_NOTIFICATIONS} from '../../store/initialStates';
+import {RootState} from '../../store/rootReducer';
+import {setNotificationsReadDate} from '../../services';
+import {ScreenType} from '../../types';
+import {haveUnreadNotifications} from '../../utils';
import MainStackScreen from '../main/MainStackScreen';
-import { NotificationPill } from '../../components/notifications';
+import {NotificationPill} from '../../components/notifications';
const Tabs = createBottomTabNavigator();
const NavigationBar: React.FC = () => {
- const { isOnboardedUser, newNotificationReceived } = useSelector(
+ const {isOnboardedUser, newNotificationReceived} = useSelector(
(state: RootState) => state.user,
);
- const { notifications: { notifications } = NO_NOTIFICATIONS } = useSelector(
+ const {notifications: {notifications} = NO_NOTIFICATIONS} = useSelector(
(state: RootState) => state,
);
// Triggered if user clicks on Notifications page to close the pill
@@ -41,15 +41,21 @@ const NavigationBar: React.FC = () => {
<>
<NotificationPill showIcon={showIcon} />
<Tabs.Navigator
- screenOptions={({ route }) => ({
- tabBarIcon: ({ focused }) => {
+ screenOptions={({route}) => ({
+ tabBarIcon: ({focused}) => {
switch (route.name) {
case 'Home':
return <NavigationIcon tab="Home" disabled={!focused} />;
case 'Chat':
return <NavigationIcon tab="Chat" disabled={!focused} />;
case 'Upload':
- return <NavigationIcon tab="Upload" disabled={!focused} />;
+ return (
+ <NavigationIcon
+ tab="Upload"
+ disabled={!focused}
+ isBigger={true}
+ />
+ );
case 'Notifications':
return (
<NavigationIcon
@@ -86,22 +92,27 @@ const NavigationBar: React.FC = () => {
<Tabs.Screen
name="SuggestedPeople"
component={MainStackScreen}
- initialParams={{ screenType: ScreenType.SuggestedPeople }}
+ initialParams={{screenType: ScreenType.SuggestedPeople}}
+ />
+ <Tabs.Screen
+ name="Chat"
+ component={MainStackScreen}
+ initialParams={{screenType: ScreenType.Chat}}
/>
<Tabs.Screen
name="Chat"
component={MainStackScreen}
- initialParams={{ screenType: ScreenType.Chat }}
+ initialParams={{screenType: ScreenType.Chat}}
/>
<Tabs.Screen
name="Upload"
component={MainStackScreen}
- initialParams={{ screenType: ScreenType.Upload }}
+ initialParams={{screenType: ScreenType.Upload}}
/>
<Tabs.Screen
name="Notifications"
component={MainStackScreen}
- initialParams={{ screenType: ScreenType.Notifications }}
+ initialParams={{screenType: ScreenType.Notifications}}
listeners={{
tabPress: (_) => {
// Closes the pill once this screen has been opened
@@ -114,7 +125,7 @@ const NavigationBar: React.FC = () => {
<Tabs.Screen
name="Profile"
component={MainStackScreen}
- initialParams={{ screenType: ScreenType.Profile }}
+ initialParams={{screenType: ScreenType.Profile}}
/>
</Tabs.Navigator>
</>
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 e18679dc..28e8808f 100644
--- a/src/screens/profile/CaptionScreen.tsx
+++ b/src/screens/profile/CaptionScreen.tsx
@@ -1,6 +1,6 @@
-import { RouteProp } from '@react-navigation/native';
-import { StackNavigationProp } from '@react-navigation/stack';
-import React, { Fragment, useEffect, useState } from 'react';
+import {RouteProp} from '@react-navigation/native';
+import {StackNavigationProp} from '@react-navigation/stack';
+import React, {Fragment, useEffect, useState} from 'react';
import {
Alert,
Image,
@@ -13,29 +13,36 @@ import {
TouchableWithoutFeedback,
View,
} from 'react-native';
-import { MentionInputControlled } from '../../components';
-import { Button, normalize } from 'react-native-elements';
-import { useDispatch, useSelector } from 'react-redux';
+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';
-import { CaptionScreenHeader } from '../../components/';
+import {SearchBackground} from '../../components';
+import {CaptionScreenHeader} from '../../components/';
import TaggLoadingIndicator from '../../components/common/TaggLoadingIndicator';
-import { TAGG_LIGHT_BLUE_2 } from '../../constants';
+import {TAGG_LIGHT_BLUE_2} from '../../constants';
import {
ERROR_SOMETHING_WENT_WRONG_REFRESH,
ERROR_UPLOAD,
SUCCESS_PIC_UPLOAD,
} from '../../constants/strings';
-import { MainStackParams } from '../../routes';
-import { patchMoment, postMoment, postMomentTags } from '../../services';
+import {MainStackParams} from '../../routes';
+import {
+ handlePresignedURL,
+ handleVideoUpload,
+ patchMoment,
+ postMoment,
+ postMomentTags,
+} from '../../services';
import {
loadUserMoments,
updateProfileCompletionStage,
} from '../../store/actions';
-import { RootState } from '../../store/rootReducer';
-import { MomentTagType } from '../../types';
-import { SCREEN_WIDTH, StatusBarHeight } from '../../utils';
-import { mentionPartTypes } from '../../utils/comments';
+import {RootState} from '../../store/rootReducer';
+import {MomentTagType} from '../../types';
+import {SCREEN_WIDTH, StatusBarHeight} from '../../utils';
+import {mentionPartTypes} from '../../utils/comments';
/**
* Upload Screen to allow users to upload posts to Tagg
@@ -50,10 +57,10 @@ interface CaptionScreenProps {
navigation: CaptionScreenNavigationProp;
}
-const CaptionScreen: React.FC<CaptionScreenProps> = ({ route, navigation }) => {
- const { title, image, screenType, selectedTags, moment } = route.params;
+const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
+ const {title, screenType, selectedTags, moment} = route.params;
const {
- user: { userId },
+ user: {userId},
} = useSelector((state: RootState) => state.user);
const dispatch = useDispatch();
const [caption, setCaption] = useState(moment ? moment.caption : '');
@@ -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(
@@ -188,19 +219,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....."
@@ -212,11 +250,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,
})
}
@@ -259,7 +296,7 @@ const styles = StyleSheet.create({
header: {
marginVertical: 20,
},
- image: {
+ media: {
position: 'relative',
width: SCREEN_WIDTH,
aspectRatio: 1,
@@ -298,7 +335,7 @@ const styles = StyleSheet.create({
letterSpacing: normalize(0.3),
textAlign: 'right',
},
- tagIcon: { width: 20, height: 20 },
+ tagIcon: {width: 20, height: 20},
});
export default CaptionScreen;
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={
diff --git a/src/services/MomentService.ts b/src/services/MomentService.ts
index b837585a..0c93876a 100644
--- a/src/services/MomentService.ts
+++ b/src/services/MomentService.ts
@@ -5,12 +5,17 @@ import {
MOMENTTAG_ENDPOINT,
MOMENT_TAGS_ENDPOINT,
MOMENT_THUMBNAIL_ENDPOINT,
+ PRESIGNED_URL_ENDPOINT,
+ TAGG_CUSTOMER_SUPPORT,
} from '../constants';
-import {MomentPostType, MomentTagType} from '../types';
+import {
+ ERROR_SOMETHING_WENT_WRONG,
+ ERROR_SOMETHING_WENT_WRONG_REFRESH,
+} from '../constants/strings';
+import {MomentPostType, MomentTagType, PresignedURLResponse} from '../types';
import {checkImageUploadStatus} from '../utils';
export const postMoment = async (
- fileName: string,
uri: string,
caption: string,
category: string,
@@ -18,13 +23,10 @@ export const postMoment = async (
) => {
try {
const request = new FormData();
- //Manipulating filename to end with .jpg instead of .heic
- if (fileName.endsWith('.heic') || fileName.endsWith('.HEIC')) {
- fileName = fileName.split('.')[0] + '.jpg';
- }
+
request.append('image', {
uri: uri,
- name: fileName,
+ name: 'moment.jpg',
type: 'image/jpg',
});
request.append('moment', category);
@@ -208,3 +210,108 @@ export const deleteMomentTag = async (moment_tag_id: string) => {
return false;
}
};
+/**
+ * This function makes a request to the server in order to provide the client with a presigned URL.
+ * This is called first, in order for the client to directly upload a file to S3
+ * @param value: string | undefined
+ * @returns a PresignedURLResponse object
+ */
+export const handlePresignedURL = async (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);
+ const filename = `pc_${randHash}.mov`;
+ const token = await AsyncStorage.getItem('token');
+ const response = await fetch(PRESIGNED_URL_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ body: JSON.stringify({
+ filename,
+ category: momentCategory,
+ }),
+ });
+ const status = response.status;
+ let data: PresignedURLResponse = await response.json();
+ if (status === 200) {
+ return data;
+ } else {
+ if (status === 404) {
+ console.log(
+ `Please make sure that the email / username entered is registered with us. You may contact our customer support at ${TAGG_CUSTOMER_SUPPORT}`,
+ );
+ } else {
+ console.log(ERROR_SOMETHING_WENT_WRONG_REFRESH);
+ }
+ console.log(response);
+ }
+ } catch (error) {
+ console.log(error);
+ console.log(ERROR_SOMETHING_WENT_WRONG);
+ }
+};
+/**
+ * This util function takes in the file object and the PresignedURLResponse object, creates form data from the latter,
+ * and makes a post request to the presigned URL, sending the file object inside of the form data.
+ * @param filePath: the path to the file, including filename
+ * @param urlObj PresignedURLResponse | undefined
+ * @returns responseURL or boolean
+ */
+export const handleVideoUpload = async (
+ filePath: string,
+ urlObj: PresignedURLResponse | undefined,
+) => {
+ try {
+ if (urlObj === null || urlObj === undefined) {
+ console.log('Invalid urlObj');
+ return false;
+ }
+ const form = new FormData();
+ form.append('key', urlObj.response_url.fields.key);
+ form.append(
+ 'x-amz-algorithm',
+ urlObj.response_url.fields['x-amz-algorithm'],
+ );
+ form.append(
+ 'x-amz-credential',
+ urlObj.response_url.fields['x-amz-credential'],
+ );
+ form.append('x-amz-date', urlObj.response_url.fields['x-amz-date']);
+ form.append('policy', urlObj.response_url.fields.policy);
+ form.append(
+ 'x-amz-signature',
+ urlObj.response_url.fields['x-amz-signature'],
+ );
+ form.append('file', {
+ uri: filePath,
+ // other types such as 'quicktime' 'image' etc exist, and we can programmatically type this, but for now sticking with simple 'video'
+ type: 'video',
+ name: urlObj.response_url.fields.key,
+ });
+ const response = await fetch(urlObj.response_url.url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ body: form,
+ });
+ const status = response.status;
+ if (status === 200 || status === 204) {
+ console.log('complete');
+ return true;
+ } else {
+ if (status === 404) {
+ console.log(
+ `Please make sure that the email / username entered is registered with us. You may contact our customer support at ${TAGG_CUSTOMER_SUPPORT}`,
+ );
+ } else {
+ console.log(ERROR_SOMETHING_WENT_WRONG_REFRESH);
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ console.log(ERROR_SOMETHING_WENT_WRONG);
+ }
+ return false;
+};
diff --git a/src/types/types.ts b/src/types/types.ts
index 74c35703..5f70d1f8 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -1,6 +1,6 @@
-import { ImageSourcePropType } from 'react-native';
+import {ImageSourcePropType} from 'react-native';
import Animated from 'react-native-reanimated';
-import { Channel as ChannelType, StreamChat } from 'stream-chat';
+import {Channel as ChannelType, StreamChat} from 'stream-chat';
export interface UserType {
userId: string;
@@ -171,7 +171,7 @@ export enum ScreenType {
Notifications,
SuggestedPeople,
Chat,
- Upload
+ Upload,
}
/**
@@ -237,10 +237,10 @@ export type NotificationType = {
verbage: string;
notification_type: TypeOfNotification;
notification_object:
- | CommentNotificationType
- | ThreadNotificationType
- | MomentType
- | undefined;
+ | CommentNotificationType
+ | ThreadNotificationType
+ | MomentType
+ | undefined;
timestamp: string;
unread: boolean;
};
@@ -344,14 +344,14 @@ export type ChatContextType = {
setChannel: React.Dispatch<
React.SetStateAction<
| ChannelType<
- LocalAttachmentType,
- LocalChannelType,
- LocalCommandType,
- LocalEventType,
- LocalMessageType,
- LocalResponseType,
- LocalUserType
- >
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalResponseType,
+ LocalUserType
+ >
| undefined
>
>;
@@ -366,3 +366,19 @@ export type ReactionType = {
id: string;
type: ReactionOptionsType;
};
+// used to handle direct S3 uploads by packaging presigned_url info into one object
+export type PresignedURLResponse = {
+ response_msg: string;
+ response_url: {
+ url: string;
+ fields: {
+ key: string | undefined;
+ 'x-amz-algorithm': string;
+ 'x-amz-credential': string;
+ 'x-amz-date': string;
+ policy: string;
+ 'x-amz-signature': string;
+ };
+ };
+ moment_id: string;
+};
diff --git a/src/utils/camera.ts b/src/utils/camera.ts
index 73461ad7..3937129a 100644
--- a/src/utils/camera.ts
+++ b/src/utils/camera.ts
@@ -1,43 +1,41 @@
import CameraRoll from '@react-native-community/cameraroll';
-import {Dispatch, RefObject, SetStateAction} from 'react';
+import {RefObject} from 'react';
import {Alert} from 'react-native';
-import {RNCamera} from 'react-native-camera';
-import ImagePicker from 'react-native-image-crop-picker';
-import {ScreenType} from 'src/types';
+import {
+ RNCamera,
+ TakePictureOptions,
+ TakePictureResponse,
+} from 'react-native-camera';
+import ImagePicker, {Image, Video} from 'react-native-image-crop-picker';
import {ERROR_UPLOAD} from '../constants/strings';
/*
- * Captures a photo and pauses to shoe the preview of the picture taken
+ * Captures a photo and pauses to show the preview of the picture taken
*/
export const takePicture = (
cameraRef: RefObject<RNCamera>,
- setShowSaveButton: Dispatch<SetStateAction<boolean>>,
- setCapturedImage: Dispatch<SetStateAction<string>>,
+ callback: (pic: TakePictureResponse) => void,
) => {
if (cameraRef !== null) {
cameraRef.current?.pausePreview();
- const options = {
+ const options: TakePictureOptions = {
forceUpOrientation: true,
+ orientation: 'portrait',
writeExif: false,
};
- cameraRef.current?.takePictureAsync(options).then((response) => {
- setShowSaveButton(true);
- setCapturedImage(response.uri);
+ cameraRef.current?.takePictureAsync(options).then((pic) => {
+ callback(pic);
});
}
};
-export const downloadImage = (capturedImageURI: string) => {
+export const saveImageToGallery = (capturedImageURI: string) => {
CameraRoll.save(capturedImageURI, {album: 'Recents', type: 'photo'})
.then((_res) => Alert.alert('Saved to device!'))
.catch((_err) => Alert.alert('Failed to save to device!'));
};
-export const navigateToImagePicker = (
- navigation: any,
- screenType: ScreenType,
- title: string,
-) => {
+export const navigateToImagePicker = (callback: (pic: Image) => void) => {
ImagePicker.openPicker({
smartAlbums: [
'Favorites',
@@ -48,13 +46,23 @@ export const navigateToImagePicker = (
],
mediaType: 'photo',
})
- .then((picture) => {
- if ('path' in picture) {
- navigation.navigate('ZoomInCropper', {
- screenType,
- title,
- image: picture,
- });
+ .then((pic) => {
+ callback(pic);
+ })
+ .catch((err) => {
+ if (err.code && err.code !== 'E_PICKER_CANCELLED') {
+ Alert.alert(ERROR_UPLOAD);
+ }
+ });
+};
+
+export const navigateToVideoPicker = (callback: (vid: Video) => void) => {
+ ImagePicker.openPicker({
+ mediaType: 'video',
+ })
+ .then(async (vid) => {
+ if (vid.path) {
+ callback(vid);
}
})
.catch((err) => {
@@ -63,3 +71,28 @@ export const navigateToImagePicker = (
}
});
};
+
+export const showGIFFailureAlert = (onSuccess: () => void) =>
+ Alert.alert(
+ 'Warning',
+ 'The app currently cannot handle GIFs, and will only save a static image.',
+ [
+ {
+ text: 'Cancel',
+ onPress: () => {},
+ style: 'cancel',
+ },
+ {
+ text: 'Post',
+ onPress: onSuccess,
+ style: 'default',
+ },
+ ],
+ {
+ cancelable: true,
+ onDismiss: () =>
+ Alert.alert(
+ 'This alert was dismissed by tapping outside of the alert dialog.',
+ ),
+ },
+ );