aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBrian Kim <brian@tagg.id>2021-06-15 17:15:43 +0900
committerBrian Kim <brian@tagg.id>2021-06-15 17:15:43 +0900
commit4a422e43c6a6deaeff5d8fcc692138454653e4b9 (patch)
tree2e86ab879ea35e879581eb64be955d0e5481ff80 /src
parentc57b4959c90cec90dd0936f75a9086a4430b66b1 (diff)
parentdb0678d647f774dcb1cd60513985d9b6fbd0e28b (diff)
Merge with master
Diffstat (limited to 'src')
-rw-r--r--src/App.tsx19
-rw-r--r--src/assets/images/Group 479.jpgbin0 -> 753 bytes
-rw-r--r--src/assets/images/Group 479.svg5
-rw-r--r--src/assets/images/Profile Icon.pngbin0 -> 1504 bytes
-rw-r--r--src/assets/images/pill-icon-1.pngbin0 -> 868 bytes
-rw-r--r--src/assets/images/pill-icon-2.pngbin0 -> 1276 bytes
-rw-r--r--src/assets/images/pill-icon-3.pngbin0 -> 1037 bytes
-rw-r--r--src/assets/images/pill-icon-4.pngbin0 -> 1249 bytes
-rw-r--r--src/assets/images/purple-tip.pngbin0 -> 680 bytes
-rw-r--r--src/components/comments/CommentsCount.tsx58
-rw-r--r--src/components/comments/index.ts1
-rw-r--r--src/components/common/BottomDrawer.tsx2
-rw-r--r--src/components/common/GenericMoreInfoDrawer.tsx14
-rw-r--r--src/components/moments/IndividualMomentTitleBar.tsx12
-rw-r--r--src/components/moments/MomentCommentPreview.tsx99
-rw-r--r--src/components/moments/MomentPost.tsx67
-rw-r--r--src/components/moments/MomentPostContent.tsx157
-rw-r--r--src/components/moments/MomentPostHeader.tsx15
-rw-r--r--src/components/notifications/Notification.tsx11
-rw-r--r--src/components/notifications/NotificationPill.tsx209
-rw-r--r--src/components/notifications/index.ts1
-rw-r--r--src/components/profile/MomentMoreInfoDrawer.tsx90
-rw-r--r--src/components/profile/ProfileMoreInfoDrawer.tsx5
-rw-r--r--src/components/suggestedPeople/legacy/BadgesDropdown.tsx6
-rw-r--r--src/constants/api.ts2
-rw-r--r--src/constants/constants.ts3
-rw-r--r--src/constants/regex.ts2
-rw-r--r--src/routes/main/MainStackNavigator.tsx8
-rw-r--r--src/routes/main/MainStackScreen.tsx1
-rw-r--r--src/routes/tabs/NavigationBar.tsx159
-rw-r--r--src/screens/main/NotificationsScreen.tsx6
-rw-r--r--src/screens/moments/TagFriendsScreen.tsx4
-rw-r--r--src/screens/onboarding/BasicInfoOnboarding.tsx6
-rw-r--r--src/screens/profile/CaptionScreen.tsx97
-rw-r--r--src/screens/profile/EditProfile.tsx15
-rw-r--r--src/screens/profile/IndividualMoment.tsx135
-rw-r--r--src/screens/profile/InviteFriendsScreen.tsx56
-rw-r--r--src/screens/profile/MomentCommentsScreen.tsx5
-rw-r--r--src/services/MomentService.ts53
-rw-r--r--src/services/NotificationService.ts63
-rw-r--r--src/store/initialStates.ts11
-rw-r--r--src/types/types.ts16
-rw-r--r--src/utils/comments.tsx35
-rw-r--r--src/utils/common.ts5
-rw-r--r--src/utils/moments.ts8
-rw-r--r--src/utils/users.ts18
46 files changed, 996 insertions, 483 deletions
diff --git a/src/App.tsx b/src/App.tsx
index 92e7abee..64f40bae 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -27,16 +27,15 @@ export const ChatContext = React.createContext({} as ChatContextType);
const App = () => {
const routeNameRef = useRef();
const [channel, setChannel] = useState<ChannelGroupedType>();
- const chatClient =
- StreamChat.getInstance<
- LocalAttachmentType,
- LocalChannelType,
- LocalCommandType,
- LocalEventType,
- LocalMessageType,
- LocalResponseType,
- LocalUserType
- >(STREAM_CHAT_API);
+ const chatClient = StreamChat.getInstance<
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalResponseType,
+ LocalUserType
+ >(STREAM_CHAT_API);
return (
<Provider store={store}>
<NavigationContainer
diff --git a/src/assets/images/Group 479.jpg b/src/assets/images/Group 479.jpg
new file mode 100644
index 00000000..74abad92
--- /dev/null
+++ b/src/assets/images/Group 479.jpg
Binary files differ
diff --git a/src/assets/images/Group 479.svg b/src/assets/images/Group 479.svg
new file mode 100644
index 00000000..4e1eee01
--- /dev/null
+++ b/src/assets/images/Group 479.svg
@@ -0,0 +1,5 @@
+<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<ellipse cx="7.75104" cy="5.91915" rx="2.00104" ry="1.93282" fill="white"/>
+<path d="M7.75195 8.0791C5.67981 8.0791 4 9.6062 4 11.49H11.5039C11.5039 9.6062 9.8241 8.0791 7.75195 8.0791Z" fill="white"/>
+<path d="M7.99349 1C4.13109 1 1 4.13109 1 7.99349C1 11.1633 3.10881 13.8405 6 14.6987L8 17L9.98697 14.6987C12.8782 13.8405 14.987 11.1633 14.987 7.99349C14.987 4.13109 11.8559 1 7.99349 1Z" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
diff --git a/src/assets/images/Profile Icon.png b/src/assets/images/Profile Icon.png
new file mode 100644
index 00000000..f8eae388
--- /dev/null
+++ b/src/assets/images/Profile Icon.png
Binary files differ
diff --git a/src/assets/images/pill-icon-1.png b/src/assets/images/pill-icon-1.png
new file mode 100644
index 00000000..06956c6a
--- /dev/null
+++ b/src/assets/images/pill-icon-1.png
Binary files differ
diff --git a/src/assets/images/pill-icon-2.png b/src/assets/images/pill-icon-2.png
new file mode 100644
index 00000000..b2370b80
--- /dev/null
+++ b/src/assets/images/pill-icon-2.png
Binary files differ
diff --git a/src/assets/images/pill-icon-3.png b/src/assets/images/pill-icon-3.png
new file mode 100644
index 00000000..6cdf0b15
--- /dev/null
+++ b/src/assets/images/pill-icon-3.png
Binary files differ
diff --git a/src/assets/images/pill-icon-4.png b/src/assets/images/pill-icon-4.png
new file mode 100644
index 00000000..6e132647
--- /dev/null
+++ b/src/assets/images/pill-icon-4.png
Binary files differ
diff --git a/src/assets/images/purple-tip.png b/src/assets/images/purple-tip.png
new file mode 100644
index 00000000..27f5a89a
--- /dev/null
+++ b/src/assets/images/purple-tip.png
Binary files differ
diff --git a/src/components/comments/CommentsCount.tsx b/src/components/comments/CommentsCount.tsx
deleted file mode 100644
index f4f8197d..00000000
--- a/src/components/comments/CommentsCount.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import {useNavigation} from '@react-navigation/native';
-import * as React from 'react';
-import {StyleSheet, TouchableOpacity} from 'react-native';
-import {Text} from 'react-native-animatable';
-import CommentIcon from '../../assets/icons/moment-comment-icon.svg';
-import {ScreenType} from '../../types';
-
-/**
- * Provides a view for the comment icon and the comment count.
- * When the user clicks on this view, a new screen opens to display all the comments.
- */
-
-type CommentsCountProps = {
- commentsCount: string;
- momentId: string;
- screenType: ScreenType;
-};
-
-const CommentsCount: React.FC<CommentsCountProps> = ({
- commentsCount,
- momentId,
- screenType,
-}) => {
- const navigation = useNavigation();
- const navigateToCommentsScreen = async () => {
- navigation.push('MomentCommentsScreen', {
- moment_id: momentId,
- screenType,
- });
- };
- return (
- <>
- <TouchableOpacity onPress={navigateToCommentsScreen}>
- <CommentIcon style={styles.image} />
- <Text style={styles.count}>
- {commentsCount !== '0' ? commentsCount : ''}
- </Text>
- </TouchableOpacity>
- </>
- );
-};
-
-const styles = StyleSheet.create({
- image: {
- position: 'relative',
- width: 21,
- height: 21,
- },
- count: {
- position: 'relative',
- fontWeight: 'bold',
- color: 'white',
- paddingTop: '3%',
- textAlign: 'center',
- },
-});
-
-export default CommentsCount;
diff --git a/src/components/comments/index.ts b/src/components/comments/index.ts
index 653f594c..77334cb3 100644
--- a/src/components/comments/index.ts
+++ b/src/components/comments/index.ts
@@ -1,4 +1,3 @@
-export {default as CommentsCount} from '../comments/CommentsCount';
export {default as CommentTile} from './CommentTile';
export {default as AddComment} from './AddComment';
export {default as MentionInputControlled} from './MentionInputControlled';
diff --git a/src/components/common/BottomDrawer.tsx b/src/components/common/BottomDrawer.tsx
index 16e98690..b79b8820 100644
--- a/src/components/common/BottomDrawer.tsx
+++ b/src/components/common/BottomDrawer.tsx
@@ -23,7 +23,7 @@ const BottomDrawer: React.FC<BottomDrawerProps> = (props) => {
const {isOpen, setIsOpen, showHeader, initialSnapPosition} = props;
const drawerRef = useRef<BottomSheet>(null);
const [modalVisible, setModalVisible] = useState(isOpen);
- const bgAlpha = useValue(isOpen ? 1 : 0);
+ const bgAlpha = useValue(isOpen ? 0 : 1);
useEffect(() => {
if (isOpen) {
diff --git a/src/components/common/GenericMoreInfoDrawer.tsx b/src/components/common/GenericMoreInfoDrawer.tsx
index 0928ed44..cfc45131 100644
--- a/src/components/common/GenericMoreInfoDrawer.tsx
+++ b/src/components/common/GenericMoreInfoDrawer.tsx
@@ -3,15 +3,16 @@ import {
GestureResponderEvent,
StyleSheet,
Text,
+ TextStyle,
TouchableOpacity,
View,
ViewProps,
ViewStyle,
} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
-import BottomDrawer from './BottomDrawer';
import {TAGG_LIGHT_BLUE} from '../../constants';
import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+import BottomDrawer from './BottomDrawer';
// conforms the JSX onPress attribute type
type OnPressHandler = (event: GestureResponderEvent) => void;
@@ -20,13 +21,12 @@ interface GenericMoreInfoDrawerProps extends ViewProps {
isOpen: boolean;
setIsOpen: (visible: boolean) => void;
showIcons: boolean;
- textColor: string;
// An array of title, onPressHandler, and icon component
- buttons: [string, OnPressHandler, JSX.Element?][];
+ buttons: [string, OnPressHandler, JSX.Element?, TextStyle?][];
}
const GenericMoreInfoDrawer: React.FC<GenericMoreInfoDrawerProps> = (props) => {
- const {buttons, showIcons, textColor} = props;
+ const {buttons, showIcons} = props;
// each button is 80px high, cancel button is always there
const initialSnapPosition =
(buttons.length + 1) * 80 + useSafeAreaInsets().bottom;
@@ -44,13 +44,11 @@ const GenericMoreInfoDrawer: React.FC<GenericMoreInfoDrawerProps> = (props) => {
showHeader={false}
initialSnapPosition={initialSnapPosition}>
<View style={styles.panel}>
- {buttons.map(([title, action, icon], index) => (
+ {buttons.map(([title, action, icon, textStyle], index) => (
<View key={index}>
<TouchableOpacity style={panelButtonStyle} onPress={action}>
{showIcons && <View style={styles.icon}>{icon}</View>}
- <Text style={[styles.panelButtonTitle, {color: textColor}]}>
- {title}
- </Text>
+ <Text style={[styles.panelButtonTitle, textStyle]}>{title}</Text>
</TouchableOpacity>
<View style={styles.divider} />
</View>
diff --git a/src/components/moments/IndividualMomentTitleBar.tsx b/src/components/moments/IndividualMomentTitleBar.tsx
index 79453ade..4ae9471f 100644
--- a/src/components/moments/IndividualMomentTitleBar.tsx
+++ b/src/components/moments/IndividualMomentTitleBar.tsx
@@ -1,8 +1,13 @@
import React from 'react';
-import {TouchableOpacity} from 'react-native';
-import {Text, View, StyleSheet, ViewProps} from 'react-native';
-import {normalize} from '../../utils';
+import {
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+ ViewProps,
+} from 'react-native';
import CloseIcon from '../../assets/ionicons/close-outline.svg';
+import {normalize} from '../../utils';
interface IndividualMomentTitleBarProps extends ViewProps {
title: string;
@@ -30,7 +35,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
- height: '5%',
},
headerContainer: {
width: '80%',
diff --git a/src/components/moments/MomentCommentPreview.tsx b/src/components/moments/MomentCommentPreview.tsx
new file mode 100644
index 00000000..f6b9d75b
--- /dev/null
+++ b/src/components/moments/MomentCommentPreview.tsx
@@ -0,0 +1,99 @@
+import {useNavigation} from '@react-navigation/native';
+import React from 'react';
+import {Image, StyleSheet, Text, View} from 'react-native';
+import {TouchableOpacity} from 'react-native-gesture-handler';
+import {useDispatch, useStore} from 'react-redux';
+import {MomentCommentPreviewType, ScreenType, UserType} from '../../types';
+import {navigateToProfile, normalize} from '../../utils';
+import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments';
+
+interface MomentCommentPreviewProps {
+ momentId: string;
+ commentsCount: number | null;
+ commentPreview: MomentCommentPreviewType | null;
+ screenType: ScreenType;
+}
+
+const MomentCommentPreview: React.FC<MomentCommentPreviewProps> = ({
+ momentId,
+ commentsCount,
+ commentPreview,
+ screenType,
+}) => {
+ const navigation = useNavigation();
+ const state = useStore().getState();
+ const commentCountText =
+ !commentsCount || commentsCount === 0
+ ? 'No Comments'
+ : commentsCount + ' comments';
+
+ return (
+ <TouchableOpacity
+ style={styles.commentsPreviewContainer}
+ onPress={() =>
+ navigation.push('MomentCommentsScreen', {
+ moment_id: momentId,
+ screenType,
+ })
+ }>
+ <Text style={styles.whiteBold}>{commentCountText}</Text>
+ {commentPreview && (
+ <View style={styles.previewContainer}>
+ <Image
+ source={{
+ uri: commentPreview.commenter.thumbnail_url,
+ }}
+ style={styles.avatar}
+ />
+ <Text style={styles.whiteBold} numberOfLines={1}>
+ <Text> </Text>
+ <Text>{commentPreview.commenter.username}</Text>
+ <Text> </Text>
+ {renderTextWithMentions({
+ value: commentPreview.comment,
+ styles: styles.normalFont,
+ partTypes: mentionPartTypes('white'),
+ onPress: (user: UserType) =>
+ navigateToProfile(
+ state,
+ useDispatch,
+ navigation,
+ screenType,
+ user,
+ ),
+ })}
+ </Text>
+ </View>
+ )}
+ </TouchableOpacity>
+ );
+};
+
+const styles = StyleSheet.create({
+ commentsPreviewContainer: {
+ height: normalize(50),
+ flexDirection: 'column',
+ justifyContent: 'space-around',
+ marginHorizontal: '5%',
+ marginBottom: '2%',
+ },
+ whiteBold: {
+ fontWeight: '700',
+ color: 'white',
+ fontSize: normalize(13),
+ },
+ previewContainer: {
+ flexDirection: 'row',
+ width: '95%',
+ },
+ avatar: {
+ height: normalize(16),
+ width: normalize(16),
+ borderRadius: 99,
+ },
+ normalFont: {
+ fontWeight: 'normal',
+ },
+});
+
+export default MomentCommentPreview;
diff --git a/src/components/moments/MomentPost.tsx b/src/components/moments/MomentPost.tsx
index 7149a5b4..d87028e3 100644
--- a/src/components/moments/MomentPost.tsx
+++ b/src/components/moments/MomentPost.tsx
@@ -1,21 +1,25 @@
import React, {useEffect, useState} from 'react';
-import {StyleSheet, View} from 'react-native';
+import {StyleSheet} from 'react-native';
import {useSelector} from 'react-redux';
import {MomentPostContent, MomentPostHeader} from '.';
import {deleteMomentTag, loadMomentTags} from '../../services';
import {RootState} from '../../store/rootReducer';
-import {MomentTagType, MomentType, ScreenType} from '../../types';
-import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils';
+import {MomentPostType, MomentTagType, ScreenType} from '../../types';
+import {normalize, SCREEN_HEIGHT} from '../../utils';
interface MomentPostProps {
- item: MomentType;
+ moment: MomentPostType;
userXId: string | undefined;
screenType: ScreenType;
+ index: number;
}
-const ITEM_HEIGHT = SCREEN_HEIGHT * 0.9;
-
-const MomentPost: React.FC<MomentPostProps> = ({item, userXId, screenType}) => {
+const MomentPost: React.FC<MomentPostProps> = ({
+ moment,
+ userXId,
+ screenType,
+ index,
+}) => {
const {userId: loggedInUserId, username: loggedInUsername} = useSelector(
(state: RootState) => state.user.user,
);
@@ -30,16 +34,13 @@ const MomentPost: React.FC<MomentPostProps> = ({item, userXId, screenType}) => {
const isOwnProfile = username === loggedInUsername;
- const loadTags = async () => {
- const response = await loadMomentTags(item.moment_id);
- setTags(response ? response : []);
- };
-
/*
* Load tags on initial render to pass tags data to moment header and content
*/
useEffect(() => {
- loadTags();
+ loadMomentTags(moment.moment_id).then((response) => {
+ setTags(response ? response : []);
+ });
}, []);
/*
@@ -71,52 +72,34 @@ const MomentPost: React.FC<MomentPostProps> = ({item, userXId, screenType}) => {
};
return (
- <View style={styles.postContainer}>
+ <>
<MomentPostHeader
+ style={styles.postHeader}
userXId={userXId}
screenType={screenType}
username={isOwnProfile ? loggedInUsername : username}
- momentId={item.moment_id}
- style={styles.postHeader}
momentTagId={momentTagId}
removeTag={removeTag}
+ moment={moment}
+ tags={tags}
/>
<MomentPostContent
style={styles.postContent}
- momentId={item.moment_id}
- caption={item.caption}
- pathHash={item.moment_url}
- dateTime={item.date_created}
+ moment={moment}
screenType={screenType}
momentTags={tags}
+ index={index}
/>
- </View>
+ </>
);
};
const styles = StyleSheet.create({
- contentContainer: {
- width: SCREEN_WIDTH,
- height: SCREEN_HEIGHT,
- paddingTop: StatusBarHeight,
- flex: 1,
- paddingBottom: 0,
- },
- content: {
- flex: 9,
- },
- header: {
- flex: 1,
- },
- postContainer: {
- height: ITEM_HEIGHT,
- width: SCREEN_WIDTH,
- flex: 1,
- },
- postHeader: {
- flex: 1,
+ postHeader: {},
+ postContent: {
+ minHeight: SCREEN_HEIGHT * 0.8,
+ paddingBottom: normalize(20),
},
- postContent: {flex: 9},
});
export default MomentPost;
diff --git a/src/components/moments/MomentPostContent.tsx b/src/components/moments/MomentPostContent.tsx
index 4a1f3894..aca2999c 100644
--- a/src/components/moments/MomentPostContent.tsx
+++ b/src/components/moments/MomentPostContent.tsx
@@ -1,77 +1,81 @@
import {useNavigation} from '@react-navigation/native';
-import React, {useEffect, useRef, useState} from 'react';
+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, {Easing} from 'react-native-reanimated';
+import Animated, {EasingNode} from 'react-native-reanimated';
import {useDispatch, useStore} from 'react-redux';
-import {getCommentsCount} from '../../services';
+import {MomentContext} from '../../screens/profile/IndividualMoment';
import {RootState} from '../../store/rootReducer';
-import {MomentTagType, ScreenType, UserType} from '../../types';
import {
+ MomentCommentPreviewType,
+ MomentPostType,
+ MomentTagType,
+ ScreenType,
+ UserType,
+} from '../../types';
+import {
+ getLoggedInUserAsProfilePreview,
getTimePosted,
navigateToProfile,
normalize,
- SCREEN_HEIGHT,
SCREEN_WIDTH,
} from '../../utils';
import {mentionPartTypes, renderTextWithMentions} from '../../utils/comments';
-import {CommentsCount} from '../comments';
+import {AddComment} from '../comments';
import {MomentTags} from '../common';
+import MomentCommentPreview from './MomentCommentPreview';
interface MomentPostContentProps extends ViewProps {
screenType: ScreenType;
- momentId: string;
- caption: string;
- pathHash: string;
- dateTime: string;
+ moment: MomentPostType;
momentTags: MomentTagType[];
+ index: number;
}
const MomentPostContent: React.FC<MomentPostContentProps> = ({
screenType,
- momentId,
- caption,
- pathHash,
- dateTime,
+ moment,
style,
momentTags,
+ index,
}) => {
+ const [tags, setTags] = useState<MomentTagType[]>(momentTags);
const state: RootState = useStore().getState();
const navigation = useNavigation();
const dispatch = useDispatch();
- const [elapsedTime, setElapsedTime] = useState('');
- const [comments_count, setCommentsCount] = useState('');
- const [tags, setTags] = useState<MomentTagType[]>(momentTags);
const imageRef = useRef(null);
const [visible, setVisible] = useState(false);
-
const [fadeValue, setFadeValue] = useState<Animated.Value<number>>(
new Animated.Value(0),
);
+ const [commentCount, setCommentCount] = useState<number>(
+ moment.comments_count,
+ );
+ const [commentPreview, setCommentPreview] =
+ useState<MomentCommentPreviewType | null>(moment.comment_preview);
+ const {keyboardVisible, scrollTo} = useContext(MomentContext);
+ const [hideText, setHideText] = useState(false);
useEffect(() => {
setTags(momentTags);
}, [momentTags]);
useEffect(() => {
- const fetchCommentsCount = async () => {
- const count = await getCommentsCount(momentId, false);
- setCommentsCount(count);
- };
- setElapsedTime(getTimePosted(dateTime));
- fetchCommentsCount();
- }, [dateTime, momentId]);
-
- useEffect(() => {
const fade = async () => {
Animated.timing(fadeValue, {
toValue: 1,
duration: 250,
- easing: Easing.linear,
+ easing: EasingNode.linear,
}).start();
};
fade();
}, [fadeValue]);
+ useEffect(() => {
+ if (!keyboardVisible && hideText) {
+ setHideText(false);
+ }
+ }, [keyboardVisible, hideText]);
+
return (
<View style={[styles.container, style]}>
<TouchableWithoutFeedback
@@ -82,76 +86,95 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({
<Image
ref={imageRef}
style={styles.image}
- source={{uri: pathHash}}
+ source={{uri: moment.moment_url}}
resizeMode={'cover'}
/>
{tags.length > 0 && (
<Image
source={require('../../assets/icons/tag_indicate.png')}
- style={[styles.tagIcon]}
+ style={styles.tagIcon}
/>
)}
</TouchableWithoutFeedback>
{visible && (
<Animated.View style={[styles.tapTag, {opacity: fadeValue}]}>
- <MomentTags editing={false} tags={tags} imageRef={imageRef} />
+ <MomentTags
+ editing={false}
+ tags={tags}
+ setTags={() => null}
+ imageRef={imageRef}
+ />
</Animated.View>
)}
- <View style={styles.footerContainer}>
- <CommentsCount
- commentsCount={comments_count}
- momentId={momentId}
- screenType={screenType}
- />
- <Text style={styles.text}>{elapsedTime}</Text>
- </View>
- {renderTextWithMentions({
- value: caption,
- styles: styles.captionText,
- partTypes: mentionPartTypes('white'),
- onPress: (user: UserType) =>
- navigateToProfile(state, dispatch, navigation, screenType, user),
- })}
+ {!hideText && (
+ <>
+ {moment.caption !== '' &&
+ renderTextWithMentions({
+ value: moment.caption,
+ styles: styles.captionText,
+ partTypes: mentionPartTypes('white'),
+ onPress: (user: UserType) =>
+ navigateToProfile(
+ state,
+ dispatch,
+ navigation,
+ screenType,
+ user,
+ ),
+ })}
+ <MomentCommentPreview
+ momentId={moment.moment_id}
+ commentsCount={commentCount}
+ commentPreview={commentPreview}
+ screenType={screenType}
+ />
+ </>
+ )}
+ <AddComment
+ placeholderText={'Add a comment here!'}
+ momentId={moment.moment_id}
+ callback={(message) => {
+ setCommentPreview({
+ commenter: getLoggedInUserAsProfilePreview(state),
+ comment: message,
+ });
+ setCommentCount(commentCount + 1);
+ }}
+ onFocus={() => {
+ setHideText(true);
+ scrollTo(index);
+ }}
+ isKeyboardAvoiding={false}
+ theme={'dark'}
+ />
+ <Text style={styles.text}>{getTimePosted(moment.date_created)}</Text>
</View>
);
};
const styles = StyleSheet.create({
- container: {
- height: SCREEN_HEIGHT,
- },
+ container: {},
image: {
width: SCREEN_WIDTH,
aspectRatio: 1,
marginBottom: '3%',
},
- footerContainer: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginLeft: '7%',
- marginRight: '5%',
- marginBottom: '2%',
- },
text: {
- position: 'relative',
- paddingBottom: '1%',
- paddingTop: '1%',
- marginLeft: '7%',
- marginRight: '2%',
- color: '#ffffff',
- fontWeight: 'bold',
+ marginHorizontal: '5%',
+ color: 'white',
+ fontWeight: '500',
+ textAlign: 'right',
+ marginTop: 5,
},
captionText: {
position: 'relative',
- paddingBottom: '34%',
- paddingTop: '1%',
- marginLeft: '5%',
- marginRight: '5%',
+ marginHorizontal: '5%',
color: '#ffffff',
fontWeight: '500',
fontSize: normalize(13),
lineHeight: normalize(15.51),
letterSpacing: normalize(0.6),
+ marginBottom: normalize(18),
},
tapTag: {
position: 'absolute',
diff --git a/src/components/moments/MomentPostHeader.tsx b/src/components/moments/MomentPostHeader.tsx
index dc6a3cd9..5f26951a 100644
--- a/src/components/moments/MomentPostHeader.tsx
+++ b/src/components/moments/MomentPostHeader.tsx
@@ -10,7 +10,7 @@ import {
import {useDispatch, useSelector, useStore} from 'react-redux';
import {loadUserMoments} from '../../store/actions';
import {RootState} from '../../store/rootReducer';
-import {ScreenType} from '../../types';
+import {MomentTagType, MomentType, ScreenType} from '../../types';
import {fetchUserX, userXInStore} from '../../utils';
import {MomentMoreInfoDrawer} from '../profile';
import TaggAvatar from '../profile/TaggAvatar';
@@ -19,19 +19,21 @@ interface MomentPostHeaderProps extends ViewProps {
userXId?: string;
screenType: ScreenType;
username: string;
- momentId: string;
momentTagId: string;
removeTag: () => Promise<void>;
+ moment: MomentType;
+ tags: MomentTagType[];
}
const MomentPostHeader: React.FC<MomentPostHeaderProps> = ({
userXId,
screenType,
username,
- momentId,
style,
momentTagId,
removeTag,
+ moment,
+ tags,
}) => {
const [drawerVisible, setDrawerVisible] = useState(false);
const dispatch = useDispatch();
@@ -62,20 +64,23 @@ const MomentPostHeader: React.FC<MomentPostHeaderProps> = ({
style={styles.avatar}
userXId={userXId}
screenType={screenType}
+ editable={false}
/>
<Text style={styles.headerText}>{username}</Text>
</TouchableOpacity>
<MomentMoreInfoDrawer
isOpen={drawerVisible}
setIsOpen={setDrawerVisible}
- momentId={momentId}
isOwnProfile={isOwnProfile}
momentTagId={momentTagId}
removeTag={removeTag}
dismissScreenAndUpdate={() => {
dispatch(loadUserMoments(loggedInUserId));
- navigation.pop();
+ navigation.goBack();
}}
+ screenType={screenType}
+ moment={moment}
+ tags={tags}
/>
</View>
);
diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx
index 3f9cc56a..fd1b11ac 100644
--- a/src/components/notifications/Notification.tsx
+++ b/src/components/notifications/Notification.tsx
@@ -2,9 +2,7 @@ import {useNavigation} from '@react-navigation/native';
import React, {useEffect, useState} from 'react';
import {Alert, Image, StyleSheet, Text, View} from 'react-native';
import {TouchableWithoutFeedback} from 'react-native-gesture-handler';
-import LinearGradient from 'react-native-linear-gradient';
import {useDispatch, useStore} from 'react-redux';
-import {BACKGROUND_GRADIENT_MAP} from '../../constants';
import {ERROR_DELETED_OBJECT} from '../../constants/strings';
import {loadImageFromURL} from '../../services';
import {
@@ -49,7 +47,6 @@ const Notification: React.FC<NotificationProps> = (props) => {
verbage,
notification_type,
notification_object,
- unread,
timestamp,
},
screenType,
@@ -323,13 +320,7 @@ const Notification: React.FC<NotificationProps> = (props) => {
</View>
);
- return unread ? (
- <LinearGradient colors={BACKGROUND_GRADIENT_MAP[2]} useAngle angle={90}>
- {renderContent()}
- </LinearGradient>
- ) : (
- renderContent()
- );
+ return renderContent();
};
const styles = StyleSheet.create({
diff --git a/src/components/notifications/NotificationPill.tsx b/src/components/notifications/NotificationPill.tsx
new file mode 100644
index 00000000..525cd7fa
--- /dev/null
+++ b/src/components/notifications/NotificationPill.tsx
@@ -0,0 +1,209 @@
+import React, {useEffect, useState, useRef} from 'react';
+import {Image, StyleSheet, Text, View} from 'react-native';
+import LinearGradient from 'react-native-linear-gradient';
+import {SCREEN_WIDTH, isIPhoneX, numberWithCommas} from '../../utils';
+import {
+ NOTIFICATION_ICON_GRADIENT,
+ CHIN_HEIGHT,
+ NAV_BAR_HEIGHT,
+} from '../../constants';
+import {getNotificationsUnreadCount} from '../../services';
+import {normalize} from 'react-native-elements';
+import PillIcon4 from '../../assets/images/Group 479.svg';
+
+interface NotificationPillProps {
+ showIcon: boolean;
+}
+
+export const NotificationPill: React.FC<NotificationPillProps> = ({
+ showIcon,
+}) => {
+ const [iconStart, setIconStart] = useState<number[]>([0, -100]);
+ const [tipStart, setTipStart] = useState<number[]>([0, -100]);
+ const [notificationSets, setNotificationSets] = useState<{
+ CMT?: number;
+ FRD_REQ?: number;
+ P_VIEW?: number;
+ MOM_TAG?: number;
+ }>({});
+ const [timeCount, setTimeCount] = useState<boolean>(false);
+ const [timeOut, setTimeOut] = useState<boolean>(false);
+ const iconRef = useRef(null);
+ const tipRef = useRef(null);
+ const pillTip = require('../../assets/images/purple-tip.png');
+
+ const navBarPos = 20;
+
+ // If there are notifications, determines the size of the pill
+ // and sets points for correct placement
+ useEffect(() => {
+ setTimeout(() => {
+ if (iconRef.current) {
+ iconRef.current.measure(
+ (
+ _fx: number,
+ _fy: number,
+ width: number,
+ height: number,
+ _px: number,
+ _py: number,
+ ) => {
+ if (tipRef.current) {
+ tipRef.current.measure(
+ (
+ __fx: number,
+ __fy: number,
+ width2: number,
+ __height: number,
+ __px: number,
+ __py: number,
+ ) => {
+ const x = SCREEN_WIDTH / 2 - width / 2;
+ const y = isIPhoneX()
+ ? CHIN_HEIGHT + NAV_BAR_HEIGHT + navBarPos
+ : NAV_BAR_HEIGHT + navBarPos;
+ setIconStart([x, y]);
+ setTipStart([width / 2 - width2 / 2, height - 1]);
+ setTimeCount(true);
+ },
+ );
+ }
+ },
+ );
+ } else {
+ }
+ }, 100);
+ }, [notificationSets, iconRef, tipRef]);
+
+ // Used so that pill disappears after 10 seconds
+ useEffect(() => {
+ if (timeCount) {
+ setTimeout(() => {
+ setTimeOut(true);
+ }, 10000);
+ }
+ }, [timeCount]);
+
+ // Gets data from backend to check for unreads
+ useEffect(() => {
+ const getCount = async () => {
+ const data = await getNotificationsUnreadCount();
+ setTimeout(() => {
+ if (data) {
+ setNotificationSets(data);
+ }
+ }, 100);
+ };
+
+ getCount();
+ }, []);
+
+ return (
+ <>
+ {notificationSets &&
+ Object.keys(notificationSets).length !== 0 &&
+ showIcon &&
+ !timeOut && (
+ <View
+ style={[
+ styles.purpleContainer,
+ {bottom: iconStart[1], left: iconStart[0]},
+ ]}
+ ref={iconRef}>
+ <LinearGradient
+ colors={NOTIFICATION_ICON_GRADIENT}
+ style={styles.iconPurple}>
+ {notificationSets.CMT && (
+ <>
+ <Image
+ source={require('../../assets/images/pill-icon-1.png')}
+ style={styles.indicationIcon}
+ />
+ <Text style={styles.text}>
+ {numberWithCommas(notificationSets.CMT)}
+ </Text>
+ </>
+ )}
+ {notificationSets.FRD_REQ && (
+ <>
+ <Image
+ source={require('../../assets/images/pill-icon-2.png')}
+ style={styles.indicationIcon}
+ />
+ <Text style={styles.text}>
+ {numberWithCommas(notificationSets.FRD_REQ)}
+ </Text>
+ </>
+ )}
+ {notificationSets.P_VIEW && (
+ <>
+ <Image
+ source={require('../../assets/images/pill-icon-3.png')}
+ style={styles.indicationIcon}
+ />
+ <Text style={styles.text}>
+ {numberWithCommas(notificationSets.P_VIEW)}
+ </Text>
+ </>
+ )}
+ {notificationSets.MOM_TAG && (
+ <>
+ <PillIcon4 style={styles.indicationIcon} />
+ <Text style={styles.text}>
+ {numberWithCommas(notificationSets.MOM_TAG)}
+ </Text>
+ </>
+ )}
+ </LinearGradient>
+ <Image
+ style={[styles.tip, {top: tipStart[1], left: tipStart[0]}]}
+ source={pillTip}
+ ref={tipRef}
+ />
+ </View>
+ )}
+ </>
+ );
+};
+
+const styles = StyleSheet.create({
+ purpleContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ position: 'absolute',
+ zIndex: 999,
+ },
+ iconPurple: {
+ padding: 5,
+ borderRadius: 15,
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ text: {
+ margin: 2,
+ color: 'white',
+ fontSize: normalize(10),
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 5,
+ },
+ tip: {
+ position: 'absolute',
+ zIndex: 999,
+ height: 12,
+ flex: 1,
+ resizeMode: 'contain',
+ },
+ indicationIcon: {
+ height: 14,
+ width: 14,
+ margin: 2,
+ marginLeft: 5,
+ },
+ svgIndicationIcon: {
+ height: 14,
+ width: 14,
+ margin: 3,
+ },
+});
diff --git a/src/components/notifications/index.ts b/src/components/notifications/index.ts
index 733b56f1..077c26a4 100644
--- a/src/components/notifications/index.ts
+++ b/src/components/notifications/index.ts
@@ -1,2 +1,3 @@
export {default as Notification} from './Notification';
export {InviteFriendsPrompt} from './NotificationPrompts';
+export {NotificationPill} from './NotificationPill';
diff --git a/src/components/profile/MomentMoreInfoDrawer.tsx b/src/components/profile/MomentMoreInfoDrawer.tsx
index 1265497e..a796ffd8 100644
--- a/src/components/profile/MomentMoreInfoDrawer.tsx
+++ b/src/components/profile/MomentMoreInfoDrawer.tsx
@@ -1,44 +1,58 @@
+import {useNavigation} from '@react-navigation/core';
import React, {useEffect, useState} from 'react';
import {
Alert,
GestureResponderEvent,
StyleSheet,
+ TextStyle,
TouchableOpacity,
ViewProps,
} from 'react-native';
import MoreIcon from '../../assets/icons/more_horiz-24px.svg';
import {ERROR_DELETE_MOMENT, MOMENT_DELETED_MSG} from '../../constants/strings';
import {deleteMoment, sendReport} from '../../services';
+import {MomentTagType, MomentType, ScreenType} from '../../types/types';
import {GenericMoreInfoDrawer} from '../common';
enum MomentDrawerOptions {
DeleteMoment = 'Delete Moment',
ReportIssue = 'Report an Issue',
RemoveTag = 'Remove yourself from moment',
+ EditMoment = 'Edit Moment',
}
interface MomentMoreInfoDrawerProps extends ViewProps {
isOpen: boolean;
setIsOpen: (visible: boolean) => void;
- momentId: string;
isOwnProfile: boolean;
momentTagId: string;
removeTag: () => Promise<void>;
dismissScreenAndUpdate: () => void;
+ screenType: ScreenType;
+ moment: MomentType;
+ tags: MomentTagType[];
}
const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => {
const {
- momentId,
setIsOpen,
isOwnProfile,
dismissScreenAndUpdate,
momentTagId,
removeTag,
+ screenType,
+ moment,
+ tags,
} = props;
+ const navigation = useNavigation();
+
+ const [drawerButtons, setDrawerButtons] = useState<
+ [string, (event: GestureResponderEvent) => void, JSX.Element?, TextStyle?][]
+ >([]);
+
const handleDeleteMoment = async () => {
setIsOpen(false);
- deleteMoment(momentId).then((success) => {
+ deleteMoment(moment.moment_id).then((success) => {
if (success) {
// set time out for UI transitions
setTimeout(() => {
@@ -88,7 +102,8 @@ const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => {
[
{
text: 'Mark as inappropriate',
- onPress: () => sendReport(momentId, 'Mark as inappropriate'),
+ onPress: () =>
+ sendReport(moment.moment_id, 'Mark as inappropriate'),
},
{
text: 'Cancel',
@@ -96,7 +111,7 @@ const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => {
},
{
text: 'Mark as abusive',
- onPress: () => sendReport(momentId, 'Mark as abusive'),
+ onPress: () => sendReport(moment.moment_id, 'Mark as abusive'),
},
],
{cancelable: false},
@@ -104,42 +119,52 @@ const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => {
}, 500);
};
- const [drawerButtons, setDrawerButtons] = useState<
- [string, (event: GestureResponderEvent) => void, JSX.Element?][]
- >([
- isOwnProfile
- ? [MomentDrawerOptions.DeleteMoment, handleDeleteMoment]
- : [MomentDrawerOptions.ReportIssue, handleReportMoment],
- ]);
+ const handleEditMoment = async () => {
+ setIsOpen(false);
+ navigation.navigate('CaptionScreen', {
+ screenType: screenType,
+ selectedTags: tags,
+ moment: moment,
+ });
+ };
/*
* Update bottom drawer options to contain/not contain 'remove tag' option
*/
useEffect(() => {
- const setupBottomDrawer = () => {
- const present = drawerButtons.findIndex(
- (button) => button[0] === MomentDrawerOptions.RemoveTag,
- );
- /*
- * If user is not tagged but button is present, remove button from bottom drawer
- * If user is tagged but button is not present, add button to bottom drawer
- */
- if (momentTagId !== '' && present === -1) {
- const localDrawerButtons = drawerButtons;
- localDrawerButtons.push([
+ let newButtons: [
+ string,
+ (event: GestureResponderEvent) => void,
+ JSX.Element?,
+ TextStyle?,
+ ][] = [];
+ if (!isOwnProfile) {
+ newButtons.push([
+ MomentDrawerOptions.ReportIssue,
+ handleReportMoment,
+ undefined,
+ {color: 'red'},
+ ]);
+ // should we have the "delete moment" option?
+ if (momentTagId !== '') {
+ newButtons.push([
MomentDrawerOptions.RemoveTag,
handleRemoveTag,
+ undefined,
+ {color: 'red'},
]);
- setDrawerButtons(localDrawerButtons);
- } else if (momentTagId === '' && present !== -1) {
- const filteredButtons = drawerButtons.filter(
- (button) => button[0] !== MomentDrawerOptions.RemoveTag,
- );
- setDrawerButtons(filteredButtons);
}
- };
- setupBottomDrawer();
- }, [momentTagId]);
+ } else {
+ newButtons.push([
+ MomentDrawerOptions.DeleteMoment,
+ handleDeleteMoment,
+ undefined,
+ {color: 'red'},
+ ]);
+ newButtons.push([MomentDrawerOptions.EditMoment, handleEditMoment]);
+ }
+ setDrawerButtons(newButtons);
+ }, [tags, momentTagId]);
return (
<>
@@ -153,7 +178,6 @@ const MomentMoreInfoDrawer: React.FC<MomentMoreInfoDrawerProps> = (props) => {
<GenericMoreInfoDrawer
{...props}
showIcons={false}
- textColor={'red'}
buttons={drawerButtons}
/>
</>
diff --git a/src/components/profile/ProfileMoreInfoDrawer.tsx b/src/components/profile/ProfileMoreInfoDrawer.tsx
index ecc45211..656f81bb 100644
--- a/src/components/profile/ProfileMoreInfoDrawer.tsx
+++ b/src/components/profile/ProfileMoreInfoDrawer.tsx
@@ -55,12 +55,12 @@ const ProfileMoreInfoDrawer: React.FC<ProfileMoreInfoDrawerProps> = (props) => {
<GenericMoreInfoDrawer
{...props}
showIcons={false}
- textColor={'red'}
buttons={[
[
(isBlocked ? 'Unblock' : 'Block') + ` ${userXName}`,
onBlockUnblock,
undefined,
+ {color: 'red'},
],
]}
/>
@@ -68,7 +68,6 @@ const ProfileMoreInfoDrawer: React.FC<ProfileMoreInfoDrawerProps> = (props) => {
<GenericMoreInfoDrawer
{...props}
showIcons={true}
- textColor={'black'}
buttons={[
[
'Settings',
@@ -77,6 +76,7 @@ const ProfileMoreInfoDrawer: React.FC<ProfileMoreInfoDrawerProps> = (props) => {
source={require('../../assets/images/settings/settings.png')}
style={styles.image}
/>,
+ {color: 'black'},
],
[
'Edit Profile',
@@ -85,6 +85,7 @@ const ProfileMoreInfoDrawer: React.FC<ProfileMoreInfoDrawerProps> = (props) => {
source={require('../../assets/images/settings/edit-profile.png')}
style={styles.image}
/>,
+ {color: 'black'},
],
]}
/>
diff --git a/src/components/suggestedPeople/legacy/BadgesDropdown.tsx b/src/components/suggestedPeople/legacy/BadgesDropdown.tsx
index 2c177e69..307205b8 100644
--- a/src/components/suggestedPeople/legacy/BadgesDropdown.tsx
+++ b/src/components/suggestedPeople/legacy/BadgesDropdown.tsx
@@ -1,7 +1,7 @@
import React, {useEffect, useState} from 'react';
import {StyleSheet} from 'react-native';
import {TouchableOpacity} from 'react-native-gesture-handler';
-import Animated, {Easing} from 'react-native-reanimated';
+import Animated, {EasingNode} from 'react-native-reanimated';
import {BadgeIcon, UniversityIcon} from '../..';
import {UniversityBadgeDisplayType, UniversityType} from '../../../types';
import {normalize} from '../../../utils';
@@ -41,7 +41,7 @@ const BadgesDropdown: React.FC<BadgesDropdownProps> = ({
Animated.timing(top[i], {
toValue: i * 40 + 50,
duration: 150,
- easing: Easing.linear,
+ easing: EasingNode.linear,
}).start();
}
}
@@ -54,7 +54,7 @@ const BadgesDropdown: React.FC<BadgesDropdownProps> = ({
Animated.timing(top[i], {
toValue: 0,
duration: 150,
- easing: Easing.linear,
+ easing: EasingNode.linear,
}).start();
}
}
diff --git a/src/constants/api.ts b/src/constants/api.ts
index f02ee407..b55489d9 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -45,6 +45,8 @@ export const BLOCK_USER_ENDPOINT: string = API_URL + 'block/';
export const PASSWORD_RESET_ENDPOINT: string = API_URL + 'password-reset/';
export const MOMENT_CATEGORY_ENDPOINT: string = API_URL + 'moment-category/';
export const NOTIFICATIONS_ENDPOINT: string = API_URL + 'notifications/';
+export const NOTIFICATIONS_COUNT_ENDPOINT: string = API_URL + 'notifications/unread_count/';
+export const NOTIFICATIONS_DATE: string = API_URL + 'notifications/seen/';
export const DISCOVER_ENDPOINT: string = API_URL + 'discover/';
export const SEARCH_BUTTONS_ENDPOPINT: string =
DISCOVER_ENDPOINT + 'search_buttons/';
diff --git a/src/constants/constants.ts b/src/constants/constants.ts
index a6d98883..f4ffd750 100644
--- a/src/constants/constants.ts
+++ b/src/constants/constants.ts
@@ -21,6 +21,8 @@ export const AVATAR_GRADIENT_DIM = 50;
export const TAGG_ICON_DIM = 58;
export const TAGG_RING_DIM = normalize(60);
+// default height of the navigation bar, from react native library, unless on ipad
+export const NAV_BAR_HEIGHT = 49;
export const BADGE_LIMIT = 5;
export const INTEGRATED_SOCIAL_LIST: string[] = [
@@ -91,6 +93,7 @@ export const BADGE_GRADIENT_REST = [
'rgba(78, 54, 41, 1)',
'rgba(236, 32, 39, 1)',
];
+export const NOTIFICATION_ICON_GRADIENT = ['#8F01FF', '#7B02DA'];
export const SOCIAL_FONT_COLORS = {
INSTAGRAM: INSTAGRAM_FONT_COLOR,
diff --git a/src/constants/regex.ts b/src/constants/regex.ts
index 61523203..f934185d 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/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx
index d22c1874..8fce5e2f 100644
--- a/src/routes/main/MainStackNavigator.tsx
+++ b/src/routes/main/MainStackNavigator.tsx
@@ -37,14 +37,14 @@ export type MainStackParams = {
screenType: ScreenType;
};
CaptionScreen: {
- title: string;
- image: Image;
+ title?: string;
+ image?: Image;
screenType: ScreenType;
selectedTags?: MomentTagType[];
+ moment?: MomentType;
};
TagFriendsScreen: {
- image: Image;
- screenType: ScreenType;
+ imagePath: string;
selectedTags?: MomentTagType[];
};
TagSelectionScreen: {
diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx
index f6a012d6..3be2ff28 100644
--- a/src/routes/main/MainStackScreen.tsx
+++ b/src/routes/main/MainStackScreen.tsx
@@ -245,7 +245,6 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
<MainStack.Screen
name="InviteFriendsScreen"
component={InviteFriendsScreen}
- initialParams={{screenType}}
options={{
...headerBarOptions('black', 'Invites'),
}}
diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx
index 000ac614..c3a42739 100644
--- a/src/routes/tabs/NavigationBar.tsx
+++ b/src/routes/tabs/NavigationBar.tsx
@@ -4,9 +4,11 @@ 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';
const Tabs = createBottomTabNavigator();
@@ -18,10 +20,14 @@ const NavigationBar: React.FC = () => {
const {notifications: {notifications} = NO_NOTIFICATIONS} = useSelector(
(state: RootState) => state,
);
+ // Triggered if user clicks on Notifications page to close the pill
+ const [showIcon, setShowIcon] = useState<boolean>(true);
const [unreadNotificationsPresent, setUnreadNotificationsPresent] =
useState<boolean>(false);
+ // Prior to pill inclusion, determines if notification bell
+ // should have purple dot
useEffect(() => {
const determine = async () => {
setUnreadNotificationsPresent(
@@ -32,77 +38,88 @@ const NavigationBar: React.FC = () => {
}, [notifications]);
return (
- <Tabs.Navigator
- screenOptions={({route}) => ({
- tabBarIcon: ({focused}) => {
- switch (route.name) {
- case 'Home':
- return <NavigationIcon tab="Home" disabled={!focused} />;
- case 'Search':
- return <NavigationIcon tab="Search" disabled={!focused} />;
- case 'Upload':
- return <NavigationIcon tab="Upload" disabled={!focused} />;
- case 'Notifications':
- return (
- <NavigationIcon
- newIcon={
- newNotificationReceived || unreadNotificationsPresent
- }
- tab="Notifications"
- disabled={!focused}
- />
- );
- case 'Chat':
- return <NavigationIcon tab="Chat" disabled={!focused} />;
- case 'Profile':
- return <NavigationIcon tab="Profile" disabled={!focused} />;
- case 'SuggestedPeople':
- return (
- <NavigationIcon tab="SuggestedPeople" disabled={!focused} />
- );
- default:
- return <Fragment />;
- }
- },
- })}
- initialRouteName={isOnboardedUser ? 'Profile' : 'SuggestedPeople'}
- tabBarOptions={{
- showLabel: false,
- style: {
- backgroundColor: 'transparent',
- position: 'absolute',
- borderTopWidth: 0,
- left: 0,
- right: 0,
- bottom: '1%',
- },
- }}>
- <Tabs.Screen
- name="SuggestedPeople"
- component={MainStackScreen}
- initialParams={{screenType: ScreenType.SuggestedPeople}}
- />
- <Tabs.Screen
- name="Search"
- component={MainStackScreen}
- initialParams={{screenType: ScreenType.Search}}
- />
- <Tabs.Screen
- name="Notifications"
- component={MainStackScreen}
- initialParams={{screenType: ScreenType.Notifications}}
- />
- <Tabs.Screen
- name="Chat"
- component={MainStackScreen}
- initialParams={{screenType: ScreenType.Chat}}
- />
- <Tabs.Screen
- name="Profile"
- component={MainStackScreen}
- initialParams={{screenType: ScreenType.Profile}}
- />
- </Tabs.Navigator>
+ <>
+ <NotificationPill showIcon={showIcon} />
+ <Tabs.Navigator
+ screenOptions={({route}) => ({
+ tabBarIcon: ({focused}) => {
+ switch (route.name) {
+ case 'Home':
+ return <NavigationIcon tab="Home" disabled={!focused} />;
+ case 'Search':
+ return <NavigationIcon tab="Search" disabled={!focused} />;
+ case 'Upload':
+ return <NavigationIcon tab="Upload" disabled={!focused} />;
+ case 'Notifications':
+ return (
+ <NavigationIcon
+ newIcon={
+ newNotificationReceived || unreadNotificationsPresent
+ }
+ tab="Notifications"
+ disabled={!focused}
+ />
+ );
+ case 'Chat':
+ return <NavigationIcon tab="Chat" disabled={!focused} />;
+ case 'Profile':
+ return <NavigationIcon tab="Profile" disabled={!focused} />;
+ case 'SuggestedPeople':
+ return (
+ <NavigationIcon tab="SuggestedPeople" disabled={!focused} />
+ );
+ default:
+ return <Fragment />;
+ }
+ },
+ })}
+ initialRouteName={isOnboardedUser ? 'Profile' : 'SuggestedPeople'}
+ tabBarOptions={{
+ showLabel: false,
+ style: {
+ backgroundColor: 'transparent',
+ position: 'absolute',
+ borderTopWidth: 0,
+ left: 0,
+ right: 0,
+ bottom: '1%',
+ },
+ }}>
+ <Tabs.Screen
+ name="SuggestedPeople"
+ component={MainStackScreen}
+ initialParams={{screenType: ScreenType.SuggestedPeople}}
+ />
+ <Tabs.Screen
+ name="Search"
+ component={MainStackScreen}
+ initialParams={{screenType: ScreenType.Search}}
+ />
+ <Tabs.Screen
+ name="Notifications"
+ component={MainStackScreen}
+ initialParams={{screenType: ScreenType.Notifications}}
+ listeners={{
+ tabPress: (_) => {
+ // Closes the pill once this screen has been opened
+ setShowIcon(false);
+ // Updates backend's date of reading notifications
+ setNotificationsReadDate();
+ },
+ }}
+ />
+ <Tabs.Screen
+ name="Chat"
+ component={MainStackScreen}
+ initialParams={{screenType: ScreenType.Chat}}
+ />
+ <Tabs.Screen
+ name="Profile"
+ component={MainStackScreen}
+ initialParams={{screenType: ScreenType.Profile}}
+ />
+ </Tabs.Navigator>
+ </>
);
};
diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx
index ebcccc8e..03842b0a 100644
--- a/src/screens/main/NotificationsScreen.tsx
+++ b/src/screens/main/NotificationsScreen.tsx
@@ -19,6 +19,7 @@ import {SafeAreaView} from 'react-native-safe-area-context';
import {useDispatch, useSelector} from 'react-redux';
import FindFriendsBlueIcon from '../../assets/icons/findFriends/find-friends-blue-icon.svg';
import {TabsGradient} from '../../components';
+import EmptyContentView from '../../components/common/EmptyContentView';
import {Notification} from '../../components/notifications';
import {NewChatPrompt} from '../../components/notifications/NotificationPrompts';
import {
@@ -28,7 +29,6 @@ import {
import {RootState} from '../../store/rootReducer';
import {NotificationType, ScreenType} from '../../types';
import {getDateAge, normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
-import EmptyContentView from '../../components/common/EmptyContentView';
const NotificationsScreen: React.FC = () => {
const {newNotificationReceived} = useSelector(
@@ -290,7 +290,9 @@ const NotificationsScreen: React.FC = () => {
contentContainerStyle={styles.container}
stickySectionHeadersEnabled={false}
sections={sectionedNotifications}
- keyExtractor={(_item, index) => index.toString()}
+ keyExtractor={(item, index) =>
+ item.timestamp.toString() + index.toString()
+ }
renderItem={renderNotification}
renderSectionHeader={renderSectionHeader}
renderSectionFooter={renderSectionFooter}
diff --git a/src/screens/moments/TagFriendsScreen.tsx b/src/screens/moments/TagFriendsScreen.tsx
index c8bca9f4..570c3776 100644
--- a/src/screens/moments/TagFriendsScreen.tsx
+++ b/src/screens/moments/TagFriendsScreen.tsx
@@ -30,7 +30,7 @@ interface TagFriendsScreenProps {
route: TagFriendsScreenRouteProps;
}
const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => {
- const {image, selectedTags} = route.params;
+ const {imagePath, selectedTags} = route.params;
const navigation = useNavigation();
const imageRef = useRef(null);
const [tags, setTags] = useState<MomentTagType[]>([]);
@@ -85,7 +85,7 @@ const TagFriendsScreen: React.FC<TagFriendsScreenProps> = ({route}) => {
<Image
ref={imageRef}
style={styles.image}
- source={{uri: image.path}}
+ source={{uri: imagePath}}
resizeMode={'cover'}
/>
</TouchableWithoutFeedback>
diff --git a/src/screens/onboarding/BasicInfoOnboarding.tsx b/src/screens/onboarding/BasicInfoOnboarding.tsx
index e5e6f59b..d5998ac1 100644
--- a/src/screens/onboarding/BasicInfoOnboarding.tsx
+++ b/src/screens/onboarding/BasicInfoOnboarding.tsx
@@ -13,7 +13,7 @@ import {
TouchableOpacity,
} from 'react-native';
import {normalize} from 'react-native-elements';
-import Animated, {Easing, useValue} from 'react-native-reanimated';
+import Animated, {EasingNode, useValue} from 'react-native-reanimated';
import {
ArrowButton,
Background,
@@ -99,7 +99,7 @@ const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => {
Animated.timing(fadeButtonValue, {
toValue: target,
duration: 100,
- easing: Easing.linear,
+ easing: EasingNode.linear,
}).start();
};
@@ -108,7 +108,7 @@ const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => {
Animated.timing(fadeValue, {
toValue: 1,
duration: 1000,
- easing: Easing.linear,
+ easing: EasingNode.linear,
}).start();
};
fade();
diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx
index 755c668b..253346d5 100644
--- a/src/screens/profile/CaptionScreen.tsx
+++ b/src/screens/profile/CaptionScreen.tsx
@@ -21,9 +21,13 @@ import {SearchBackground} from '../../components';
import {CaptionScreenHeader} from '../../components/';
import TaggLoadingIndicator from '../../components/common/TaggLoadingIndicator';
import {TAGG_LIGHT_BLUE_2} from '../../constants';
-import {ERROR_UPLOAD, SUCCESS_PIC_UPLOAD} from '../../constants/strings';
+import {
+ ERROR_SOMETHING_WENT_WRONG_REFRESH,
+ ERROR_UPLOAD,
+ SUCCESS_PIC_UPLOAD,
+} from '../../constants/strings';
import {MainStackParams} from '../../routes';
-import {postMoment, postMomentTags} from '../../services';
+import {patchMoment, postMoment, postMomentTags} from '../../services';
import {
loadUserMoments,
updateProfileCompletionStage,
@@ -47,14 +51,16 @@ interface CaptionScreenProps {
}
const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
- const {title, image, screenType, selectedTags} = route.params;
+ const {title, image, screenType, selectedTags, moment} = route.params;
const {
user: {userId},
} = useSelector((state: RootState) => state.user);
const dispatch = useDispatch();
- const [caption, setCaption] = useState('');
+ const [caption, setCaption] = useState(moment ? moment.caption : '');
const [loading, setLoading] = useState(false);
- const [tags, setTags] = useState<MomentTagType[]>([]);
+ const [tags, setTags] = useState<MomentTagType[]>(
+ selectedTags ? selectedTags : [],
+ );
const [taggedList, setTaggedList] = useState<string>('');
useEffect(() => {
@@ -84,22 +90,37 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
});
};
- const handleShare = async () => {
- const handleFailed = () => {
- setLoading(false);
- setTimeout(() => {
- Alert.alert(ERROR_UPLOAD);
- }, 500);
- };
- const handleSuccess = () => {
+ const handleFailed = () => {
+ setLoading(false);
+ setTimeout(() => {
+ Alert.alert(moment ? ERROR_SOMETHING_WENT_WRONG_REFRESH : ERROR_UPLOAD);
+ }, 500);
+ };
+ const handleSuccess = () => {
+ setLoading(false);
+ if (moment) {
setLoading(false);
+ navigation.goBack();
+ } else {
navigateToProfile();
setTimeout(() => {
Alert.alert(SUCCESS_PIC_UPLOAD);
}, 500);
- };
+ }
+ };
+
+ const formattedTags = () => {
+ return tags.map((tag) => ({
+ x: Math.floor(tag.x),
+ y: Math.floor(tag.y),
+ z: Math.floor(tag.z),
+ user_id: tag.user.id,
+ }));
+ };
+
+ const handleShare = async () => {
setLoading(true);
- if (!image.filename) {
+ if (!image?.filename || !title) {
return;
}
const momentResponse = await postMoment(
@@ -115,12 +136,7 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
}
const momentTagResponse = await postMomentTags(
momentResponse.moment_id,
- tags.map((tag) => ({
- x: Math.floor(tag.x),
- y: Math.floor(tag.y),
- z: Math.floor(tag.z),
- user_id: tag.user.id,
- })),
+ formattedTags(),
);
if (!momentTagResponse) {
handleFailed();
@@ -133,6 +149,23 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
handleSuccess();
};
+ const handleDone = async () => {
+ setLoading(true);
+ if (moment?.moment_id) {
+ const success = await patchMoment(
+ moment.moment_id,
+ caption,
+ formattedTags(),
+ );
+ if (success) {
+ dispatch(loadUserMoments(userId));
+ handleSuccess();
+ } else {
+ handleFailed();
+ }
+ }
+ };
+
return (
<SearchBackground>
{loading ? <TaggLoadingIndicator fullscreen /> : <Fragment />}
@@ -145,20 +178,25 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
<Button
title="Cancel"
buttonStyle={styles.button}
- onPress={() => navigateToProfile()}
+ onPress={() =>
+ moment ? navigation.goBack() : navigateToProfile()
+ }
/>
<Button
- title="Share"
+ title={moment ? 'Done' : 'Share'}
titleStyle={styles.shareButtonTitle}
buttonStyle={styles.button}
- onPress={handleShare}
+ onPress={moment ? handleDone : handleShare}
/>
</View>
- <CaptionScreenHeader style={styles.header} {...{title: title}} />
+ <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: image.path}}
+ source={{uri: moment ? moment.moment_url : image?.path}}
resizeMode={'cover'}
/>
<MentionInputControlled
@@ -172,8 +210,11 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
<TouchableOpacity
onPress={() =>
navigation.navigate('TagFriendsScreen', {
- image: image,
- screenType: screenType,
+ imagePath: moment
+ ? moment.moment_url
+ : image
+ ? image.path
+ : '',
selectedTags: tags,
})
}
diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx
index 26802e45..20a62b19 100644
--- a/src/screens/profile/EditProfile.tsx
+++ b/src/screens/profile/EditProfile.tsx
@@ -305,14 +305,13 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => {
type: 'image/jpg',
});
}
- if (form.website) {
- if (form.isValidWebsite) {
- request.append('website', form.website);
- } else {
- setForm({...form, attemptedSubmit: false});
- setTimeout(() => setForm({...form, attemptedSubmit: true}));
- invalidFields = true;
- }
+
+ if (form.isValidWebsite) {
+ request.append('website', form.website);
+ } else {
+ setForm({...form, attemptedSubmit: false});
+ setTimeout(() => setForm({...form, attemptedSubmit: true}));
+ invalidFields = true;
}
if (form.bio) {
diff --git a/src/screens/profile/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx
index 4ad4515d..f8113aba 100644
--- a/src/screens/profile/IndividualMoment.tsx
+++ b/src/screens/profile/IndividualMoment.tsx
@@ -1,103 +1,138 @@
import {BlurView} from '@react-native-community/blur';
import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
-import React from 'react';
-import {FlatList, StyleSheet, View} from 'react-native';
+import React, {useEffect, useRef, useState} from 'react';
+import {FlatList, Keyboard, StyleSheet} from 'react-native';
import {useSelector} from 'react-redux';
import {IndividualMomentTitleBar, MomentPost} from '../../components';
+import {AVATAR_DIM} from '../../constants';
import {MainStackParams} from '../../routes';
import {RootState} from '../../store/rootreducer';
-import {MomentType} from '../../types';
-import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils';
+import {MomentPostType} from '../../types';
+import {
+ isIPhoneX,
+ normalize,
+ SCREEN_HEIGHT,
+ StatusBarHeight,
+} from '../../utils';
/**
* Individual moment view opened when user clicks on a moment tile
*/
+
+type MomentContextType = {
+ keyboardVisible: boolean;
+ scrollTo: (index: number) => void;
+};
+
+export const MomentContext = React.createContext({} as MomentContextType);
+
type IndividualMomentRouteProp = RouteProp<MainStackParams, 'IndividualMoment'>;
+
type IndividualMomentNavigationProp = StackNavigationProp<
MainStackParams,
'IndividualMoment'
>;
+
interface IndividualMomentProps {
route: IndividualMomentRouteProp;
navigation: IndividualMomentNavigationProp;
}
-const ITEM_HEIGHT = SCREEN_HEIGHT * 0.9;
-
const IndividualMoment: React.FC<IndividualMomentProps> = ({
route,
navigation,
}) => {
- const {moment_category, moment_id} = route.params.moment;
- const {userXId, screenType} = route.params;
-
+ const {
+ userXId,
+ screenType,
+ moment: {moment_category, moment_id},
+ } = route.params;
const {moments} = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.moments,
);
-
+ const scrollRef = useRef<FlatList<MomentPostType>>(null);
const momentData = moments.filter(
(m) => m.moment_category === moment_category,
);
const initialIndex = momentData.findIndex((m) => m.moment_id === moment_id);
+ const [keyboardVisible, setKeyboardVisible] = useState(false);
+
+ useEffect(() => {
+ const showKeyboard = () => setKeyboardVisible(true);
+ const hideKeyboard = () => setKeyboardVisible(false);
+ Keyboard.addListener('keyboardWillShow', showKeyboard);
+ Keyboard.addListener('keyboardWillHide', hideKeyboard);
+ return () => {
+ Keyboard.removeListener('keyboardWillShow', showKeyboard);
+ Keyboard.removeListener('keyboardWillHide', hideKeyboard);
+ };
+ }, []);
+
+ 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 (
- <BlurView
- blurType="light"
- blurAmount={30}
- reducedTransparencyFallbackColor="white"
- style={styles.contentContainer}>
- <IndividualMomentTitleBar
- style={styles.header}
- close={() => navigation.pop()}
- {...{title: moment_category}}
- />
- <View style={styles.content}>
+ <MomentContext.Provider
+ value={{
+ keyboardVisible,
+ scrollTo,
+ }}>
+ <BlurView
+ blurType="light"
+ blurAmount={30}
+ reducedTransparencyFallbackColor="white"
+ style={styles.contentContainer}>
+ <IndividualMomentTitleBar
+ style={styles.header}
+ close={() => navigation.goBack()}
+ title={moment_category}
+ />
<FlatList
+ ref={scrollRef}
data={momentData}
- renderItem={({item}: {item: MomentType}) => (
- <MomentPost userXId={userXId} screenType={screenType} item={item} />
+ contentContainerStyle={styles.listContentContainer}
+ renderItem={({item, index}) => (
+ <MomentPost
+ moment={item}
+ userXId={userXId}
+ screenType={screenType}
+ index={index}
+ />
)}
- keyExtractor={(item, index) => index.toString()}
+ keyExtractor={(item, _) => item.moment_id}
showsVerticalScrollIndicator={false}
- snapToAlignment={'start'}
- snapToInterval={ITEM_HEIGHT}
- decelerationRate={'fast'}
initialScrollIndex={initialIndex}
- getItemLayout={(data, index) => ({
- length: ITEM_HEIGHT,
- offset: ITEM_HEIGHT * index,
- index,
- })}
- pagingEnabled
+ onScrollToIndexFailed={() => {
+ // TODO: code below does not work, index resets to 0
+ // const wait = new Promise((resolve) => setTimeout(resolve, 500));
+ // wait.then(() => {
+ // console.log('scrolling to ', initialIndex);
+ // scrollRef.current?.scrollToIndex({index: initialIndex});
+ // });
+ }}
/>
- </View>
- </BlurView>
+ </BlurView>
+ </MomentContext.Provider>
);
};
const styles = StyleSheet.create({
contentContainer: {
- width: SCREEN_WIDTH,
- height: SCREEN_HEIGHT,
paddingTop: StatusBarHeight,
flex: 1,
- paddingBottom: 0,
- },
- content: {
- flex: 9,
},
header: {
- flex: 1,
- },
- postContainer: {
- height: ITEM_HEIGHT,
- width: SCREEN_WIDTH,
- flex: 1,
+ height: normalize(70),
},
- postHeader: {
- flex: 1,
+ listContentContainer: {
+ paddingBottom: SCREEN_HEIGHT * 0.2,
},
- postContent: {flex: 9},
});
export default IndividualMoment;
diff --git a/src/screens/profile/InviteFriendsScreen.tsx b/src/screens/profile/InviteFriendsScreen.tsx
index 89f2e62f..9ee6fb1c 100644
--- a/src/screens/profile/InviteFriendsScreen.tsx
+++ b/src/screens/profile/InviteFriendsScreen.tsx
@@ -21,7 +21,6 @@ import {
getRemainingInviteCount,
usersFromContactsService,
} from '../../services/UserFriendsService';
-import {ProfilePreviewType} from '../../types';
import {
extractContacts,
HeaderHeight,
@@ -45,11 +44,10 @@ export type SearchResultType = {
const InviteFriendsScreen: React.FC = () => {
const navigation = useNavigation();
- const [usersFromContacts, setUsersFromContacts] = useState<
- ProfilePreviewType[]
+ const [nonUsersFromContacts, setNonUsersFromContacts] = useState<
+ InviteContactType[]
>([]);
- const [nonUsersFromContacts, setNonUsersFromContacts] = useState<[]>([]);
- const [pendingUsers] = useState<[]>([]);
+ const [pendingUsers, setPendingUsers] = useState<InviteContactType[]>([]);
const [results, setResults] = useState<SearchResultType>({
nonUsersFromContacts: nonUsersFromContacts,
pendingUsers: pendingUsers,
@@ -80,8 +78,8 @@ const InviteFriendsScreen: React.FC = () => {
const permission = await checkPermission();
if (permission === 'authorized') {
let response = await usersFromContactsService(retrievedContacts);
- await setUsersFromContacts(response.existing_tagg_users);
await setNonUsersFromContacts(response.invite_from_contacts);
+ await setPendingUsers(response.pending_users);
setResults({
nonUsersFromContacts: response.invite_from_contacts,
pendingUsers: response.pending_users,
@@ -100,30 +98,32 @@ const InviteFriendsScreen: React.FC = () => {
useEffect(() => {
const search = async () => {
if (query.length > 0) {
- const searchResultsUsers = usersFromContacts.filter(
- (item: ProfilePreviewType) =>
- (item.first_name + ' ' + item.last_name)
- .toLowerCase()
- .startsWith(query) ||
- item.username.toLowerCase().startsWith(query) ||
- item.last_name.toLowerCase().startsWith(query),
- );
- const searchResultsNonUsers = nonUsersFromContacts.filter(
- (item: InviteContactType) =>
- (item.firstName + ' ' + item.lastName)
- .toLowerCase()
- .startsWith(query) ||
- item.lastName.toLowerCase().startsWith(query),
- );
- const sanitizedResult = {
- usersFromContacts: searchResultsUsers,
+ const searchResultsPendingUsers = pendingUsers
+ ? pendingUsers.filter(
+ (item: InviteContactType) =>
+ (item.firstName + ' ' + item.lastName)
+ .toLowerCase()
+ .startsWith(query) ||
+ item.lastName.toLowerCase().startsWith(query),
+ )
+ : [];
+ const searchResultsNonUsers = nonUsersFromContacts
+ ? nonUsersFromContacts.filter(
+ (item: InviteContactType) =>
+ (item.firstName + ' ' + item.lastName)
+ .toLowerCase()
+ .startsWith(query) ||
+ item.lastName.toLowerCase().startsWith(query),
+ )
+ : [];
+ setResults({
nonUsersFromContacts: searchResultsNonUsers,
- };
- setResults(sanitizedResult);
+ pendingUsers: searchResultsPendingUsers,
+ });
} else {
setResults({
- nonUsersFromContacts: nonUsersFromContacts,
- pendingUsers: pendingUsers,
+ nonUsersFromContacts: nonUsersFromContacts || [],
+ pendingUsers: pendingUsers || [],
});
}
};
@@ -203,7 +203,7 @@ const InviteFriendsScreen: React.FC = () => {
styles.subheader,
{
height:
- 72 *
+ 75 *
(results.pendingUsers ? results.pendingUsers.length : 1),
},
]}>
diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx
index 402e5f44..7dfe8ae9 100644
--- a/src/screens/profile/MomentCommentsScreen.tsx
+++ b/src/screens/profile/MomentCommentsScreen.tsx
@@ -48,8 +48,9 @@ const MomentCommentsScreen: React.FC<MomentCommentsScreenProps> = ({route}) => {
React.useState(true);
//Keeps track of the current comments object in focus so that the application knows which comment to post a reply to
- const [commentTapped, setCommentTapped] =
- useState<CommentType | CommentThreadType | undefined>();
+ const [commentTapped, setCommentTapped] = useState<
+ CommentType | CommentThreadType | undefined
+ >();
useEffect(() => {
navigation.setOptions({
diff --git a/src/services/MomentService.ts b/src/services/MomentService.ts
index af602dc7..b837585a 100644
--- a/src/services/MomentService.ts
+++ b/src/services/MomentService.ts
@@ -6,7 +6,7 @@ import {
MOMENT_TAGS_ENDPOINT,
MOMENT_THUMBNAIL_ENDPOINT,
} from '../constants';
-import {MomentTagType, MomentType} from '../types';
+import {MomentPostType, MomentTagType} from '../types';
import {checkImageUploadStatus} from '../utils';
export const postMoment = async (
@@ -54,11 +54,39 @@ export const postMoment = async (
return undefined;
};
-export const loadMoments: (
- userId: string,
- token: string,
-) => Promise<MomentType[]> = async (userId, token) => {
- let moments: MomentType[] = [];
+export const patchMoment = async (
+ momentId: string,
+ caption: string,
+ tags: {
+ x: number;
+ y: number;
+ z: number;
+ user_id: string;
+ }[],
+) => {
+ try {
+ const request = new FormData();
+ request.append('moment_id', momentId);
+ request.append('captions', JSON.stringify({[momentId]: caption}));
+ request.append('tags', JSON.stringify(tags));
+ const token = await AsyncStorage.getItem('token');
+ let response = await fetch(MOMENTS_ENDPOINT, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ Authorization: 'Token ' + token,
+ },
+ body: request,
+ });
+ let statusCode = response.status;
+ return statusCode === 200 || statusCode === 201;
+ } catch (err) {
+ console.log(err);
+ }
+ return false;
+};
+
+export const loadMoments = async (userId: string, token: string) => {
try {
const response = await fetch(MOMENTS_ENDPOINT + '?user_id=' + userId, {
method: 'GET',
@@ -66,19 +94,14 @@ export const loadMoments: (
Authorization: 'Token ' + token,
},
});
- const status = response.status;
- if (status === 200) {
- const data = await response.json();
- moments = data;
- } else {
- console.log('Could not load moments!');
- return [];
+ if (response.status === 200) {
+ const typedData: MomentPostType[] = await response.json();
+ return typedData;
}
} catch (err) {
console.log(err);
- return [];
}
- return moments;
+ return [];
};
export const deleteMoment = async (momentId: string) => {
diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts
index c5c843f5..ccaa9135 100644
--- a/src/services/NotificationService.ts
+++ b/src/services/NotificationService.ts
@@ -1,5 +1,9 @@
import AsyncStorage from '@react-native-community/async-storage';
-import {NOTIFICATIONS_ENDPOINT} from '../constants';
+import {
+ NOTIFICATIONS_ENDPOINT,
+ NOTIFICATIONS_COUNT_ENDPOINT,
+ NOTIFICATIONS_DATE,
+} from '../constants';
import {NotificationType} from '../types';
export const getNotificationsData: () => Promise<NotificationType[]> =
@@ -29,3 +33,60 @@ export const getNotificationsData: () => Promise<NotificationType[]> =
return [];
}
};
+
+export const getNotificationsUnreadCount = async () => {
+ try {
+ const token = await AsyncStorage.getItem('token');
+ const response = await fetch(NOTIFICATIONS_COUNT_ENDPOINT, {
+ method: 'GET',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ });
+ if (response.status === 200) {
+ const data: any = await response.json();
+ const typedData: {
+ CMT?: number;
+ FRD_REQ?: number;
+ P_VIEW?: number;
+ MOM_TAG?: number;
+ } = {};
+ if (data.CMT) {
+ typedData.CMT = data.CMT;
+ }
+ if (data.FRD_REQ && data.FRD_REQ > 0) {
+ typedData.FRD_REQ = data.FRD_REQ;
+ }
+ if (data.P_VIEW && data.P_VIEW > 0) {
+ typedData.P_VIEW = data.P_VIEW;
+ }
+ if (data.MOM_TAG && data.MOM_TAG > 0) {
+ typedData.MOM_TAG = data.MOM_TAG;
+ }
+ return typedData;
+ }
+ } catch (error) {
+ console.log('Unable to fetch notifications');
+ }
+ return undefined;
+};
+
+export const setNotificationsReadDate: () => Promise<boolean> = async () => {
+ try {
+ const token = await AsyncStorage.getItem('token');
+ const response = await fetch(NOTIFICATIONS_DATE, {
+ method: 'POST',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ });
+ if (response.status === 204) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (error) {
+ console.log('Unable to fetch notifications');
+ return false;
+ }
+};
diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts
index e2902a2d..92a1e456 100644
--- a/src/store/initialStates.ts
+++ b/src/store/initialStates.ts
@@ -1,14 +1,17 @@
-import {CommentThreadType, UniversityType} from './../types/types';
import {
- MomentType,
NotificationType,
- ProfilePreviewType,
ProfileInfoType,
+ ProfilePreviewType,
ScreenType,
SocialAccountType,
UserType,
UserXType,
} from '../types';
+import {
+ CommentThreadType,
+ MomentPostType,
+ UniversityType,
+} from './../types/types';
export const NO_PROFILE: ProfileInfoType = {
biography: '',
@@ -29,7 +32,7 @@ export const NO_PROFILE: ProfileInfoType = {
is_private: true,
};
-export const EMPTY_MOMENTS_LIST = <MomentType[]>[];
+export const EMPTY_MOMENTS_LIST = <MomentPostType[]>[];
export const EMPTY_NOTIFICATIONS_LIST = <NotificationType[]>[];
diff --git a/src/types/types.ts b/src/types/types.ts
index e54c2201..171a9ff3 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -119,6 +119,16 @@ export interface MomentType {
thumbnail_url: string;
}
+export interface MomentPostType extends MomentType {
+ comments_count: number;
+ comment_preview: MomentCommentPreviewType;
+}
+
+export interface MomentCommentPreviewType {
+ commenter: ProfilePreviewType;
+ comment: string;
+}
+
export interface MomentTagType {
id: string;
user: ProfilePreviewType;
@@ -172,7 +182,7 @@ export enum ScreenType {
*/
export interface UserXType {
friends: ProfilePreviewType[];
- moments: MomentType[];
+ moments: MomentPostType[];
socialAccounts: Record<string, SocialAccountType>;
momentCategories: string[];
user: UserType;
@@ -254,7 +264,9 @@ export type TypeOfNotification =
// notification_object is MomentType
| 'MOM_TAG'
// notification_object is undefined
- | 'SYSTEM_MSG';
+ | 'SYSTEM_MSG'
+ // notification_object is undefined
+ | 'P_VIEW';
export type UniversityBadge = {
id: number;
diff --git a/src/utils/comments.tsx b/src/utils/comments.tsx
index 80786b74..28879622 100644
--- a/src/utils/comments.tsx
+++ b/src/utils/comments.tsx
@@ -80,9 +80,9 @@ export const renderTextWithMentions: React.FC<RenderProps> = ({
};
export const mentionPartTypes: (
- style: 'blue' | 'white',
+ theme: 'blue' | 'white',
component: 'caption' | 'comment',
-) => PartType[] = (style, component) => {
+) => PartType[] = (theme, component) => {
return [
{
trigger: '@',
@@ -91,17 +91,26 @@ export const mentionPartTypes: (
),
allowedSpacesCount: 0,
isInsertSpaceAfterMention: true,
- textStyle:
- style === 'blue'
- ? {
- color: TAGG_LIGHT_BLUE,
- top: normalize(3),
- }
- : {
- color: 'white',
- fontWeight: '800',
- top: normalize(7.5),
- },
+ textStyle: _textStyle(theme),
},
];
};
+
+const _textStyle: (theme: 'blue' | 'white') => StyleProp<TextStyle> = (
+ theme,
+) => {
+ switch (theme) {
+ case 'blue':
+ return {
+ color: TAGG_LIGHT_BLUE,
+ top: normalize(3),
+ };
+ case 'white':
+ default:
+ return {
+ color: 'white',
+ fontWeight: '800',
+ top: normalize(3),
+ };
+ }
+};
diff --git a/src/utils/common.ts b/src/utils/common.ts
index cfd9244a..1956e811 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -233,3 +233,8 @@ export const badgesToDisplayBadges = (
img: badgeToImgMap[b.category + b.name],
}));
};
+
+// Documentation: https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
+export const numberWithCommas = (digits: number) => {
+ return digits.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+};
diff --git a/src/utils/moments.ts b/src/utils/moments.ts
index 90d69519..9e8cc332 100644
--- a/src/utils/moments.ts
+++ b/src/utils/moments.ts
@@ -19,21 +19,21 @@ export const getTimePosted = (date_time: string) => {
// 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');
+ time = difference + 'm ago';
}
// 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');
+ time = difference + 'h ago';
}
// Any number of days
else if (difference >= 24 * 60 * 60 && difference < 24 * 60 * 60 * 3) {
difference = now.diff(datePosted, 'days');
- time = difference + (difference === 1 ? ' day' : ' days');
+ time = difference + 'd ago';
}
// More than 3 days
else if (difference >= 24 * 60 * 60 * 3) {
- time = datePosted.format('MMMM D, YYYY');
+ time = datePosted.format('M-D-YYYY');
}
return time;
};
diff --git a/src/utils/users.ts b/src/utils/users.ts
index 64ad10e9..c1c3b8bc 100644
--- a/src/utils/users.ts
+++ b/src/utils/users.ts
@@ -306,3 +306,21 @@ export const patchProfile = async (
return false;
});
};
+
+/**
+ * Returns the logged-in user's info in ProfilePreviewType from redux store.
+ * @param state the current state of the redux store
+ * @returns logged-in user in ProfilePreviewType
+ */
+export const getLoggedInUserAsProfilePreview: (
+ state: RootState,
+) => ProfilePreviewType = (state) => {
+ const nameSplit = state.user.profile.name.split(' ');
+ return {
+ id: state.user.user.userId,
+ username: state.user.user.username,
+ first_name: nameSplit[0],
+ last_name: nameSplit[1],
+ thumbnail_url: state.user.avatar ?? '', // in full res
+ };
+};