aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/constants/api.ts25
-rw-r--r--src/constants/strings.ts1
-rw-r--r--src/routes/main/MainStackNavigator.tsx1
-rw-r--r--src/routes/main/MainStackScreen.tsx421
-rw-r--r--src/screens/chat/ChatListScreen.tsx15
-rw-r--r--src/screens/chat/ChatResultsCell.tsx117
-rw-r--r--src/screens/chat/ChatResultsList.tsx102
-rw-r--r--src/screens/chat/ChatSearchBar.tsx112
-rw-r--r--src/screens/chat/NewChatModal.tsx161
-rw-r--r--src/screens/chat/index.ts4
-rw-r--r--src/utils/common.ts30
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;
+ }
+};