diff options
author | Ivan Chen <ivan@tagg.id> | 2021-06-11 18:12:24 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-11 18:12:24 -0400 |
commit | 78f32c1400eff46d4c768b78fbaf672826c74285 (patch) | |
tree | 00e62c1821d4973d214fdd47f8293749972c1925 /src | |
parent | 4add0eed33032012fb945fb02a928aed426b9465 (diff) | |
parent | 24f79b9cb3456b3901155ed2e4c8fc66710b97b2 (diff) |
Merge pull request #466 from IvanIFChen/tma904-moment-comment-revamp
[TMA-904] Moment Comment Revamp
Diffstat (limited to 'src')
21 files changed, 458 insertions, 300 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/components/comments/AddComment.tsx b/src/components/comments/AddComment.tsx index b229d010..9667046c 100644 --- a/src/components/comments/AddComment.tsx +++ b/src/components/comments/AddComment.tsx @@ -24,10 +24,21 @@ import {MentionInputControlled} from './MentionInputControlled'; export interface AddCommentProps { momentId: string; placeholderText: string; + callback?: (message: string) => void; + onFocus?: () => void; + isKeyboardAvoiding?: boolean; + theme?: 'dark' | 'white'; } -const AddComment: React.FC<AddCommentProps> = ({momentId, placeholderText}) => { - const {setShouldUpdateAllComments, commentTapped} = +const AddComment: React.FC<AddCommentProps> = ({ + momentId, + placeholderText, + callback = (_) => null, + onFocus = () => null, + isKeyboardAvoiding = true, + theme = 'white', +}) => { + const {setShouldUpdateAllComments = () => null, commentTapped} = useContext(CommentContext); const [inReplyToMention, setInReplyToMention] = useState(''); const [comment, setComment] = useState(''); @@ -50,13 +61,15 @@ const AddComment: React.FC<AddCommentProps> = ({momentId, placeholderText}) => { if (trimmed === '') { return; } + const message = inReplyToMention + trimmed; const postedComment = await postComment( - inReplyToMention + trimmed, + message, objectId, isReplyingToComment || isReplyingToReply, ); if (postedComment) { + callback(message); setComment(''); setInReplyToMention(''); @@ -100,43 +113,63 @@ const AddComment: React.FC<AddCommentProps> = ({momentId, placeholderText}) => { } }, [isReplyingToComment, isReplyingToReply, commentTapped]); - return ( - <KeyboardAvoidingView - behavior={Platform.OS === 'ios' ? 'padding' : 'height'} - keyboardVerticalOffset={SCREEN_HEIGHT * 0.1}> - <View - style={[ - styles.container, - keyboardVisible ? styles.whiteBackround : {}, - ]}> - <View style={styles.textContainer}> - <Avatar style={styles.avatar} uri={avatar} /> - <MentionInputControlled - containerStyle={styles.text} - placeholder={placeholderText} - value={inReplyToMention + comment} - onChange={(newText: string) => { - // skipping the `inReplyToMention` text - setComment( - newText.substring(inReplyToMention.length, newText.length), - ); - }} - inputRef={ref} - partTypes={mentionPartTypes('blue')} - /> + const mainContent = () => ( + <View + style={[ + theme === 'white' ? styles.containerWhite : styles.containerDark, + keyboardVisible && theme !== 'dark' ? styles.whiteBackround : {}, + ]}> + <View style={styles.textContainer}> + <Avatar style={styles.avatar} uri={avatar} /> + <MentionInputControlled + containerStyle={styles.text} + placeholderTextColor={theme === 'dark' ? '#828282' : undefined} + placeholder={placeholderText} + value={inReplyToMention + comment} + onFocus={onFocus} + onChange={(newText: string) => { + // skipping the `inReplyToMention` text + setComment( + newText.substring(inReplyToMention.length, newText.length), + ); + }} + inputRef={ref} + partTypes={mentionPartTypes('blue')} + /> + {(theme === 'white' || (theme === 'dark' && keyboardVisible)) && ( <View style={styles.submitButton}> - <TouchableOpacity style={styles.submitButton} onPress={addComment}> + <TouchableOpacity + style={ + comment === '' + ? [styles.submitButton, styles.greyButton] + : styles.submitButton + } + disabled={comment === ''} + onPress={addComment}> <UpArrowIcon width={35} height={35} color={'white'} /> </TouchableOpacity> </View> - </View> + )} </View> + </View> + ); + return isKeyboardAvoiding ? ( + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + keyboardVerticalOffset={SCREEN_HEIGHT * 0.1}> + {mainContent()} </KeyboardAvoidingView> + ) : ( + mainContent() ); }; const styles = StyleSheet.create({ - container: { + containerDark: { + alignItems: 'center', + width: SCREEN_WIDTH, + }, + containerWhite: { backgroundColor: '#f7f7f7', alignItems: 'center', width: SCREEN_WIDTH, @@ -176,6 +209,9 @@ const styles = StyleSheet.create({ marginVertical: '2%', alignSelf: 'flex-end', }, + greyButton: { + backgroundColor: 'grey', + }, whiteBackround: { backgroundColor: '#fff', }, 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 6293f799..ebd93844 100644 --- a/src/components/comments/index.ts +++ b/src/components/comments/index.ts @@ -1,3 +1,2 @@ -export {default as CommentsCount} from '../comments/CommentsCount'; export {default as CommentTile} from './CommentTile'; export {default as AddComment} from './AddComment'; 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/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..e53ed258 --- /dev/null +++ b/src/components/moments/MomentCommentPreview.tsx @@ -0,0 +1,97 @@ +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; + commentPreview: MomentCommentPreviewType | null; + screenType: ScreenType; +} + +const MomentCommentPreview: React.FC<MomentCommentPreviewProps> = ({ + momentId, + commentsCount, + commentPreview, + screenType, +}) => { + const navigation = useNavigation(); + const state = useStore().getState(); + const commentCountText = + 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 !== null && ( + <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 b659177d..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,17 +34,14 @@ 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(); - }, [item]); + loadMomentTags(moment.moment_id).then((response) => { + setTags(response ? response : []); + }); + }, []); /* * Check if loggedInUser has been tagged in the picture and set the id @@ -71,50 +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} - style={styles.postHeader} momentTagId={momentTagId} removeTag={removeTag} - moment={item} + moment={moment} tags={tags} /> <MomentPostContent style={styles.postContent} + moment={moment} screenType={screenType} - moment={item} 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 d831d7ee..aca2999c 100644 --- a/src/components/moments/MomentPostContent.tsx +++ b/src/components/moments/MomentPostContent.tsx @@ -1,71 +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, MomentType, 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; - moment: MomentType; + moment: MomentPostType; momentTags: MomentTagType[]; + index: number; } const MomentPostContent: React.FC<MomentPostContentProps> = ({ screenType, - style, moment, + style, momentTags, + index, }) => { - const [elapsedTime, setElapsedTime] = useState(''); - const [comments_count, setCommentsCount] = useState(''); const [tags, setTags] = useState<MomentTagType[]>(momentTags); const state: RootState = useStore().getState(); const navigation = useNavigation(); const dispatch = useDispatch(); 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(moment.moment_id, false); - setCommentsCount(count); - }; - setElapsedTime(getTimePosted(moment.date_created)); - fetchCommentsCount(); - }, [moment.date_created, moment.moment_id]); - - 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,70 +92,89 @@ const MomentPostContent: React.FC<MomentPostContentProps> = ({ {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={moment.moment_id} - screenType={screenType} - /> - <Text style={styles.text}>{elapsedTime}</Text> - </View> - {renderTextWithMentions({ - value: moment.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 cde7639c..5f26951a 100644 --- a/src/components/moments/MomentPostHeader.tsx +++ b/src/components/moments/MomentPostHeader.tsx @@ -64,6 +64,7 @@ const MomentPostHeader: React.FC<MomentPostHeaderProps> = ({ style={styles.avatar} userXId={userXId} screenType={screenType} + editable={false} /> <Text style={styles.headerText}>{username}</Text> </TouchableOpacity> @@ -75,7 +76,7 @@ const MomentPostHeader: React.FC<MomentPostHeaderProps> = ({ removeTag={removeTag} dismissScreenAndUpdate={() => { dispatch(loadUserMoments(loggedInUserId)); - navigation.pop(); + navigation.goBack(); }} screenType={screenType} moment={moment} 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/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/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/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/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 c66d2786..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 ( @@ -86,11 +86,7 @@ export const patchMoment = async ( return false; }; -export const loadMoments: ( - userId: string, - token: string, -) => Promise<MomentType[]> = async (userId, token) => { - let moments: MomentType[] = []; +export const loadMoments = async (userId: string, token: string) => { try { const response = await fetch(MOMENTS_ENDPOINT + '?user_id=' + userId, { method: 'GET', @@ -98,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/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 fd75ab50..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; diff --git a/src/utils/comments.tsx b/src/utils/comments.tsx index 5c17cefe..910b44e7 100644 --- a/src/utils/comments.tsx +++ b/src/utils/comments.tsx @@ -79,8 +79,8 @@ export const renderTextWithMentions: React.FC<RenderProps> = ({ ); }; -export const mentionPartTypes: (style: 'blue' | 'white') => PartType[] = ( - style, +export const mentionPartTypes: (theme: 'blue' | 'white') => PartType[] = ( + theme, ) => { return [ { @@ -88,17 +88,26 @@ export const mentionPartTypes: (style: 'blue' | 'white') => PartType[] = ( renderSuggestions: (props) => <TaggTypeahead {...props} />, 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/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 + }; +}; |