diff options
Diffstat (limited to 'src')
34 files changed, 680 insertions, 448 deletions
diff --git a/src/App.tsx b/src/App.tsx index ea3617dc..9510c193 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,57 @@ import {NavigationContainer} from '@react-navigation/native'; -import React from 'react'; +import React, {useState} from 'react'; import {Provider} from 'react-redux'; +import {Channel as ChannelType, StreamChat} from 'stream-chat'; +import {OverlayProvider} from 'stream-chat-react-native'; +import {STREAM_CHAT_API} from './constants'; import {navigationRef} from './RootNavigation'; import Routes from './routes'; import store from './store/configureStore'; +import { + ChatContextType, + LocalAttachmentType, + LocalChannelType, + LocalCommandType, + LocalEventType, + LocalMessageType, + LocalResponseType, + LocalUserType, +} from './types'; + +export const ChatContext = React.createContext({} as ChatContextType); const App = () => { + const [channel, setChannel] = useState< + ChannelType< + LocalAttachmentType, + LocalChannelType, + LocalCommandType, + LocalEventType, + LocalMessageType, + LocalResponseType, + LocalUserType + > + >(); + const chatClient = StreamChat.getInstance< + LocalAttachmentType, + LocalChannelType, + LocalCommandType, + LocalEventType, + LocalMessageType, + LocalResponseType, + LocalUserType + >(STREAM_CHAT_API); return ( /** * This is the provider from the redux store, it acts as the root provider for our application */ <Provider store={store}> <NavigationContainer ref={navigationRef}> - <Routes /> + <ChatContext.Provider value={{channel, setChannel, chatClient}}> + <OverlayProvider> + <Routes /> + </OverlayProvider> + </ChatContext.Provider> </NavigationContainer> </Provider> ); diff --git a/src/assets/images/cover-placeholder@2x.png b/src/assets/images/cover-placeholder@2x.png Binary files differindex 402ac1fe..70294346 100644 --- a/src/assets/images/cover-placeholder@2x.png +++ b/src/assets/images/cover-placeholder@2x.png diff --git a/src/assets/images/cover-placeholder@3x.png b/src/assets/images/cover-placeholder@3x.png Binary files differindex be87023d..66fa3ce1 100644 --- a/src/assets/images/cover-placeholder@3x.png +++ b/src/assets/images/cover-placeholder@3x.png diff --git a/src/components/common/BottomDrawer.tsx b/src/components/common/BottomDrawer.tsx index bef9434a..988c1e79 100644 --- a/src/components/common/BottomDrawer.tsx +++ b/src/components/common/BottomDrawer.tsx @@ -6,7 +6,7 @@ import { View, ViewProps, } from 'react-native'; -import Animated from 'react-native-reanimated'; +import Animated, {interpolateColors} from 'react-native-reanimated'; import BottomSheet from 'reanimated-bottom-sheet'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; @@ -50,6 +50,10 @@ const BottomDrawer: React.FC<BottomDrawerProps> = (props) => { ); }; + const backgroundColor = interpolateColors(bgAlpha, { + inputRange: [0, 1], + outputColorRange: ['rgba(0,0,0,0.3)', 'rgba(0,0,0,0)'], + }); return ( <Modal transparent @@ -75,17 +79,7 @@ const BottomDrawer: React.FC<BottomDrawerProps> = (props) => { onPress={() => { setIsOpen(false); }}> - <Animated.View - style={[ - styles.backgroundView, - { - backgroundColor: Animated.interpolateColors(bgAlpha, { - inputRange: [0, 1], - outputColorRange: ['rgba(0,0,0,0.3)', 'rgba(0,0,0,0)'], - }), - }, - ]} - /> + <Animated.View style={[styles.backgroundView, {backgroundColor}]} /> </TouchableWithoutFeedback> </Modal> ); diff --git a/src/components/common/GradientBorderButton.tsx b/src/components/common/GradientBorderButton.tsx index 32ac5c52..a5dbde9d 100644 --- a/src/components/common/GradientBorderButton.tsx +++ b/src/components/common/GradientBorderButton.tsx @@ -42,7 +42,7 @@ const GradientBorderButton: React.FC<GradientBorderButtonProps> = ({ }; const styles = StyleSheet.create({ container: { - marginVertical: 15, + marginVertical: 10, }, gradientContainer: { width: SCREEN_WIDTH / 2 - 40, diff --git a/src/components/common/TabsGradient.tsx b/src/components/common/TabsGradient.tsx index a95e8bc3..07c55042 100644 --- a/src/components/common/TabsGradient.tsx +++ b/src/components/common/TabsGradient.tsx @@ -14,7 +14,7 @@ const TabsGradient: React.FC = () => { }; const styles = StyleSheet.create({ gradient: { - position: 'absolute', + ...StyleSheet.absoluteFillObject, top: (SCREEN_HEIGHT / 10) * 9, height: SCREEN_HEIGHT / 10, width: SCREEN_WIDTH, diff --git a/src/components/index.ts b/src/components/index.ts index d5649323..47dc583b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,3 +6,4 @@ export * from './taggs'; export * from './comments'; export * from './moments'; export * from './suggestedPeople'; +export * from './messages'; diff --git a/src/components/messages/MessagesHeader.tsx b/src/components/messages/MessagesHeader.tsx new file mode 100644 index 00000000..d8445580 --- /dev/null +++ b/src/components/messages/MessagesHeader.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import {StyleSheet, View} from 'react-native'; +import {Text} from 'react-native-animatable'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {normalize} from '../../utils'; + +type MessagesHeaderProps = { + createChannel: () => void; +}; + +const MessagesHeader: React.FC<MessagesHeaderProps> = ({createChannel}) => { + return ( + <View style={styles.header}> + <Text style={styles.headerText}>Messages</Text> + <Text style={styles.unreadText}>2 unread</Text> + <View style={styles.flex} /> + <TouchableOpacity + style={styles.compose} + onPress={createChannel}> + <Text>Compose</Text> + </TouchableOpacity> + </View> + ); +}; + +const styles = StyleSheet.create({ + flex: { + flex: 1, + }, + header: { + marginHorizontal: '8%', + marginTop: '5%', + alignItems: 'center', + flexDirection: 'row', + }, + headerText: { + fontWeight: '700', + fontSize: normalize(18), + lineHeight: normalize(21), + }, + unreadText: { + color: '#8F01FF', + marginLeft: 10, + fontWeight: '700', + lineHeight: normalize(17), + fontSize: normalize(14), + }, + compose: {}, +}); + +export default MessagesHeader; diff --git a/src/components/messages/index.ts b/src/components/messages/index.ts new file mode 100644 index 00000000..2d6bb581 --- /dev/null +++ b/src/components/messages/index.ts @@ -0,0 +1 @@ +export {default as MessagesHeader} from './MessagesHeader'; diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index fef92dc1..05098d14 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -1,14 +1,10 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; -import { - LayoutChangeEvent, - NativeScrollEvent, - NativeSyntheticEvent, - RefreshControl, - StyleSheet, -} from 'react-native'; -import Animated from 'react-native-reanimated'; +import {LayoutChangeEvent, RefreshControl, StyleSheet} from 'react-native'; +import Animated, { + useSharedValue, + useAnimatedScrollHandler, +} from 'react-native-reanimated'; import {useDispatch, useSelector, useStore} from 'react-redux'; -import {COVER_HEIGHT} from '../../constants'; import { blockUnblockUser, loadFriendsData, @@ -20,12 +16,11 @@ import { NO_USER, } from '../../store/initialStates'; import {RootState} from '../../store/rootreducer'; -import {ContentProps} from '../../types'; +import {ScreenType} from '../../types'; import { canViewProfile, fetchUserX, getUserAsProfilePreviewType, - SCREEN_HEIGHT, userLogin, } from '../../utils'; import TaggsBar from '../taggs/TaggsBar'; @@ -35,8 +30,13 @@ import ProfileBody from './ProfileBody'; import ProfileCutout from './ProfileCutout'; import ProfileHeader from './ProfileHeader'; import PublicProfile from './PublicProfile'; +import {useScrollToTop} from '@react-navigation/native'; -const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { +interface ContentProps { + userXId: string | undefined; + screenType: ScreenType; +} +const Content: React.FC<ContentProps> = ({userXId, screenType}) => { const dispatch = useDispatch(); const { user = NO_USER, @@ -60,6 +60,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { * If scrolling is enabled. Set to false before scrolling up for the tutorial. */ const [scrollEnabled, setScrollEnabled] = useState<boolean>(true); + const y = useSharedValue<number>(0); /** * States @@ -109,45 +110,32 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { * updateUserXFriends updates friends list for the user. */ const handleBlockUnblock = async (callback?: () => void) => { - await dispatch( + dispatch( blockUnblockUser( loggedInUser, getUserAsProfilePreviewType(user, profile), isBlocked, ), ); - await dispatch(loadFriendsData(loggedInUser.userId)); - await dispatch(updateUserXFriends(user.userId, state)); + dispatch(loadFriendsData(loggedInUser.userId)); + dispatch(updateUserXFriends(user.userId, state)); if (callback) { callback(); } }; - const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { - /** - * Set the new y position - */ - const newY = e.nativeEvent.contentOffset.y; - y.setValue(newY); + const scrollHandler = useAnimatedScrollHandler((event) => { + y.value = event.contentOffset.y; + }); - /** - * Do not allow overflow of scroll on bottom of the screen - * SCREEN_HEIGHT - COVER_HEIGHT = Height of the scroll view - */ - if (newY >= SCREEN_HEIGHT - COVER_HEIGHT) { - setShouldBounce(false); - } else if (newY === 0) { - setShouldBounce(true); - } - }; + useScrollToTop(scrollViewRef); return ( <Animated.ScrollView ref={scrollViewRef} contentContainerStyle={styles.contentContainer} style={styles.container} - onScroll={(e) => handleScroll(e)} - bounces={shouldBounce} + onScroll={scrollHandler} showsVerticalScrollIndicator={false} scrollEventThrottle={1} stickyHeaderIndices={[4]} @@ -171,7 +159,6 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { /> <TaggsBar {...{y, profileBodyHeight, userXId, screenType}} - whiteRing={undefined} onLayout={onSocialsBarLayout} /> {canViewProfile(state, userXId, screenType) ? ( diff --git a/src/components/profile/Cover.tsx b/src/components/profile/Cover.tsx index ee804ff3..27777b64 100644 --- a/src/components/profile/Cover.tsx +++ b/src/components/profile/Cover.tsx @@ -27,7 +27,7 @@ const Cover: React.FC<CoverProps> = ({userXId, screenType}) => { const styles = StyleSheet.create({ container: { - position: 'absolute', + ...StyleSheet.absoluteFillObject, }, image: { width: IMAGE_WIDTH, diff --git a/src/components/profile/PublicProfile.tsx b/src/components/profile/PublicProfile.tsx index eceb2fc3..1c49bff5 100644 --- a/src/components/profile/PublicProfile.tsx +++ b/src/components/profile/PublicProfile.tsx @@ -100,7 +100,7 @@ const PublicProfile: React.FC<ContentProps> = ({ scrollViewRef.current ) { setScrollEnabled(false); - scrollViewRef.current.getNode().scrollTo({y: 0}); + scrollViewRef.current.scrollTo({y: 0}); navigation.navigate('MomentUploadPrompt', { screenType, momentCategory: momentCategories[0], diff --git a/src/components/search/RecentSearches.tsx b/src/components/search/RecentSearches.tsx index 84d35cac..6cea9338 100644 --- a/src/components/search/RecentSearches.tsx +++ b/src/components/search/RecentSearches.tsx @@ -6,6 +6,7 @@ import { StyleSheet, TouchableOpacityProps, ScrollView, + Keyboard, } from 'react-native'; import { PreviewType, @@ -15,7 +16,7 @@ import { } from '../../types'; import {TAGG_LIGHT_BLUE} from '../../constants'; import SearchResults from './SearchResults'; -import {SCREEN_HEIGHT} from '../../utils'; +import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; interface RecentSearchesProps extends TouchableOpacityProps { sectionTitle: PreviewType; @@ -25,35 +26,25 @@ interface RecentSearchesProps extends TouchableOpacityProps { } const RecentSearches: React.FC<RecentSearchesProps> = (props) => { - const {sectionTitle, recents, recentCategories, screenType} = props; + const {recents, recentCategories} = props; return ( - <ScrollView - style={styles.mainContainer} - contentContainerStyle={styles.contentContainer}> + <> <View style={styles.header}> - <Text style={styles.title}>{sectionTitle}</Text> + <Text style={styles.title}>Recent</Text> <TouchableOpacity {...props}> <Text style={styles.clear}>Clear all</Text> </TouchableOpacity> </View> - <SearchResults - results={recents} - categories={recentCategories} - previewType={sectionTitle} - screenType={screenType} - /> - </ScrollView> + <ScrollView + onScrollBeginDrag={Keyboard.dismiss} + contentContainerStyle={{paddingBottom: useBottomTabBarHeight()}}> + <SearchResults results={recents} categories={recentCategories} /> + </ScrollView> + </> ); }; const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - }, - contentContainer: { - paddingBottom: SCREEN_HEIGHT * 0.1, - flex: 1, - }, header: { paddingHorizontal: 25, paddingVertical: 5, diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 4824b56f..d441b07b 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -9,20 +9,23 @@ import { TextInputSubmitEditingEventData, TouchableOpacity, View, + ViewStyle, + LayoutChangeEvent, } from 'react-native'; import {normalize} from 'react-native-elements'; -import Animated, {interpolate} from 'react-native-reanimated'; +import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import Icon from 'react-native-vector-icons/Feather'; import {useSelector} from 'react-redux'; import {RootState} from '../../store/rootReducer'; -import {getSearchSuggestions, SCREEN_HEIGHT} from '../../utils'; +import {getSearchSuggestions} from '../../utils'; const AnimatedIcon = Animated.createAnimatedComponent(Icon); interface SearchBarProps extends TextInputProps { onCancel: () => void; - top: Animated.Value<number>; + animationProgress: Animated.SharedValue<number>; searching: boolean; + onLayout: (e: LayoutChangeEvent) => void; } const SearchBar: React.FC<SearchBarProps> = ({ onFocus, @@ -31,7 +34,8 @@ const SearchBar: React.FC<SearchBarProps> = ({ value, onCancel, searching, - top, + animationProgress, + onLayout, }) => { const handleSubmit = ( e: NativeSyntheticEvent<TextInputSubmitEditingEventData>, @@ -107,19 +111,15 @@ const SearchBar: React.FC<SearchBarProps> = ({ }, [searching]); /* - * Animated nodes used in search bar activation animation. + * On-search marginRight style ("cancel" button slides and fades in). */ - const marginRight: Animated.Node<number> = interpolate(top, { - inputRange: [-SCREEN_HEIGHT, 0], - outputRange: [0, 58], - }); - const opacity: Animated.Node<number> = interpolate(top, { - inputRange: [-SCREEN_HEIGHT, 0], - outputRange: [0, 1], - }); + const animatedStyles = useAnimatedStyle<ViewStyle>(() => ({ + marginRight: animationProgress.value * 58, + opacity: animationProgress.value, + })); return ( - <View style={styles.container}> + <View style={styles.container} onLayout={onLayout}> <Animated.View style={styles.inputContainer}> <AnimatedIcon name="search" @@ -131,13 +131,13 @@ const SearchBar: React.FC<SearchBarProps> = ({ style={[styles.input]} placeholderTextColor={'#828282'} onSubmitEditing={handleSubmit} - clearButtonMode="while-editing" + clearButtonMode="always" autoCapitalize="none" autoCorrect={false} {...{placeholder, value, onChangeText, onFocus, onBlur}} /> </Animated.View> - <Animated.View style={{marginRight, opacity}}> + <Animated.View style={animatedStyles}> <TouchableOpacity style={styles.cancelButton} onPress={onCancel}> <Text style={styles.cancelText}>Cancel</Text> </TouchableOpacity> @@ -151,6 +151,7 @@ const styles = StyleSheet.create({ height: 40, paddingHorizontal: 20, flexDirection: 'row', + zIndex: 2, }, inputContainer: { flexGrow: 1, diff --git a/src/components/search/SearchCategories.tsx b/src/components/search/SearchCategories.tsx index c747b34f..3d142981 100644 --- a/src/components/search/SearchCategories.tsx +++ b/src/components/search/SearchCategories.tsx @@ -3,29 +3,40 @@ import React, {useEffect, useState} from 'react'; import {StyleSheet, View} from 'react-native'; import {getSuggestedSearchBubbleSuggestions} from '../../services/ExploreService'; import {SearchCategoryType} from '../../types'; -import {SCREEN_WIDTH} from '../../utils'; import GradientBorderButton from '../common/GradientBorderButton'; +import {useSelector} from 'react-redux'; +import {RootState} from 'src/store/rootReducer'; interface SearchCategoriesProps { darkStyle?: boolean; - defaultButtons?: SearchCategoryType[]; + useSuggestions: boolean; } const SearchCategories: React.FC<SearchCategoriesProps> = ({ darkStyle = false, - defaultButtons, + useSuggestions, }) => { const navigation = useNavigation(); - const mtSearchCategory: (key: number) => SearchCategoryType = (key) => ({ + const { + profile: {university = ''}, + } = useSelector((state: RootState) => state.user); + const defaultButtons: SearchCategoryType[] = [21, 22, 23, 24].map( + (year, index) => ({ + id: index * -1, + name: `${university.split(' ')[0]} '${year}`, + category: university, + }), + ); + const createloadingCategory: (key: number) => SearchCategoryType = (key) => ({ id: key, name: '...', category: '...', }); const [buttons, setButtons] = useState<SearchCategoryType[]>([ - mtSearchCategory(-1), - mtSearchCategory(-2), - mtSearchCategory(-3), - mtSearchCategory(-4), + createloadingCategory(1), + createloadingCategory(2), + createloadingCategory(3), + createloadingCategory(4), ]); useEffect(() => { @@ -36,7 +47,7 @@ const SearchCategories: React.FC<SearchCategoriesProps> = ({ setButtons(localButtons); } }; - if (!defaultButtons) { + if (useSuggestions) { loadButtons(); } else { setButtons(defaultButtons); @@ -45,33 +56,34 @@ const SearchCategories: React.FC<SearchCategoriesProps> = ({ return ( <View style={styles.container}> - {buttons.map((searchCategory) => ( - <GradientBorderButton - key={searchCategory.id} - text={searchCategory.name} - darkStyle={darkStyle} - onPress={() => { - if (searchCategory.name !== '...') { - navigation.push('DiscoverUsers', { - searchCategory, - }); - } - }} - /> - ))} + <View style={styles.categoryContainer}> + {buttons.map((searchCategory, index) => ( + <GradientBorderButton + key={index} + text={searchCategory.name} + darkStyle={darkStyle} + onPress={() => { + if (searchCategory.name !== '...') { + navigation.push('DiscoverUsers', { + searchCategory, + }); + } + }} + /> + ))} + </View> </View> ); }; const styles = StyleSheet.create({ container: { - zIndex: 0, - top: '3%', - alignSelf: 'center', + paddingVertical: 20, + }, + categoryContainer: { flexDirection: 'row', - width: SCREEN_WIDTH * 0.9, - flexWrap: 'wrap', justifyContent: 'space-evenly', + flexWrap: 'wrap', }, }); export default SearchCategories; diff --git a/src/components/search/SearchResultList.tsx b/src/components/search/SearchResultList.tsx index 687b2285..a32760e1 100644 --- a/src/components/search/SearchResultList.tsx +++ b/src/components/search/SearchResultList.tsx @@ -1,15 +1,23 @@ import React, {useEffect, useState} from 'react'; -import {SectionList, StyleSheet, Text, View} from 'react-native'; +import { + SectionList, + StyleSheet, + Text, + View, + Keyboard, + SectionListData, +} from 'react-native'; import {useSelector} from 'react-redux'; import {RootState} from '../../store/rootreducer'; import {NO_RESULTS_FOUND} from '../../constants/strings'; import {PreviewType, ScreenType} from '../../types'; -import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import {normalize, SCREEN_WIDTH} from '../../utils'; import SearchResultsCell from './SearchResultCell'; +import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; interface SearchResultsProps { - results: Array<any> | undefined; - keyboardVisible: boolean; + // TODO: make sure results come in as same type, regardless of profile, category, badges + results: SectionListData<any>[]; previewType: PreviewType; screenType: ScreenType; } @@ -21,11 +29,8 @@ const sectionHeader: React.FC<Boolean> = (showBorder: Boolean) => { return null; }; -const SearchResultList: React.FC<SearchResultsProps> = ({ - results, - keyboardVisible, -}) => { - const [showEmptyView, setshowEmptyView] = useState(false); +const SearchResultList: React.FC<SearchResultsProps> = ({results}) => { + const [showEmptyView, setshowEmptyView] = useState<boolean>(false); const {user: loggedInUser} = useSelector((state: RootState) => state.user); useEffect(() => { @@ -38,57 +43,41 @@ const SearchResultList: React.FC<SearchResultsProps> = ({ } }, [results]); - return ( - <View style={styles.container}> - {showEmptyView && ( - <View style={styles.noResultsTextContainer}> - <Text style={styles.noResultsTextStyle}>{NO_RESULTS_FOUND}</Text> - </View> - )} - {!showEmptyView && ( - <SectionList - style={[ - {width: SCREEN_WIDTH}, - keyboardVisible ? styles.keyboardOpen : {}, - ]} - contentContainerStyle={styles.sectionListContentContainer} - sections={results} - keyExtractor={(item, index) => item.id + index} - renderItem={({section, item}) => { - return ( - <SearchResultsCell - profileData={item} - loggedInUser={loggedInUser} - /> - ); - }} - renderSectionHeader={({section: {data}}) => - sectionHeader(data.length !== 0) - } - /> - )} + return showEmptyView ? ( + <View style={styles.container} onTouchStart={Keyboard.dismiss}> + <Text style={styles.noResultsTextStyle}>{NO_RESULTS_FOUND}</Text> </View> + ) : ( + <SectionList + onScrollBeginDrag={Keyboard.dismiss} + contentContainerStyle={[{paddingBottom: useBottomTabBarHeight()}]} + sections={results} + keyExtractor={(item, index) => item.id + index} + renderItem={({item}) => { + return ( + <SearchResultsCell profileData={item} loggedInUser={loggedInUser} /> + ); + }} + renderSectionHeader={({section: {data}}) => + sectionHeader(data.length !== 0) + } + stickySectionHeadersEnabled={false} + /> ); }; const styles = StyleSheet.create({ container: { - height: SCREEN_HEIGHT, - paddingBottom: SCREEN_HEIGHT * 0.1, - }, - sectionListContentContainer: { - paddingBottom: SCREEN_HEIGHT * 0.15, - width: SCREEN_WIDTH, + flex: 1, + marginTop: 30, + alignItems: 'center', }, sectionHeaderStyle: { width: '100%', height: 0.5, - marginBottom: normalize(24), + marginVertical: 5, backgroundColor: '#C4C4C4', }, - keyboardOpen: { - marginBottom: SCREEN_HEIGHT * 0.35, - }, noResultsTextContainer: { justifyContent: 'center', flexDirection: 'row', diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx index ef518d8b..a73d0b40 100644 --- a/src/components/search/SearchResults.tsx +++ b/src/components/search/SearchResults.tsx @@ -1,18 +1,10 @@ import React from 'react'; -import { - ProfilePreviewType, - PreviewType, - ScreenType, - CategoryPreviewType, -} from '../../types'; -import {View} from 'react-native'; +import {ProfilePreviewType, CategoryPreviewType} from '../../types'; import SearchResultsCell from './SearchResultCell'; import {useSelector} from 'react-redux'; import {RootState} from '../../store/rootReducer'; interface SearchResultsProps { results: ProfilePreviewType[]; - previewType: PreviewType; - screenType: ScreenType; categories: CategoryPreviewType[]; } const SearchResults: React.FC<SearchResultsProps> = ({results, categories}) => { @@ -22,7 +14,7 @@ const SearchResults: React.FC<SearchResultsProps> = ({results, categories}) => { */ const {user: loggedInUser} = useSelector((state: RootState) => state.user); return ( - <View> + <> {categories .slice(0) .reverse() @@ -43,7 +35,7 @@ const SearchResults: React.FC<SearchResultsProps> = ({results, categories}) => { {...{loggedInUser}} /> ))} - </View> + </> ); }; diff --git a/src/components/search/SearchResultsBackground.tsx b/src/components/search/SearchResultsBackground.tsx index 2833553d..e5236295 100644 --- a/src/components/search/SearchResultsBackground.tsx +++ b/src/components/search/SearchResultsBackground.tsx @@ -1,28 +1,55 @@ import React from 'react'; -import {StyleSheet} from 'react-native'; -import Animated, {interpolate} from 'react-native-reanimated'; -import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import {StyleSheet, ViewStyle} from 'react-native'; +import Animated, { + useAnimatedStyle, + useDerivedValue, + interpolate, + Extrapolate, +} from 'react-native-reanimated'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; interface SearchResultsBackgroundProps { - top: Animated.Value<number>; + animationProgress: Animated.SharedValue<number>; + searchBarHeight: number; + searching: boolean; } const SearchResultsBackground: React.FC<SearchResultsBackgroundProps> = ({ - top, + animationProgress, + searchBarHeight, + searching, children, }) => { - const opacityBackground: Animated.Node<number> = interpolate(top, { - inputRange: [-SCREEN_HEIGHT, 0], - outputRange: [0, 1], - }); - const opacityContent: Animated.Node<number> = interpolate(top, { - inputRange: [-SCREEN_HEIGHT / 40, 0], - outputRange: [0, 1], - }); + const {top: topInset} = useSafeAreaInsets(); + /* + * On-search container style (opacity fade-in). + */ + const backgroundAnimatedStyles = useAnimatedStyle<ViewStyle>(() => ({ + opacity: animationProgress.value, + })); + /* + * Derived animation value for contentAnimatedStyles. + */ + const contentAnimationProgress = useDerivedValue<number>(() => + interpolate(animationProgress.value, [0.9, 1], [0, 1], Extrapolate.CLAMP), + ); + /* + * On-search content style (delayed opacity fade-in). + */ + const contentAnimatedStyles = useAnimatedStyle<ViewStyle>(() => ({ + opacity: contentAnimationProgress.value, + })); return ( <Animated.View - style={[styles.container, {opacity: opacityBackground, top}]}> - <Animated.View - style={[styles.contentContainer, {opacity: opacityContent}]}> + style={[ + styles.container, + backgroundAnimatedStyles, + { + // absolute: inset + search screen paddingTop + searchBar + padding + paddingTop: topInset + 15 + searchBarHeight + 10, + }, + ]} + pointerEvents={searching ? 'auto' : 'none'}> + <Animated.View style={[styles.contentContainer, contentAnimatedStyles]}> {children} </Animated.View> </Animated.View> @@ -30,15 +57,11 @@ const SearchResultsBackground: React.FC<SearchResultsBackgroundProps> = ({ }; const styles = StyleSheet.create({ container: { - height: SCREEN_HEIGHT, - width: SCREEN_WIDTH, - position: 'absolute', + ...StyleSheet.absoluteFillObject, backgroundColor: 'white', }, contentContainer: { flex: 1, - paddingVertical: 10, - paddingBottom: SCREEN_HEIGHT / 15, }, }); export default SearchResultsBackground; diff --git a/src/components/suggestedPeople/SPTaggsBar.tsx b/src/components/suggestedPeople/SPTaggsBar.tsx new file mode 100644 index 00000000..adac6dcf --- /dev/null +++ b/src/components/suggestedPeople/SPTaggsBar.tsx @@ -0,0 +1,133 @@ +import React, {useEffect, useState} from 'react'; +import {StyleSheet} from 'react-native'; +import Animated from 'react-native-reanimated'; +import {useDispatch, useSelector, useStore} from 'react-redux'; +import {INTEGRATED_SOCIAL_LIST, SOCIAL_LIST} from '../../constants'; +import {getLinkedSocials} from '../../services'; +import {loadIndividualSocial, updateSocial} from '../../store/actions'; +import {RootState} from '../../store/rootReducer'; +import {ScreenType} from '../../types'; +import {canViewProfile} from '../../utils'; +import Tagg from '../taggs/Tagg'; + +const {View, ScrollView} = Animated; +interface TaggsBarProps { + userXId: string | undefined; + screenType: ScreenType; + linkedSocials?: string[]; +} +const TaggsBar: React.FC<TaggsBarProps> = ({ + userXId, + screenType, + linkedSocials, +}) => { + let [taggs, setTaggs] = useState<Object[]>([]); + let [taggsNeedUpdate, setTaggsNeedUpdate] = useState(true); + const {user} = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId] : state.user, + ); + const state: RootState = useStore().getState(); + const allowTaggsNavigation = canViewProfile(state, userXId, screenType); + + const dispatch = useDispatch(); + + /** + * Updates the individual social that needs update + * If username is empty, update nonintegrated socials like Snapchat and TikTok + * @param socialType Type of the social that needs update + */ + const handleSocialUpdate = (socialType: string, username: string) => { + if (username !== '') { + dispatch(updateSocial(socialType, username)); + } else { + dispatch(loadIndividualSocial(user.userId, socialType)); + } + }; + + /** + * This useEffect should be called evey time the user being viewed is changed OR + * And update is triggered manually + */ + useEffect(() => { + const loadData = async () => { + const socials: string[] = linkedSocials + ? linkedSocials + : await getLinkedSocials(user.userId); + const unlinkedSocials = SOCIAL_LIST.filter( + (s) => socials.indexOf(s) === -1, + ); + let new_taggs = []; + let i = 0; + for (let social of socials) { + new_taggs.push( + <Tagg + key={i} + social={social} + userXId={userXId} + user={user} + isLinked={true} + isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1} + setTaggsNeedUpdate={setTaggsNeedUpdate} + setSocialDataNeedUpdate={handleSocialUpdate} + whiteRing={true} + allowNavigation={allowTaggsNavigation} + />, + ); + i++; + } + if (!userXId) { + for (let social of unlinkedSocials) { + new_taggs.push( + <Tagg + key={i} + social={social} + isLinked={false} + isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1} + setTaggsNeedUpdate={setTaggsNeedUpdate} + setSocialDataNeedUpdate={handleSocialUpdate} + userXId={userXId} + user={user} + whiteRing={true} + allowNavigation={allowTaggsNavigation} + />, + ); + i++; + } + } + setTaggs(new_taggs); + setTaggsNeedUpdate(false); + }; + if (user.userId) { + loadData(); + } + }, [taggsNeedUpdate, user]); + + return taggs.length > 0 ? ( + <View style={styles.spContainer}> + <ScrollView + horizontal + showsHorizontalScrollIndicator={false} + contentContainerStyle={styles.contentContainer}> + {taggs} + </ScrollView> + </View> + ) : ( + <></> + ); +}; + +const styles = StyleSheet.create({ + spContainer: { + shadowColor: '#000', + shadowRadius: 10, + shadowOffset: {width: 0, height: 2}, + zIndex: 1, + marginBottom: 25, + }, + contentContainer: { + alignItems: 'center', + paddingBottom: 5, + }, +}); + +export default TaggsBar; diff --git a/src/components/suggestedPeople/index.ts b/src/components/suggestedPeople/index.ts index 515f6fb4..339c9ae0 100644 --- a/src/components/suggestedPeople/index.ts +++ b/src/components/suggestedPeople/index.ts @@ -1,2 +1,3 @@ export {default as MutualFriends} from './MutualFriends'; export {default as BadgesDropdown} from './BadgesDropdown'; +export {default as SPTaggsBar} from './SPTaggsBar'; diff --git a/src/components/taggs/TaggPostFooter.tsx b/src/components/taggs/TaggPostFooter.tsx index ae9d889d..750f1793 100644 --- a/src/components/taggs/TaggPostFooter.tsx +++ b/src/components/taggs/TaggPostFooter.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {Linking, StyleSheet, View} from 'react-native'; +import {StyleSheet, View} from 'react-native'; import {Text} from 'react-native-animatable'; import {handleOpenSocialUrlOnBrowser} from '../../utils'; import {DateLabel} from '../common'; diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx index ec91b8e5..a5003fbb 100644 --- a/src/components/taggs/TaggsBar.tsx +++ b/src/components/taggs/TaggsBar.tsx @@ -1,6 +1,11 @@ -import React, {Fragment, useEffect, useState} from 'react'; -import {StyleSheet, LayoutChangeEvent} from 'react-native'; -import Animated from 'react-native-reanimated'; +import React, {useEffect, useState} from 'react'; +import {LayoutChangeEvent, StyleSheet} from 'react-native'; +import Animated, { + Extrapolate, + interpolate, + useAnimatedStyle, + useDerivedValue, +} from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {useDispatch, useSelector, useStore} from 'react-redux'; import { @@ -15,13 +20,12 @@ import {ScreenType} from '../../types'; import {canViewProfile} from '../../utils'; import Tagg from './Tagg'; -const {View, ScrollView, interpolate, Extrapolate} = Animated; +const {View, ScrollView} = Animated; interface TaggsBarProps { - y: Animated.Value<number>; + y: Animated.SharedValue<number>; profileBodyHeight: number; userXId: string | undefined; screenType: ScreenType; - whiteRing: boolean | undefined; linkedSocials?: string[]; onLayout: (event: LayoutChangeEvent) => void; } @@ -30,7 +34,6 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ profileBodyHeight, userXId, screenType, - whiteRing, linkedSocials, onLayout, }) => { @@ -43,7 +46,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ const allowTaggsNavigation = canViewProfile(state, userXId, screenType); const dispatch = useDispatch(); - + const insetTop = useSafeAreaInsets().top; /** * Updates the individual social that needs update * If username is empty, update nonintegrated socials like Snapchat and TikTok @@ -82,13 +85,13 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1} setTaggsNeedUpdate={setTaggsNeedUpdate} setSocialDataNeedUpdate={handleSocialUpdate} - whiteRing={whiteRing ? whiteRing : undefined} + whiteRing={false} allowNavigation={allowTaggsNavigation} />, ); i++; } - if (!userXId && !whiteRing) { + if (!userXId) { for (let social of unlinkedSocials) { new_taggs.push( <Tagg @@ -100,7 +103,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ setSocialDataNeedUpdate={handleSocialUpdate} userXId={userXId} user={user} - whiteRing={whiteRing ? whiteRing : undefined} + whiteRing={false} allowNavigation={allowTaggsNavigation} />, ); @@ -114,65 +117,55 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ loadData(); } }, [taggsNeedUpdate, user]); - - const shadowOpacity: Animated.Node<number> = interpolate(y, { - inputRange: [ - PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight, - PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight + 20, - ], - outputRange: [0, 0.2], - extrapolate: Extrapolate.CLAMP, - }); - const paddingTop: Animated.Node<number> = interpolate(y, { - inputRange: [ - PROFILE_CUTOUT_BOTTOM_Y + - profileBodyHeight - - (useSafeAreaInsets().top + 10), - PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight, - ], - outputRange: [10, useSafeAreaInsets().top], - extrapolate: Extrapolate.CLAMP, - }); + const paddingTopStylesProgress = useDerivedValue(() => + interpolate( + y.value, + [PROFILE_CUTOUT_BOTTOM_Y, PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight], + [0, 1], + Extrapolate.CLAMP, + ), + ); + const shadowOpacityStylesProgress = useDerivedValue(() => + interpolate( + y.value, + [ + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight, + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight + insetTop, + ], + [0, 1], + Extrapolate.CLAMP, + ), + ); + const animatedStyles = useAnimatedStyle(() => ({ + shadowOpacity: shadowOpacityStylesProgress.value / 5, + paddingTop: paddingTopStylesProgress.value * insetTop, + })); return taggs.length > 0 ? ( - <View - style={ - whiteRing - ? [styles.spContainer] - : [styles.container, {shadowOpacity, paddingTop}] - } - onLayout={onLayout}> + <View style={[styles.container, animatedStyles]} onLayout={onLayout}> <ScrollView horizontal showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.contentContainer}> + contentContainerStyle={[styles.contentContainer]}> {taggs} </ScrollView> </View> ) : ( - <Fragment /> + <></> ); }; const styles = StyleSheet.create({ - spContainer: { - shadowColor: '#000', - shadowRadius: 10, - shadowOffset: {width: 0, height: 2}, - zIndex: 1, - marginBottom: 25, - }, container: { backgroundColor: 'white', shadowColor: '#000', shadowRadius: 10, shadowOffset: {width: 0, height: 2}, zIndex: 1, - paddingBottom: 5, }, contentContainer: { alignItems: 'center', - paddingBottom: 5, + paddingBottom: 15, }, }); diff --git a/src/constants/api.ts b/src/constants/api.ts index 22890c33..ffe47687 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,6 +1,12 @@ /* eslint-disable */ -// const BASE_URL: string = 'http://3.22.188.127/'; // prod server -const BASE_URL: string = 'http://127.0.0.1:8000/'; // local server + +// Dev +const BASE_URL: string = 'http://127.0.0.1:8000/'; +export const STREAM_CHAT_API = 'g2hvnyqx9cmv' + +// Prod +// const BASE_URL: string = 'http://app-prod.tagg.id/'; +// export const STREAM_CHAT_API = 'ur3kg5qz8x5v' const API_URL: string = BASE_URL + 'api/'; export const LOGIN_ENDPOINT: string = API_URL + 'login/'; diff --git a/src/screens/chat/ChatListScreen.tsx b/src/screens/chat/ChatListScreen.tsx index c49c790d..9011ed4a 100644 --- a/src/screens/chat/ChatListScreen.tsx +++ b/src/screens/chat/ChatListScreen.tsx @@ -1,8 +1,13 @@ -import React from 'react'; -import {View, StyleSheet, Text, TouchableOpacity} from 'react-native'; +import AsyncStorage from '@react-native-community/async-storage'; import {StackNavigationProp} from '@react-navigation/stack'; - +import React, {useContext, useEffect, useMemo, useState} from 'react'; +import {SafeAreaView, StatusBar, StyleSheet, View} from 'react-native'; +import {useStore} from 'react-redux'; +import {ChannelList, Chat} from 'stream-chat-react-native'; +import {ChatContext} from '../../App'; +import {MessagesHeader} from '../../components/messages'; import {MainStackParams} from '../../routes'; +import {RootState} from '../../store/rootReducer'; type ChatListScreenNavigationProp = StackNavigationProp< MainStackParams, @@ -15,19 +20,80 @@ interface ChatListScreenProps { * Screen that displays all of the user's active conversations. */ const ChatListScreen: React.FC<ChatListScreenProps> = ({navigation}) => { + const {chatClient, setChannel} = useContext(ChatContext); + const [clientReady, setClientReady] = useState(false); + const state: RootState = useStore().getState(); + const loggedInUserId = state.user.user.userId; + + const memoizedFilters = useMemo( + () => ({ + members: {$in: [loggedInUserId]}, + type: 'messaging', + }), + [], + ); + + useEffect(() => { + const setupClient = async () => { + const chatToken = await AsyncStorage.getItem('chatToken'); + await chatClient.connectUser( + { + id: loggedInUserId, + }, + chatToken, + ); + return setClientReady(true); + }; + if (!clientReady) { + setupClient().catch((err) => { + console.error(err); + }); + } + }, []); + return ( - <View style={styles.container}> - <Text style={styles.placeholder}>I am the chat list.</Text> - <TouchableOpacity - style={styles.button} - onPress={() => navigation.navigate('Chat')}> - <Text>Let's go to a conversation!</Text> - </TouchableOpacity> + <View style={styles.background}> + <SafeAreaView> + <StatusBar barStyle="dark-content" /> + <MessagesHeader + createChannel={() => { + // TODO: (CHAT) change me + const channel = chatClient.channel('messaging', { + name: 'Awesome channel with foobar', + members: [loggedInUserId, 'd5295557-59ce-49fc-aa8a-442874dbffc3'], + }); + channel.create(); + }} + /> + {clientReady && ( + <Chat client={chatClient}> + <View style={styles.chatContainer}> + <ChannelList + filters={memoizedFilters} + onSelect={(channel) => { + setChannel(channel); + navigation.navigate('Chat'); + }} + options={{ + presence: true, + state: true, + watch: true, + }} + sort={{last_message_at: -1}} + /> + </View> + </Chat> + )} + </SafeAreaView> </View> ); }; const styles = StyleSheet.create({ + background: { + flex: 1, + backgroundColor: 'white', + }, container: { flex: 1, justifyContent: 'center', @@ -43,6 +109,10 @@ const styles = StyleSheet.create({ padding: 15, borderRadius: 5, }, + chatContainer: { + height: '100%', + marginTop: 10, + }, }); export default ChatListScreen; diff --git a/src/screens/chat/ChatScreen.tsx b/src/screens/chat/ChatScreen.tsx index af83f504..eeb1a7d6 100644 --- a/src/screens/chat/ChatScreen.tsx +++ b/src/screens/chat/ChatScreen.tsx @@ -1,7 +1,14 @@ -import React from 'react'; -import {View, StyleSheet, Text} from 'react-native'; -import {StackNavigationProp} from '@react-navigation/stack'; - +import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; +import {StackNavigationProp, useHeaderHeight} from '@react-navigation/stack'; +import React, {useContext} from 'react'; +import {StyleSheet, View} from 'react-native'; +import { + Channel, + Chat, + MessageInput, + MessageList, +} from 'stream-chat-react-native'; +import {ChatContext} from '../../App'; import {MainStackParams} from '../../routes'; type ChatScreenNavigationProp = StackNavigationProp<MainStackParams, 'Chat'>; @@ -12,22 +19,25 @@ interface ChatScreenProps { * Screen that displays all of the user's active conversations. */ const ChatScreen: React.FC<ChatScreenProps> = () => { + const {channel, chatClient} = useContext(ChatContext); + const headerHeight = useHeaderHeight(); + const tabbarHeight = useBottomTabBarHeight(); + return ( - <View style={styles.container}> - <Text style={styles.placeholder}>I am a chat!</Text> + <View style={[styles.container, {paddingBottom: tabbarHeight}]}> + <Chat client={chatClient}> + <Channel channel={channel} keyboardVerticalOffset={headerHeight}> + <MessageList onThreadSelect={() => {}} /> + <MessageInput /> + </Channel> + </Chat> </View> ); }; const styles = StyleSheet.create({ container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - placeholder: { - fontSize: 14, - fontWeight: 'bold', + backgroundColor: 'white', }, }); diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 49ca5ff4..dd2bb2e4 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -160,6 +160,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { await AsyncStorage.setItem('token', data.token); await AsyncStorage.setItem('userId', data.UserID); await AsyncStorage.setItem('username', username); + await AsyncStorage.setItem('chatToken', data.chatToken); } if (statusCode === 200 && data.isOnboarded) { diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx index 0d6af21e..6d9ef020 100644 --- a/src/screens/profile/ProfileScreen.tsx +++ b/src/screens/profile/ProfileScreen.tsx @@ -1,15 +1,9 @@ import React from 'react'; import {StatusBar} from 'react-native'; -import Animated from 'react-native-reanimated'; import {Content, TabsGradient} from '../../components'; import {RouteProp} from '@react-navigation/native'; import {MainStackParams} from '../../routes/'; -/**r - * Profile Screen for a user's profile - * including posts, messaging, and settings - */ - type ProfileScreenRouteProps = RouteProp<MainStackParams, 'Profile'>; interface ProfileOnboardingProps { @@ -19,31 +13,11 @@ interface ProfileOnboardingProps { const ProfileScreen: React.FC<ProfileOnboardingProps> = ({route}) => { const {screenType} = route.params; let {userXId} = route.params; - const y = Animated.useValue(0); - - /** - * This is a double safety check to avoid app crash. - * Checks if the required userXId is present in the store, if not userXId is set to dummy id - */ - // if (userXId && !(userXId in useStore().getState().userX[screenType])) { - // userXId = DUMMY_USERID; - // } - - /** - * Code under useFocusEffect gets executed every time the screen comes under focus / is being viewed by the user. - * This is done to reset the users stored in our store for the Search screen. - * Read more about useFocusEffect here : https://reactnavigation.org/docs/function-after-focusing-screen/ - */ - // useFocusEffect(() => { - // if (!userXId) { - // dispatch(resetScreenType(screenType)); - // } - // }); return ( <> <StatusBar barStyle="dark-content" /> - <Content {...{y, userXId, screenType}} /> + <Content {...{userXId, screenType}} /> <TabsGradient /> </> ); diff --git a/src/screens/profile/SettingsScreen.tsx b/src/screens/profile/SettingsScreen.tsx index 05e051b5..ecc3bafd 100644 --- a/src/screens/profile/SettingsScreen.tsx +++ b/src/screens/profile/SettingsScreen.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useContext} from 'react'; import { SafeAreaView, SectionList, @@ -17,6 +17,7 @@ import {BackgroundGradientType} from '../../types'; import {normalize, SCREEN_HEIGHT} from '../../utils/layouts'; import SettingsCell from './SettingsCell'; import {useNavigation} from '@react-navigation/core'; +import {ChatContext} from '../../App'; const SettingsScreen: React.FC = () => { const dispatch = useDispatch(); @@ -24,6 +25,7 @@ const SettingsScreen: React.FC = () => { const {suggested_people_linked} = useSelector( (state: RootState) => state.user.profile, ); + const {chatClient} = useContext(ChatContext); return ( <> @@ -49,7 +51,7 @@ const SettingsScreen: React.FC = () => { <TouchableOpacity style={styles.logoutContainerStyles} onPress={() => { - dispatch(logout()); + dispatch(logout(chatClient)); navigation.reset({ index: 0, routes: [{name: 'SuggestedPeople'}], diff --git a/src/screens/search/DiscoverUsers.tsx b/src/screens/search/DiscoverUsers.tsx index b87bfc37..f67585f2 100644 --- a/src/screens/search/DiscoverUsers.tsx +++ b/src/screens/search/DiscoverUsers.tsx @@ -126,7 +126,7 @@ const DiscoverUsers: React.FC<DiscoverUsersProps> = ({route}) => { ListFooterComponent={() => ( <> <Text style={styles.otherGroups}>Other Groups</Text> - <SearchCategories darkStyle={true} /> + <SearchCategories useSuggestions={true} darkStyle={true} /> </> )} /> diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx index 4f0cabb4..f7e1c467 100644 --- a/src/screens/search/SearchScreen.tsx +++ b/src/screens/search/SearchScreen.tsx @@ -1,8 +1,19 @@ import AsyncStorage from '@react-native-community/async-storage'; import {useFocusEffect} from '@react-navigation/native'; import React, {useEffect, useState} from 'react'; -import {Keyboard, ScrollView, StatusBar, StyleSheet} from 'react-native'; -import Animated, {Easing, timing} from 'react-native-reanimated'; +import { + Keyboard, + StatusBar, + StyleSheet, + LayoutChangeEvent, + SectionListData, +} from 'react-native'; +import { + useSharedValue, + withTiming, + Easing, + runOnJS, +} from 'react-native-reanimated'; import {SafeAreaView} from 'react-native-safe-area-context'; import {useDispatch, useSelector} from 'react-redux'; import { @@ -13,22 +24,14 @@ import { SearchResultsBackground, TabsGradient, } from '../../components'; -import {SEARCH_ENDPOINT, TAGG_LIGHT_BLUE} from '../../constants'; +import {SEARCH_ENDPOINT} from '../../constants'; import {loadSearchResults} from '../../services'; import {resetScreenType} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; -import { - CategoryPreviewType, - ProfilePreviewType, - ScreenType, - SearchCategoryType, -} from '../../types'; +import {CategoryPreviewType, ProfilePreviewType, ScreenType} from '../../types'; import { getRecentlySearchedCategories, getRecentlySearchedUsers, - normalize, - SCREEN_HEIGHT, - SCREEN_WIDTH, } from '../../utils'; /** @@ -38,11 +41,8 @@ import { const SearchScreen: React.FC = () => { const {recentSearches} = useSelector((state: RootState) => state.taggUsers); - const { - profile: {university = ''}, - } = useSelector((state: RootState) => state.user); const [query, setQuery] = useState<string>(''); - const [results, setResults] = useState<Array<any> | undefined>(undefined); + const [results, setResults] = useState<SectionListData<any>[] | undefined>(); const [recents, setRecents] = useState<Array<ProfilePreviewType>>( recentSearches ?? [], ); @@ -50,26 +50,12 @@ const SearchScreen: React.FC = () => { CategoryPreviewType[] >([]); const [searching, setSearching] = useState(false); - const top = Animated.useValue(-SCREEN_HEIGHT); - const defaultButtons: SearchCategoryType[] = [21, 22, 23, 24].map((year) => ({ - id: -1, - name: `${university.split(' ')[0]} '${year}`, - category: university, - })); - const [keyboardVisible, setKeyboardVisible] = React.useState( - 'keyboardVisible', - ); - useEffect(() => { - const showKeyboard = () => setKeyboardVisible('keyboardVisibleTrue'); - Keyboard.addListener('keyboardWillShow', showKeyboard); - return () => Keyboard.removeListener('keyboardWillShow', showKeyboard); - }, []); + /* + * Animated value + */ + const animationProgress = useSharedValue<number>(0); + const [searchBarHeight, setSearchBarHeight] = useState<number>(0); - useEffect(() => { - const hideKeyboard = () => setKeyboardVisible('keyboardVisibleFalse'); - Keyboard.addListener('keyboardWillHide', hideKeyboard); - return () => Keyboard.removeListener('keyboardWillHide', hideKeyboard); - }, []); const dispatch = useDispatch(); /* @@ -122,12 +108,22 @@ const SearchScreen: React.FC = () => { useEffect(() => { if (searching) { loadRecentlySearched().then(() => { - timing(top, topInConfig).start(); + animationProgress.value = withTiming(1, { + duration: 180, + easing: Easing.bezier(0.31, 0.14, 0.66, 0.82), + }); }); } else { setQuery(''); handleBlur(); - timing(top, topOutConfig).start(() => setResults(undefined)); + animationProgress.value = withTiming( + 0, + {duration: 180, easing: Easing.inOut(Easing.ease)}, + () => { + 'worklet'; + runOnJS(setResults)(undefined); + }, + ); } }, [searching]); @@ -153,16 +149,6 @@ const SearchScreen: React.FC = () => { } }; - const topInConfig = { - duration: 180, - toValue: 0, - easing: Easing.bezier(0.31, 0.14, 0.66, 0.82), - }; - const topOutConfig = { - duration: 180, - toValue: -SCREEN_HEIGHT, - easing: Easing.inOut(Easing.ease), - }; const handleFocus = () => { setSearching(true); }; @@ -172,9 +158,12 @@ const SearchScreen: React.FC = () => { const handleCancel = () => { setSearching(false); }; + const onSearchBarLayout = (e: LayoutChangeEvent) => { + setSearchBarHeight(e.nativeEvent.layout.height); + }; return ( - <SafeAreaView style={styles.screenContainer}> + <SafeAreaView style={styles.container}> <StatusBar barStyle="dark-content" /> <SearchBar onCancel={handleCancel} @@ -182,98 +171,39 @@ const SearchScreen: React.FC = () => { onBlur={handleBlur} onFocus={handleFocus} value={query} - {...{top, searching}} + onLayout={onSearchBarLayout} + {...{animationProgress, searching}} /> - <ScrollView - scrollEnabled={!searching} - keyboardShouldPersistTaps={'always'} - stickyHeaderIndices={[4]} - contentContainerStyle={styles.contentContainer} - showsVerticalScrollIndicator={false}> - <SearchCategories defaultButtons={defaultButtons} /> - <SearchResultsBackground {...{top}}> - {results === undefined && - recents.length + recentCategories.length !== 0 ? ( + <SearchCategories useSuggestions={false} /> + <SearchResultsBackground + {...{searching, searchBarHeight, animationProgress}}> + {results === undefined ? ( + recents.length + recentCategories.length > 0 && ( <RecentSearches sectionTitle="Recent" onPress={clearRecentlySearched} screenType={ScreenType.Search} {...{recents, recentCategories}} /> - ) : ( - <SearchResultList - {...{results}} - keyboardVisible={keyboardVisible === 'keyboardVisibleTrue'} - previewType={'Search'} - screenType={ScreenType.Search} - /> - )} - </SearchResultsBackground> - </ScrollView> + ) + ) : ( + <SearchResultList + {...{results}} + previewType={'Search'} + screenType={ScreenType.Search} + /> + )} + </SearchResultsBackground> <TabsGradient /> </SafeAreaView> ); }; const styles = StyleSheet.create({ - screenContainer: { + container: { + flex: 1, paddingTop: 15, backgroundColor: '#fff', }, - contentContainer: { - height: SCREEN_HEIGHT, - paddingTop: '2%', - paddingBottom: SCREEN_HEIGHT / 3, - paddingHorizontal: '3%', - }, - header: { - marginVertical: 20, - zIndex: 1, - }, - recentsHeaderContainer: { - flexDirection: 'row', - }, - recentsHeader: { - fontSize: 17, - fontWeight: 'bold', - flexGrow: 1, - }, - clear: { - fontSize: normalize(17), - fontWeight: 'bold', - color: TAGG_LIGHT_BLUE, - }, - image: { - width: SCREEN_WIDTH, - height: SCREEN_WIDTH, - }, - textContainer: { - marginTop: '10%', - }, - headerText: { - color: '#fff', - fontSize: normalize(32), - fontWeight: '600', - textAlign: 'center', - marginBottom: '4%', - marginHorizontal: '10%', - }, - subtext: { - color: '#fff', - fontSize: normalize(16), - fontWeight: '600', - textAlign: 'center', - marginHorizontal: '10%', - }, - cancelButton: { - position: 'absolute', - height: '100%', - justifyContent: 'center', - paddingHorizontal: 5, - }, - cancelText: { - color: '#818181', - fontWeight: '600', - }, }); export default SearchScreen; diff --git a/src/screens/suggestedPeople/SPBody.tsx b/src/screens/suggestedPeople/SPBody.tsx index 824f8b1c..fa69d812 100644 --- a/src/screens/suggestedPeople/SPBody.tsx +++ b/src/screens/suggestedPeople/SPBody.tsx @@ -3,26 +3,18 @@ import React, {Fragment, useEffect, useMemo, useState} from 'react'; import {StyleSheet, Text, View} from 'react-native'; import {Image} from 'react-native-animatable'; import {TouchableOpacity} from 'react-native-gesture-handler'; -import Animated from 'react-native-reanimated'; -import {useStore} from 'react-redux'; import RequestedButton from '../../assets/ionicons/requested-button.svg'; -import {TaggsBar} from '../../components'; +import {SPTaggsBar} from '../../components'; import {BadgesDropdown, MutualFriends} from '../../components/suggestedPeople'; import {BADGE_DATA} from '../../constants/badges'; -import {RootState} from '../../store/rootReducer'; import { ProfilePreviewType, ScreenType, SuggestedPeopleDataType, UniversityBadge, } from '../../types'; -import { - canViewProfile, - isIPhoneX, - normalize, - SCREEN_HEIGHT, - SCREEN_WIDTH, -} from '../../utils'; +import {isIPhoneX, normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import {useSharedValue} from 'react-native-reanimated'; interface SPBodyProps { item: SuggestedPeopleDataType; @@ -56,7 +48,6 @@ const SPBody: React.FC<SPBodyProps> = ({ }[] >([]); const navigation = useNavigation(); - const state: RootState = useStore().getState(); useEffect(() => { const newBadges: {badge: UniversityBadge; img: any}[] = []; const findBadgeIcons = (badge: UniversityBadge) => { @@ -159,12 +150,9 @@ const SPBody: React.FC<SPBodyProps> = ({ {user.id !== loggedInUserId && <FriendButton />} </View> </View> - <TaggsBar - y={Animated.useValue(0)} + <SPTaggsBar userXId={user.id === loggedInUserId ? undefined : user.id} - profileBodyHeight={0} screenType={screenType} - whiteRing={true} linkedSocials={social_links} /> <View style={styles.marginManager}> diff --git a/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx b/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx index a296351f..d6812f41 100644 --- a/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx +++ b/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx @@ -226,7 +226,7 @@ const SuggestedPeopleScreen: React.FC = () => { /> ); }} - keyExtractor={(item, index) => index.toString()} + keyExtractor={(_, index) => index.toString()} showsVerticalScrollIndicator={false} onViewableItemsChanged={onViewableItemsChanged} onEndReached={() => setPage(page + 1)} diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index e7d985ac..3ebd4190 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -1,4 +1,5 @@ import AsyncStorage from '@react-native-community/async-storage'; +import {StreamChat} from 'stream-chat'; import {Action, ThunkAction} from '@reduxjs/toolkit'; import { getProfilePic, @@ -164,13 +165,16 @@ export const updateReplyPosted = ( } }; -export const logout = (): ThunkAction< - Promise<void>, - RootState, - unknown, - Action<string> -> => async (dispatch) => { +export const logout = ( + client?: StreamChat, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { try { + // do our best effort here to gracefully disconnect the user + if (client) { + client.disconnectUser(); + } await AsyncStorage.clear(); dispatch({type: userLoggedIn.type, payload: {userId: '', username: ''}}); } catch (error) { diff --git a/src/types/types.ts b/src/types/types.ts index b012acaf..1a352808 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,4 +1,5 @@ import Animated from 'react-native-reanimated'; +import {Channel as ChannelType, StreamChat} from 'stream-chat'; export interface UserType { userId: string; @@ -289,3 +290,41 @@ export type ContactType = { export type UniversityBadgeType = 'Search' | 'Crest'; export type BadgeDataType = Record<UniversityType, any[]>; + +// Stream Chat Types +export type LocalAttachmentType = Record<string, unknown>; +export type LocalChannelType = Record<string, unknown>; +export type LocalCommandType = string; +export type LocalEventType = Record<string, unknown>; +export type LocalMessageType = Record<string, unknown>; +export type LocalResponseType = Record<string, unknown>; +export type LocalUserType = Record<string, unknown>; + +export type ChatContextType = { + channel: + | ChannelType< + LocalAttachmentType, + LocalChannelType, + LocalCommandType, + LocalEventType, + LocalMessageType, + LocalResponseType, + LocalUserType + > + | undefined; + setChannel: React.Dispatch< + React.SetStateAction< + | ChannelType< + LocalAttachmentType, + LocalChannelType, + LocalCommandType, + LocalEventType, + LocalMessageType, + LocalResponseType, + LocalUserType + > + | undefined + > + >; + chatClient: StreamChat; +}; |