diff options
-rw-r--r-- | src/constants/api.ts | 25 | ||||
-rw-r--r-- | src/constants/strings.ts | 1 | ||||
-rw-r--r-- | src/routes/main/MainStackNavigator.tsx | 1 | ||||
-rw-r--r-- | src/routes/main/MainStackScreen.tsx | 421 | ||||
-rw-r--r-- | src/screens/chat/ChatListScreen.tsx | 15 | ||||
-rw-r--r-- | src/screens/chat/ChatResultsCell.tsx | 117 | ||||
-rw-r--r-- | src/screens/chat/ChatResultsList.tsx | 102 | ||||
-rw-r--r-- | src/screens/chat/ChatSearchBar.tsx | 112 | ||||
-rw-r--r-- | src/screens/chat/NewChatModal.tsx | 161 | ||||
-rw-r--r-- | src/screens/chat/index.ts | 4 | ||||
-rw-r--r-- | src/utils/common.ts | 30 |
11 files changed, 764 insertions, 225 deletions
diff --git a/src/constants/api.ts b/src/constants/api.ts index ffe47687..43294386 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -2,7 +2,7 @@ // Dev const BASE_URL: string = 'http://127.0.0.1:8000/'; -export const STREAM_CHAT_API = 'g2hvnyqx9cmv' +export const STREAM_CHAT_API = 'g2hvnyqx9cmv'; // Prod // const BASE_URL: string = 'http://app-prod.tagg.id/'; @@ -24,7 +24,9 @@ export const PROFILE_PHOTO_THUMBNAIL_ENDPOINT: string = export const GET_IG_POSTS_ENDPOINT: string = API_URL + 'posts-ig/'; export const GET_FB_POSTS_ENDPOINT: string = API_URL + 'posts-fb/'; export const GET_TWITTER_POSTS_ENDPOINT: string = API_URL + 'posts-twitter/'; -export const SEARCH_ENDPOINT: string = API_URL + 'search/v2/'; +export const SEARCH_ENDPOINT: string = API_URL + 'search/'; +export const SEARCH_ENDPOINT_MESSAGES: string = API_URL + 'search/messages/'; +export const SEARCH_ENDPOINT_SUGGESTED: string = API_URL + 'search/suggested/'; export const MOMENTS_ENDPOINT: string = API_URL + 'moments/'; export const MOMENT_THUMBNAIL_ENDPOINT: string = API_URL + 'moment-thumbnail/'; export const VERIFY_INVITATION_CODE_ENDPOUNT: string = API_URL + 'verify-code/'; @@ -37,20 +39,24 @@ export const PASSWORD_RESET_ENDPOINT: string = API_URL + 'password-reset/'; export const MOMENT_CATEGORY_ENDPOINT: string = API_URL + 'moment-category/'; export const NOTIFICATIONS_ENDPOINT: string = API_URL + 'notifications/'; export const DISCOVER_ENDPOINT: string = API_URL + 'discover/'; -export const SEARCH_BUTTONS_ENDPOPINT: string = DISCOVER_ENDPOINT + 'search_buttons/'; +export const SEARCH_BUTTONS_ENDPOPINT: string = + DISCOVER_ENDPOINT + 'search_buttons/'; export const WAITLIST_USER_ENDPOINT: string = API_URL + 'waitlist-user/'; export const COMMENT_THREAD_ENDPOINT: string = API_URL + 'reply/'; export const USERS_FROM_CONTACTS_ENDPOINT: string = API_URL + 'user_contacts/find_friends/'; export const INVITE_FRIEND_ENDPOINT: string = -API_URL + 'user_contacts/invite_friend/'; + API_URL + 'user_contacts/invite_friend/'; // Suggested People export const SP_USERS_ENDPOINT: string = API_URL + 'suggested_people/'; -export const SP_UPDATE_PICTURE_ENDPOINT: string = SP_USERS_ENDPOINT + 'update_picture/'; -export const SP_MUTUAL_BADGE_HOLDERS_ENDPOINT: string = SP_USERS_ENDPOINT + 'get_mutual_badge_holders/'; +export const SP_UPDATE_PICTURE_ENDPOINT: string = + SP_USERS_ENDPOINT + 'update_picture/'; +export const SP_MUTUAL_BADGE_HOLDERS_ENDPOINT: string = + SP_USERS_ENDPOINT + 'get_mutual_badge_holders/'; export const ADD_BADGES_ENDPOINT: string = SP_USERS_ENDPOINT + 'add_badges/'; -export const UPDATE_BADGES_ENDPOINT: string = SP_USERS_ENDPOINT + 'update_badges/'; +export const UPDATE_BADGES_ENDPOINT: string = + SP_USERS_ENDPOINT + 'update_badges/'; // Register as FCM device export const FCM_ENDPOINT: string = API_URL + 'fcm/'; @@ -71,6 +77,7 @@ export const LINK_IG_OAUTH: string = `https://www.instagram.com/oauth/authorize/ export const LINK_FB_OAUTH: string = `https://www.facebook.com/v8.0/dialog/oauth?client_id=1308555659343609&redirect_uri=${DEEPLINK}&scope=user_posts,public_profile&response_type=code`; export const LINK_TWITTER_OAUTH: string = API_URL + 'link-twitter-request/'; -// Profile Links -export const COMMUNITY_GUIDELINES: string = 'https://www.tagg.id/community-guidelines'; +// Profile Links +export const COMMUNITY_GUIDELINES: string = + 'https://www.tagg.id/community-guidelines'; export const PRIVACY_POLICY: string = 'https://www.tagg.id/privacy-policy'; diff --git a/src/constants/strings.ts b/src/constants/strings.ts index 4f792dcc..300ceb90 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -17,6 +17,7 @@ export const ERROR_DUP_OLD_PWD = 'You may not use a previously used password'; export const ERROR_EMAIL_IN_USE = 'Email already in use, please try another one'; export const ERROR_FAILED_LOGIN_INFO = 'Login failed, please try re-entering your login information'; export const ERROR_FAILED_TO_COMMENT = 'Unable to post comment, refresh and try again!'; +export const ERROR_FAILED_TO_CREATE_CHANNEL = 'Failed to create a channel, Please try again!'; export const ERROR_FAILED_TO_DELETE_COMMENT = 'Unable to delete comment, refresh and try again!'; export const ERROR_INVALID_INVITATION_CODE = 'Invitation code invalid, try again or talk to the friend that sent it 😬'; export const ERROR_INVALID_LOGIN = 'Invalid login, Please login again'; diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index 01b28fd4..64ad9198 100644 --- a/src/routes/main/MainStackNavigator.tsx +++ b/src/routes/main/MainStackNavigator.tsx @@ -95,6 +95,7 @@ export type MainStackParams = { SPWelcomeScreen: {}; ChatList: undefined; Chat: undefined; + NewChatModal: undefined; }; export const MainStack = createStackNavigator<MainStackParams>(); diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx index 48c57920..37867151 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -1,7 +1,7 @@ import AsyncStorage from '@react-native-community/async-storage'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationOptions} from '@react-navigation/stack'; -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {StyleSheet, Text} from 'react-native'; import {normalize} from 'react-native-elements'; import BackIcon from '../../assets/icons/back-arrow.svg'; @@ -11,6 +11,8 @@ import { BadgeSelection, CaptionScreen, CategorySelection, + ChatListScreen, + ChatScreen, CreateCustomCategory, DiscoverUsers, EditProfile, @@ -19,18 +21,17 @@ import { InviteFriendsScreen, MomentCommentsScreen, MomentUploadPromptScreen, + NewChatModal, NotificationsScreen, - ProfileScreen, PrivacyScreen, + ProfileScreen, RequestContactsAccess, SearchScreen, + SettingsScreen, SocialMediaTaggs, SuggestedPeopleScreen, SuggestedPeopleUploadPictureScreen, SuggestedPeopleWelcomeScreen, - SettingsScreen, - ChatListScreen, - ChatScreen, } from '../../screens'; import MutualBadgeHolders from '../../screens/suggestedPeople/MutualBadgeHolders'; import {ScreenType} from '../../types'; @@ -52,7 +53,6 @@ type MainStackRouteProps = RouteProp<MainStackParams, 'Profile'>; interface MainStackProps { route: MainStackRouteProps; } - const MainStackScreen: React.FC<MainStackProps> = ({route}) => { const {screenType} = route.params; @@ -64,6 +64,10 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { 'true', ); + useEffect(() => { + loadResponseToAccessContacts(); + }, []); + const loadResponseToAccessContacts = () => { AsyncStorage.getItem('respondedToAccessContacts') .then((value) => { @@ -75,8 +79,6 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { }); }; - loadResponseToAccessContacts(); - const initialRouteName = (() => { switch (screenType) { case ScreenType.Profile: @@ -106,213 +108,228 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { }), }; - return ( - <MainStack.Navigator - screenOptions={{ - headerShown: false, - gestureResponseDistance: {horizontal: SCREEN_WIDTH * 0.6}, - }} - mode="card" - initialRouteName={initialRouteName}> - <MainStack.Screen - name="Profile" - component={ProfileScreen} - initialParams={{screenType}} - options={{ - ...headerBarOptions('white', ''), + const newChatModalStyle: StackNavigationOptions = { + cardStyle: {backgroundColor: 'rgba(0, 0, 0, 0.5)'}, + cardOverlayEnabled: true, + animationEnabled: false, + }; + + const mainStackScreen = () => { + return ( + <MainStack.Navigator + screenOptions={{ + headerShown: false, + gestureResponseDistance: {horizontal: SCREEN_WIDTH * 0.6}, }} - /> - {isSuggestedPeopleTab && - (respondedToAccessContacts && respondedToAccessContacts === 'true' ? ( + mode="card" + initialRouteName={initialRouteName}> + <MainStack.Screen + name="Profile" + component={ProfileScreen} + initialParams={{screenType}} + options={{ + ...headerBarOptions('white', ''), + }} + /> + {isSuggestedPeopleTab && + (respondedToAccessContacts && respondedToAccessContacts === 'true' ? ( + <MainStack.Screen + name="SuggestedPeople" + component={SuggestedPeopleScreen} + initialParams={{screenType}} + /> + ) : ( + <MainStack.Screen + name="SuggestedPeople" + component={RequestContactsAccess} + initialParams={{screenType}} + /> + ))} + {isNotificationsTab && ( <MainStack.Screen - name="SuggestedPeople" - component={SuggestedPeopleScreen} + name="Notifications" + component={NotificationsScreen} initialParams={{screenType}} /> - ) : ( + )} + {isSearchTab && ( <MainStack.Screen - name="SuggestedPeople" - component={RequestContactsAccess} + name="Search" + component={SearchScreen} initialParams={{screenType}} /> - ))} - {isNotificationsTab && ( + )} + <MainStack.Screen + name="DiscoverUsers" + component={DiscoverUsers} + options={{ + ...headerBarOptions('white', 'Discover Users'), + }} + /> + <MainStack.Screen + name="SettingsScreen" + component={SettingsScreen} + options={{ + ...headerBarOptions('white', 'Settings and Privacy'), + }} + /> <MainStack.Screen - name="Notifications" - component={NotificationsScreen} + name="PrivacyScreen" + component={PrivacyScreen} + options={{ + ...headerBarOptions('white', 'Privacy'), + }} + /> + <MainStack.Screen + name="AccountTypeScreen" + component={AccountType} + options={{ + ...headerBarOptions('white', 'Account Type'), + }} + /> + <MainStack.Screen + name="AnimatedTutorial" + component={AnimatedTutorial} + options={{ + ...tutorialModalStyle, + }} initialParams={{screenType}} /> - )} - {isSearchTab && ( <MainStack.Screen - name="Search" - component={SearchScreen} + name="CaptionScreen" + component={CaptionScreen} + options={{ + ...modalStyle, + gestureEnabled: false, + }} + /> + <MainStack.Screen + name="SocialMediaTaggs" + component={SocialMediaTaggs} initialParams={{screenType}} + options={{ + ...headerBarOptions('white', ''), + headerStyle: {height: AvatarHeaderHeight}, + }} /> - )} - <MainStack.Screen - name="DiscoverUsers" - component={DiscoverUsers} - options={{ - ...headerBarOptions('white', 'Discover Users'), - }} - /> - <MainStack.Screen - name="SettingsScreen" - component={SettingsScreen} - options={{ - ...headerBarOptions('white', 'Settings and Privacy'), - }} - /> - <MainStack.Screen - name="PrivacyScreen" - component={PrivacyScreen} - options={{ - ...headerBarOptions('white', 'Privacy'), - }} - /> - <MainStack.Screen - name="AccountTypeScreen" - component={AccountType} - options={{ - ...headerBarOptions('white', 'Account Type'), - }} - /> - <MainStack.Screen - name="AnimatedTutorial" - component={AnimatedTutorial} - options={{ - ...tutorialModalStyle, - }} - initialParams={{screenType}} - /> - <MainStack.Screen - name="CaptionScreen" - component={CaptionScreen} - options={{ - ...modalStyle, - gestureEnabled: false, - }} - /> - <MainStack.Screen - name="SocialMediaTaggs" - component={SocialMediaTaggs} - initialParams={{screenType}} - options={{ - ...headerBarOptions('white', ''), - headerStyle: {height: AvatarHeaderHeight}, - }} - /> - <MainStack.Screen - name="CategorySelection" - component={CategorySelection} - options={{ - ...headerBarOptions('white', ''), - }} - /> - <MainStack.Screen - name="CreateCustomCategory" - component={CreateCustomCategory} - options={{ - ...headerBarOptions('white', ''), - }} - /> - <MainStack.Screen - name="IndividualMoment" - component={IndividualMoment} - initialParams={{screenType}} - options={{ - ...modalStyle, - gestureEnabled: false, - }} - /> - <MainStack.Screen - name="MomentCommentsScreen" - component={MomentCommentsScreen} - initialParams={{screenType}} - options={{ - ...headerBarOptions('black', 'Comments'), - }} - /> - <MainStack.Screen - name="MomentUploadPrompt" - component={MomentUploadPromptScreen} - initialParams={{screenType}} - options={{ - ...modalStyle, - }} - /> - <MainStack.Screen - name="FriendsListScreen" - component={FriendsListScreen} - initialParams={{screenType}} - options={{ - ...headerBarOptions('black', 'Friends'), - }} - /> - <MainStack.Screen - name="InviteFriendsScreen" - component={InviteFriendsScreen} - initialParams={{screenType}} - options={{ - ...headerBarOptions('black', 'Invites'), - }} - /> - <MainStack.Screen - name="RequestContactsAccess" - component={RequestContactsAccess} - initialParams={{screenType}} - /> - <MainStack.Screen - name="EditProfile" - component={EditProfile} - options={{ - ...headerBarOptions('white', 'Edit Profile'), - }} - /> - <MainStack.Screen - name="UpdateSPPicture" - component={SuggestedPeopleUploadPictureScreen} - initialParams={{editing: true}} - options={{ - ...headerBarOptions('white', ''), - }} - /> - <MainStack.Screen - name="BadgeSelection" - component={BadgeSelection} - initialParams={{editing: true}} - options={{ - ...headerBarOptions('white', ''), - }} - /> - <MainStack.Screen - name="MutualBadgeHolders" - component={MutualBadgeHolders} - options={{...modalStyle}} - /> - <MainStack.Screen - name="SPWelcomeScreen" - component={SuggestedPeopleWelcomeScreen} - options={{ - ...headerBarOptions('white', ''), - }} - /> - <MainStack.Screen - name="ChatList" - component={ChatListScreen} - options={{headerTitle: 'Chats'}} - /> - <MainStack.Screen - name="Chat" - component={ChatScreen} - options={{ - ...headerBarOptions('black', ''), - headerStyle: {height: ChatHeaderHeight}, - }} - /> - </MainStack.Navigator> - ); + <MainStack.Screen + name="CategorySelection" + component={CategorySelection} + options={{ + ...headerBarOptions('white', ''), + }} + /> + <MainStack.Screen + name="CreateCustomCategory" + component={CreateCustomCategory} + options={{ + ...headerBarOptions('white', ''), + }} + /> + <MainStack.Screen + name="IndividualMoment" + component={IndividualMoment} + initialParams={{screenType}} + options={{ + ...modalStyle, + gestureEnabled: false, + }} + /> + <MainStack.Screen + name="MomentCommentsScreen" + component={MomentCommentsScreen} + initialParams={{screenType}} + options={{ + ...headerBarOptions('black', 'Comments'), + }} + /> + <MainStack.Screen + name="MomentUploadPrompt" + component={MomentUploadPromptScreen} + initialParams={{screenType}} + options={{ + ...modalStyle, + }} + /> + <MainStack.Screen + name="FriendsListScreen" + component={FriendsListScreen} + initialParams={{screenType}} + options={{ + ...headerBarOptions('black', 'Friends'), + }} + /> + <MainStack.Screen + name="InviteFriendsScreen" + component={InviteFriendsScreen} + initialParams={{screenType}} + options={{ + ...headerBarOptions('black', 'Invites'), + }} + /> + <MainStack.Screen + name="RequestContactsAccess" + component={RequestContactsAccess} + initialParams={{screenType}} + /> + <MainStack.Screen + name="EditProfile" + component={EditProfile} + options={{ + ...headerBarOptions('white', 'Edit Profile'), + }} + /> + <MainStack.Screen + name="UpdateSPPicture" + component={SuggestedPeopleUploadPictureScreen} + initialParams={{editing: true}} + options={{ + ...headerBarOptions('white', ''), + }} + /> + <MainStack.Screen + name="BadgeSelection" + component={BadgeSelection} + initialParams={{editing: true}} + options={{ + ...headerBarOptions('white', ''), + }} + /> + <MainStack.Screen + name="MutualBadgeHolders" + component={MutualBadgeHolders} + options={{...modalStyle}} + /> + <MainStack.Screen + name="SPWelcomeScreen" + component={SuggestedPeopleWelcomeScreen} + options={{ + ...headerBarOptions('white', ''), + }} + /> + <MainStack.Screen + name="ChatList" + component={ChatListScreen} + options={{headerTitle: 'Chats'}} + /> + <MainStack.Screen + name="Chat" + component={ChatScreen} + options={{ + ...headerBarOptions('black', ''), + headerStyle: {height: ChatHeaderHeight}, + }} + /> + <MainStack.Screen + name="NewChatModal" + component={NewChatModal} + options={{headerShown: false, ...newChatModalStyle}} + /> + </MainStack.Navigator> + ); + }; + + return mainStackScreen(); }; export const headerBarOptions: ( diff --git a/src/screens/chat/ChatListScreen.tsx b/src/screens/chat/ChatListScreen.tsx index 3290116b..daea9984 100644 --- a/src/screens/chat/ChatListScreen.tsx +++ b/src/screens/chat/ChatListScreen.tsx @@ -19,6 +19,7 @@ import { LocalUserType, } from '../../types'; +import NewChatModal from './NewChatModal'; type ChatListScreenNavigationProp = StackNavigationProp< MainStackParams, 'ChatList' @@ -29,8 +30,10 @@ interface ChatListScreenProps { /* * Screen that displays all of the user's active conversations. */ -const ChatListScreen: React.FC<ChatListScreenProps> = () => { - const {chatClient} = useContext(ChatContext); +const ChatListScreen: React.FC<ChatListScreenProps> = ({navigation}) => { + const {chatClient, setChannel} = useContext(ChatContext); + const [modalVisible, setChatModalVisible] = useState(false); + const [clientReady, setClientReady] = useState(false); const state: RootState = useStore().getState(); const loggedInUserId = state.user.user.userId; @@ -67,12 +70,7 @@ const ChatListScreen: React.FC<ChatListScreenProps> = () => { <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(); + setChatModalVisible(true); }} /> {clientReady && ( @@ -100,6 +98,7 @@ const ChatListScreen: React.FC<ChatListScreenProps> = () => { </View> </Chat> )} + <NewChatModal {...{modalVisible, setChatModalVisible}} /> </SafeAreaView> <TabsGradient /> </View> diff --git a/src/screens/chat/ChatResultsCell.tsx b/src/screens/chat/ChatResultsCell.tsx new file mode 100644 index 00000000..d947c122 --- /dev/null +++ b/src/screens/chat/ChatResultsCell.tsx @@ -0,0 +1,117 @@ +import {useNavigation} from '@react-navigation/native'; +import React, {useContext, useEffect, useState} from 'react'; +import {Alert, Image, StyleSheet, Text, View} from 'react-native'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {ChatContext} from '../../App'; +import {ERROR_FAILED_TO_CREATE_CHANNEL} from '../../constants/strings'; +import {loadImageFromURL} from '../../services'; +import {ProfilePreviewType, UserType} from '../../types'; +import {createChannel, normalize, SCREEN_WIDTH} from '../../utils'; +import {defaultUserProfile} from '../../utils/users'; + +interface ChatResults { + profileData: ProfilePreviewType; + loggedInUser: UserType; + setChatModalVisible: Function; +} + +const ChatResultsCell: React.FC<ChatResults> = ({ + profileData: {id, username, first_name, last_name, thumbnail_url}, + loggedInUser, + setChatModalVisible, +}) => { + const [avatar, setAvatar] = useState<string | undefined>(undefined); + const {chatClient, setChannel} = useContext(ChatContext); + + useEffect(() => { + (async () => { + if (thumbnail_url !== undefined) { + try { + const response = await loadImageFromURL(thumbnail_url); + if (response) { + setAvatar(response); + } + } catch (error) { + console.log('Error while downloading ', error); + throw error; + } + } + })(); + }, [thumbnail_url]); + + const navigation = useNavigation(); + const createChannelIfNotPresentAndNavigate = async () => { + try { + setChatModalVisible(false); + const channel = await createChannel(loggedInUser.userId, id, chatClient); + setChannel(channel); + setTimeout(() => { + navigation.navigate('Chat'); + }, 100); + } catch (error) { + Alert.alert(ERROR_FAILED_TO_CREATE_CHANNEL); + } + }; + + const userCell = () => { + return ( + <TouchableOpacity + onPress={createChannelIfNotPresentAndNavigate} + style={styles.cellContainer}> + <Image + defaultSource={defaultUserProfile()} + source={{uri: avatar}} + style={styles.imageContainer} + /> + <View style={[styles.initialTextContainer, styles.multiText]}> + <Text style={styles.initialTextStyle}>{`@${username}`}</Text> + <Text style={styles.secondaryTextStyle}> + {first_name + ' ' + last_name} + </Text> + </View> + </TouchableOpacity> + ); + }; + + return userCell(); +}; + +const styles = StyleSheet.create({ + cellContainer: { + flexDirection: 'row', + paddingHorizontal: 25, + paddingVertical: 15, + width: SCREEN_WIDTH, + }, + imageContainer: { + width: SCREEN_WIDTH * 0.112, + height: SCREEN_WIDTH * 0.112, + borderRadius: (SCREEN_WIDTH * 0.112) / 2, + }, + categoryBackground: { + backgroundColor: 'rgba(196, 196, 196, 0.45)', + justifyContent: 'center', + alignItems: 'center', + }, + categoryImage: { + width: '40%', + height: '40%', + }, + initialTextContainer: { + marginLeft: SCREEN_WIDTH * 0.08, + flexDirection: 'column', + justifyContent: 'center', + }, + initialTextStyle: { + fontWeight: '500', + fontSize: normalize(14), + }, + secondaryTextStyle: { + fontWeight: '500', + fontSize: normalize(12), + color: '#828282', + }, + multiText: {justifyContent: 'space-between'}, +}); + +export default ChatResultsCell; diff --git a/src/screens/chat/ChatResultsList.tsx b/src/screens/chat/ChatResultsList.tsx new file mode 100644 index 00000000..b9970772 --- /dev/null +++ b/src/screens/chat/ChatResultsList.tsx @@ -0,0 +1,102 @@ +import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; +import React, {useEffect, useState} from 'react'; +import { + Keyboard, + SectionList, + SectionListData, + StyleSheet, + Text, + View, +} from 'react-native'; +import {useSelector} from 'react-redux'; +import {NO_RESULTS_FOUND} from '../../constants/strings'; +import {RootState} from '../../store/rootreducer'; +import {PreviewType, ScreenType} from '../../types'; +import {normalize, SCREEN_WIDTH} from '../../utils'; +import ChatResultsCell from './ChatResultsCell'; + +interface ChatResultsProps { + // TODO: make sure results come in as same type, regardless of profile, category, badges + results: SectionListData<any>[]; + previewType: PreviewType; + screenType: ScreenType; + setChatModalVisible: Function; +} + +const ChatResultsList: React.FC<ChatResultsProps> = ({ + results, + setChatModalVisible, +}) => { + const [showEmptyView, setshowEmptyView] = useState<boolean>(false); + const {user: loggedInUser} = useSelector((state: RootState) => state.user); + const tabbarHeight = useBottomTabBarHeight(); + + useEffect(() => { + if (results && results.length > 0) { + let showEmpty = true; + + results.forEach((e) => { + if (e.data.length > 0) { + showEmpty = false; + } + }); + setshowEmptyView(showEmpty); + } + }, [results]); + + return showEmptyView ? ( + <View style={styles.container} onTouchStart={Keyboard.dismiss}> + <Text style={styles.noResultsTextStyle}>{NO_RESULTS_FOUND}</Text> + </View> + ) : ( + <SectionList + onScrollBeginDrag={Keyboard.dismiss} + contentContainerStyle={[{paddingBottom: tabbarHeight}]} + sections={results} + keyExtractor={(item, index) => item.id + index} + renderItem={({item}) => ( + <ChatResultsCell + profileData={item} + setChatModalVisible={setChatModalVisible} + loggedInUser={loggedInUser} + /> + )} + stickySectionHeadersEnabled={false} + ListEmptyComponent={() => ( + <View style={styles.empty}> + <Text>Start a new chat by searching for someone</Text> + </View> + )} + /> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + marginTop: 30, + alignItems: 'center', + }, + sectionHeaderStyle: { + width: '100%', + height: 0.5, + marginVertical: 5, + backgroundColor: '#C4C4C4', + }, + noResultsTextContainer: { + justifyContent: 'center', + flexDirection: 'row', + width: SCREEN_WIDTH, + }, + noResultsTextStyle: { + fontWeight: '500', + fontSize: normalize(14), + }, + empty: { + marginTop: 20, + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default ChatResultsList; diff --git a/src/screens/chat/ChatSearchBar.tsx b/src/screens/chat/ChatSearchBar.tsx new file mode 100644 index 00000000..4916ec45 --- /dev/null +++ b/src/screens/chat/ChatSearchBar.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { + Keyboard, + NativeSyntheticEvent, + StyleSheet, + Text, + TextInput, + TextInputProps, + TextInputSubmitEditingEventData, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native'; +import {normalize} from 'react-native-elements'; +import Animated, {useAnimatedStyle} from 'react-native-reanimated'; + +interface SearchBarProps extends TextInputProps { + onCancel: () => void; + animationProgress: Animated.SharedValue<number>; + searching: boolean; + placeholder: string; +} +const ChatSearchBar: React.FC<SearchBarProps> = ({ + onFocus, + onBlur, + onChangeText, + value, + onCancel, + searching, + animationProgress, + onLayout, + placeholder, +}) => { + const handleSubmit = ( + e: NativeSyntheticEvent<TextInputSubmitEditingEventData>, + ) => { + e.preventDefault(); + Keyboard.dismiss(); + }; + + /* + * On-search marginRight style ("cancel" button slides and fades in). + */ + const animatedStyles = useAnimatedStyle<ViewStyle>(() => ({ + marginRight: animationProgress.value * 58, + opacity: animationProgress.value, + })); + + return ( + <View style={styles.container} onLayout={onLayout}> + <Animated.View style={styles.inputContainer}> + <Animated.View style={styles.searchTextContainer}> + <Text style={styles.searchTextStyes}>To:</Text> + </Animated.View> + <TextInput + style={[styles.input]} + placeholderTextColor={'#828282'} + onSubmitEditing={handleSubmit} + clearButtonMode="always" + autoCapitalize="none" + autoCorrect={false} + {...{placeholder, value, onChangeText, onFocus, onBlur}} + /> + </Animated.View> + <Animated.View style={animatedStyles}> + <TouchableOpacity style={styles.cancelButton} onPress={onCancel}> + <Text style={styles.cancelText}>Cancel</Text> + </TouchableOpacity> + </Animated.View> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + height: 40, + paddingHorizontal: 20, + flexDirection: 'row', + zIndex: 2, + }, + searchTextContainer: {marginHorizontal: 12}, + searchTextStyes: {fontWeight: 'bold', fontSize: 14, lineHeight: 17}, + inputContainer: { + flexGrow: 1, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + borderRadius: 20, + backgroundColor: '#F0F0F0', + }, + searchIcon: { + marginRight: 8, + }, + input: { + flex: 1, + fontSize: 16, + color: '#000', + letterSpacing: normalize(0.5), + }, + cancelButton: { + height: '100%', + position: 'absolute', + justifyContent: 'center', + paddingHorizontal: 8, + }, + cancelText: { + color: '#818181', + fontWeight: '500', + }, +}); + +export default ChatSearchBar; diff --git a/src/screens/chat/NewChatModal.tsx b/src/screens/chat/NewChatModal.tsx new file mode 100644 index 00000000..95e46ecd --- /dev/null +++ b/src/screens/chat/NewChatModal.tsx @@ -0,0 +1,161 @@ +import React, {useEffect, useState} from 'react'; +import { + Keyboard, + SectionListData, + StatusBar, + StyleSheet, + Text, + View, +} from 'react-native'; +import {useSharedValue} from 'react-native-reanimated'; +import {BottomDrawer} from '../../components'; +import { + SEARCH_ENDPOINT_MESSAGES, + SEARCH_ENDPOINT_SUGGESTED, +} from '../../constants'; +import {loadSearchResults} from '../../services'; +import {ScreenType} from '../../types'; +import {normalize} from '../../utils'; +import {ChatResultsList, ChatSearchBar} from './index'; +interface NewChatModalProps { + modalVisible: boolean; + setChatModalVisible: (open: boolean) => void; +} + +const NewChatModal: React.FC<NewChatModalProps> = ({ + modalVisible, + setChatModalVisible, +}) => { + const [searching, setSearching] = useState(false); + /* + * Animated value + */ + const animationProgress = useSharedValue<number>(0); + const [results, setResults] = useState<SectionListData<any>[]>([]); + const [query, setQuery] = useState<string>(''); + const handleFocus = () => { + setSearching(true); + }; + const handleBlur = () => { + Keyboard.dismiss(); + }; + const handleCancel = () => { + setSearching(false); + }; + + const getDefaultSuggested = async () => { + const searchResults = await loadSearchResults( + `${SEARCH_ENDPOINT_SUGGESTED}`, + ); + console.log(searchResults); + const sanitizedResult = [ + { + title: 'users', + data: searchResults?.users, + }, + ]; + console.log(searchResults, sanitizedResult); + setResults(sanitizedResult); + }; + + const getQuerySuggested = async () => { + const searchResults = await loadSearchResults( + `${SEARCH_ENDPOINT_MESSAGES}?query=${query}`, + ); + if (query.length > 2) { + const sanitizedResult = [ + { + title: 'users', + data: searchResults?.users, + }, + ]; + setResults(sanitizedResult); + } else { + setResults([]); + } + }; + + useEffect(() => { + if (query.length === 0) { + getDefaultSuggested(); + } + + if (!searching) { + return; + } + + if (query.length < 3) { + return; + } + getQuerySuggested(); + }, [query]); + + const _modalContent = () => { + return ( + <View style={styles.modalShadowContainer}> + <View style={styles.titleContainerStyles}> + <Text style={styles.titleTextStyles}>New Message</Text> + </View> + <ChatSearchBar + onCancel={handleCancel} + onChangeText={setQuery} + onBlur={handleBlur} + onFocus={handleFocus} + value={query} + {...{animationProgress, searching}} + placeholder={''} + /> + {results.length > 0 && ( + <View style={styles.headerContainerStyles}> + <Text style={styles.headerTextStyles}>Suggested</Text> + </View> + )} + <ChatResultsList + {...{results, setChatModalVisible}} + previewType={'Search'} + screenType={ScreenType.Search} + /> + </View> + ); + }; + + return ( + <View> + <StatusBar barStyle="dark-content" /> + <BottomDrawer + initialSnapPosition={'90%'} + isOpen={modalVisible} + setIsOpen={setChatModalVisible} + showHeader={false}> + {_modalContent()} + </BottomDrawer> + </View> + ); +}; + +const styles = StyleSheet.create({ + modalShadowContainer: { + height: '100%', + borderRadius: 9, + backgroundColor: 'white', + }, + titleContainerStyles: {marginVertical: 24}, + titleTextStyles: { + fontWeight: 'bold', + fontSize: normalize(18), + lineHeight: normalize(21), + textAlign: 'center', + }, + headerContainerStyles: { + marginTop: 26, + marginBottom: 10, + marginHorizontal: 28, + }, + headerTextStyles: { + fontWeight: 'bold', + fontSize: normalize(17), + lineHeight: normalize(20), + }, +}); + +export default NewChatModal; diff --git a/src/screens/chat/index.ts b/src/screens/chat/index.ts index d2ccb02b..328eb8bf 100644 --- a/src/screens/chat/index.ts +++ b/src/screens/chat/index.ts @@ -1,2 +1,6 @@ export {default as ChatListScreen} from './ChatListScreen'; export {default as ChatScreen} from './ChatScreen'; +export {default as NewChatModal} from './NewChatModal'; +export {default as ChatSearchBar} from './ChatSearchBar'; +export {default as ChatResultsList} from './ChatResultsList'; +export {default as ChatResultsCell} from './ChatResultsCell'; diff --git a/src/utils/common.ts b/src/utils/common.ts index 4f31af8e..0900a084 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,14 +1,14 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import moment from 'moment'; +import {Linking} from 'react-native'; +import {getAll} from 'react-native-contacts'; +import {BROWSABLE_SOCIAL_URLS, TOGGLE_BUTTON_TYPE} from '../constants'; import { ContactType, NotificationType, - UniversityType, UniversityBadgeType, + UniversityType, } from './../types/types'; -import moment from 'moment'; -import {Linking} from 'react-native'; -import {BROWSABLE_SOCIAL_URLS, TOGGLE_BUTTON_TYPE} from '../constants'; -import AsyncStorage from '@react-native-community/async-storage'; -import {getAll} from 'react-native-contacts'; export const getToggleButtonText: ( buttonType: string, @@ -173,3 +173,21 @@ const _crestIcon = (university: UniversityType) => { return require('../assets/images/bwbadges.png'); } }; + +export const createChannel = async ( + loggedInUser: string, + id: string, + chatClient: any, +) => { + console.log(loggedInUser, id, chatClient); + try { + const channel = chatClient.channel('messaging', { + members: [loggedInUser, id], + }); + await channel.watch(); + return channel; + } catch (error) { + console.log(error); + throw error; + } +}; |