aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.tsx34
-rw-r--r--src/assets/icons/compose.svg1
-rw-r--r--src/assets/images/cover-placeholder@2x.pngbin8875 -> 9867 bytes
-rw-r--r--src/assets/images/cover-placeholder@3x.pngbin13944 -> 12116 bytes
-rw-r--r--src/assets/navigationIcons/chat-clicked.pngbin0 -> 858 bytes
-rw-r--r--src/assets/navigationIcons/chat-clicked@2x.pngbin0 -> 1856 bytes
-rw-r--r--src/assets/navigationIcons/chat-clicked@3x.pngbin0 -> 2883 bytes
-rw-r--r--src/assets/navigationIcons/chat-notifications.pngbin0 -> 1587 bytes
-rw-r--r--src/assets/navigationIcons/chat-notifications@2x.pngbin0 -> 3994 bytes
-rw-r--r--src/assets/navigationIcons/chat-notifications@3x.pngbin0 -> 6183 bytes
-rw-r--r--src/assets/navigationIcons/chat.pngbin0 -> 1587 bytes
-rw-r--r--src/assets/navigationIcons/chat@2x.pngbin0 -> 3994 bytes
-rw-r--r--src/assets/navigationIcons/chat@3x.pngbin0 -> 6183 bytes
-rw-r--r--src/components/common/BottomDrawer.tsx18
-rw-r--r--src/components/common/GradientBorderButton.tsx2
-rw-r--r--src/components/common/NavigationIcon.tsx10
-rw-r--r--src/components/common/TabsGradient.tsx2
-rw-r--r--src/components/common/TaggPrompt.tsx2
-rw-r--r--src/components/index.ts1
-rw-r--r--src/components/messages/ChannelPreview.tsx131
-rw-r--r--src/components/messages/ChatHeader.tsx84
-rw-r--r--src/components/messages/MessagesHeader.tsx59
-rw-r--r--src/components/messages/index.ts2
-rw-r--r--src/components/profile/Content.tsx63
-rw-r--r--src/components/profile/Cover.tsx2
-rw-r--r--src/components/profile/Friends.tsx8
-rw-r--r--src/components/profile/PublicProfile.tsx5
-rw-r--r--src/components/search/RecentSearches.tsx31
-rw-r--r--src/components/search/SearchBar.tsx33
-rw-r--r--src/components/search/SearchCategories.tsx68
-rw-r--r--src/components/search/SearchResultList.tsx85
-rw-r--r--src/components/search/SearchResults.tsx14
-rw-r--r--src/components/search/SearchResultsBackground.tsx65
-rw-r--r--src/components/suggestedPeople/SPTaggsBar.tsx133
-rw-r--r--src/components/suggestedPeople/index.ts1
-rw-r--r--src/components/taggs/TaggPostFooter.tsx2
-rw-r--r--src/components/taggs/TaggsBar.tsx90
-rw-r--r--src/constants/api.ts33
-rw-r--r--src/constants/strings.ts1
-rw-r--r--src/routes/Routes.tsx4
-rw-r--r--src/routes/main/MainStackNavigator.tsx4
-rw-r--r--src/routes/main/MainStackScreen.tsx410
-rw-r--r--src/routes/tabs/NavigationBar.tsx7
-rw-r--r--src/screens/chat/ChatListScreen.tsx134
-rw-r--r--src/screens/chat/ChatResultsCell.tsx117
-rw-r--r--src/screens/chat/ChatResultsList.tsx102
-rw-r--r--src/screens/chat/ChatScreen.tsx53
-rw-r--r--src/screens/chat/ChatSearchBar.tsx112
-rw-r--r--src/screens/chat/NewChatModal.tsx161
-rw-r--r--src/screens/chat/index.ts6
-rw-r--r--src/screens/index.ts1
-rw-r--r--src/screens/main/NotificationsScreen.tsx2
-rw-r--r--src/screens/onboarding/Login.tsx1
-rw-r--r--src/screens/profile/IndividualMoment.tsx14
-rw-r--r--src/screens/profile/InviteFriendsScreen.tsx2
-rw-r--r--src/screens/profile/MomentUploadPromptScreen.tsx20
-rw-r--r--src/screens/profile/ProfileScreen.tsx36
-rw-r--r--src/screens/profile/SettingsScreen.tsx6
-rw-r--r--src/screens/search/DiscoverUsers.tsx2
-rw-r--r--src/screens/search/SearchScreen.tsx180
-rw-r--r--src/screens/suggestedPeople/SPBody.tsx20
-rw-r--r--src/screens/suggestedPeople/SuggestedPeopleScreen.tsx2
-rw-r--r--src/store/actions/user.ts16
-rw-r--r--src/types/types.ts41
-rw-r--r--src/utils/common.ts30
-rw-r--r--src/utils/layouts.ts1
-rw-r--r--src/utils/messages.ts83
-rw-r--r--src/utils/moments.ts20
68 files changed, 1903 insertions, 664 deletions
diff --git a/src/App.tsx b/src/App.tsx
index ea3617dc..b8d64461 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,18 +1,48 @@
import {NavigationContainer} from '@react-navigation/native';
-import React from 'react';
+import React, {useState} from 'react';
import {Provider} from 'react-redux';
+import {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 {
+ ChannelGroupedType,
+ ChatContextType,
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalResponseType,
+ LocalUserType,
+} from './types';
+
+export const ChatContext = React.createContext({} as ChatContextType);
const App = () => {
+ const [channel, setChannel] = useState<ChannelGroupedType>();
+ 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/icons/compose.svg b/src/assets/icons/compose.svg
new file mode 100644
index 00000000..062e08cf
--- /dev/null
+++ b/src/assets/icons/compose.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 720"><defs><style>.cls-1,.cls-2,.cls-3{fill:none;}.cls-1,.cls-2{stroke:#828282;stroke-linecap:round;stroke-width:48px;}.cls-1,.cls-3{stroke-miterlimit:10;}.cls-2{stroke-linejoin:round;}.cls-3{stroke:#5c5b5e;stroke-width:0.91px;}</style></defs><path class="cls-1" d="M607.16,358.14V584.69c0,41.63-38,75.36-85,75.36H122.42c-46.91,0-84.93-33.73-84.93-75.36V168c0-41.62,38-75.4,84.93-75.4H412.23"/><path class="cls-2" d="M672.13,165.09l-289,290.8-52.62,19.66-81.11,30.28,27.77-83.35,17.06-51.26a2,2,0,0,1,.28-.32L585.59,78a60.89,60.89,0,0,1,86.54,0A61.87,61.87,0,0,1,672.13,165.09Z"/><line class="cls-3" x1="280.02" y1="283.62" x2="279.34" y2="283.87"/></svg> \ No newline at end of file
diff --git a/src/assets/images/cover-placeholder@2x.png b/src/assets/images/cover-placeholder@2x.png
index 402ac1fe..70294346 100644
--- a/src/assets/images/cover-placeholder@2x.png
+++ b/src/assets/images/cover-placeholder@2x.png
Binary files differ
diff --git a/src/assets/images/cover-placeholder@3x.png b/src/assets/images/cover-placeholder@3x.png
index be87023d..66fa3ce1 100644
--- a/src/assets/images/cover-placeholder@3x.png
+++ b/src/assets/images/cover-placeholder@3x.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat-clicked.png b/src/assets/navigationIcons/chat-clicked.png
new file mode 100644
index 00000000..f62b4cf5
--- /dev/null
+++ b/src/assets/navigationIcons/chat-clicked.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat-clicked@2x.png b/src/assets/navigationIcons/chat-clicked@2x.png
new file mode 100644
index 00000000..4ce0f46a
--- /dev/null
+++ b/src/assets/navigationIcons/chat-clicked@2x.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat-clicked@3x.png b/src/assets/navigationIcons/chat-clicked@3x.png
new file mode 100644
index 00000000..bd3a1352
--- /dev/null
+++ b/src/assets/navigationIcons/chat-clicked@3x.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat-notifications.png b/src/assets/navigationIcons/chat-notifications.png
new file mode 100644
index 00000000..cffb5751
--- /dev/null
+++ b/src/assets/navigationIcons/chat-notifications.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat-notifications@2x.png b/src/assets/navigationIcons/chat-notifications@2x.png
new file mode 100644
index 00000000..22ae62db
--- /dev/null
+++ b/src/assets/navigationIcons/chat-notifications@2x.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat-notifications@3x.png b/src/assets/navigationIcons/chat-notifications@3x.png
new file mode 100644
index 00000000..98b1073d
--- /dev/null
+++ b/src/assets/navigationIcons/chat-notifications@3x.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat.png b/src/assets/navigationIcons/chat.png
new file mode 100644
index 00000000..cffb5751
--- /dev/null
+++ b/src/assets/navigationIcons/chat.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat@2x.png b/src/assets/navigationIcons/chat@2x.png
new file mode 100644
index 00000000..22ae62db
--- /dev/null
+++ b/src/assets/navigationIcons/chat@2x.png
Binary files differ
diff --git a/src/assets/navigationIcons/chat@3x.png b/src/assets/navigationIcons/chat@3x.png
new file mode 100644
index 00000000..98b1073d
--- /dev/null
+++ b/src/assets/navigationIcons/chat@3x.png
Binary files differ
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/NavigationIcon.tsx b/src/components/common/NavigationIcon.tsx
index 1a9934f2..5128f3da 100644
--- a/src/components/common/NavigationIcon.tsx
+++ b/src/components/common/NavigationIcon.tsx
@@ -14,7 +14,8 @@ interface NavigationIconProps extends TouchableOpacityProps {
| 'Upload'
| 'Notifications'
| 'Profile'
- | 'SuggestedPeople';
+ | 'SuggestedPeople'
+ | 'Chat';
disabled?: boolean;
newIcon?: boolean;
}
@@ -44,6 +45,13 @@ const NavigationIcon = (props: NavigationIconProps) => {
: require('../../assets/navigationIcons/notifications.png')
: require('../../assets/navigationIcons/notifications-clicked.png');
break;
+ case 'Chat':
+ imgSrc = props.disabled
+ ? props.newIcon
+ ? require('../../assets/navigationIcons/chat-notifications.png')
+ : require('../../assets/navigationIcons/chat.png')
+ : require('../../assets/navigationIcons/chat-clicked.png');
+ break;
case 'Profile':
imgSrc = props.disabled
? require('../../assets/navigationIcons/profile.png')
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/common/TaggPrompt.tsx b/src/components/common/TaggPrompt.tsx
index 721b1eb8..5e125d00 100644
--- a/src/components/common/TaggPrompt.tsx
+++ b/src/components/common/TaggPrompt.tsx
@@ -68,7 +68,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white',
- height: isIPhoneX() ? SCREEN_HEIGHT / 6 : SCREEN_HEIGHT / 5,
+ height: SCREEN_HEIGHT / 4,
},
closeButton: {
position: 'relative',
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/ChannelPreview.tsx b/src/components/messages/ChannelPreview.tsx
new file mode 100644
index 00000000..312f879a
--- /dev/null
+++ b/src/components/messages/ChannelPreview.tsx
@@ -0,0 +1,131 @@
+import {useNavigation} from '@react-navigation/core';
+import React, {useContext} from 'react';
+import {Image, StyleSheet, Text, View} from 'react-native';
+import {TouchableOpacity} from 'react-native-gesture-handler';
+import {useStore} from 'react-redux';
+import {usernameRegex} from 'src/constants';
+import {ChannelPreviewMessengerProps} from 'stream-chat-react-native';
+import {ChatContext} from '../../App';
+import {
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalReactionType,
+ LocalUserType,
+} from '../../types';
+import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+import {getMember, isOnline} from '../../utils/messages';
+
+const ChannelPreview: React.FC<ChannelPreviewMessengerProps<
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalReactionType,
+ LocalUserType
+>> = (props) => {
+ const {setChannel} = useContext(ChatContext);
+ const state = useStore().getState();
+ const navigation = useNavigation();
+ const {channel} = props;
+ const member = getMember(channel, state);
+ const online = isOnline(member?.user?.last_active);
+ const unread = channel.state.unreadCount > 0;
+
+ return (
+ <TouchableOpacity
+ style={styles.container}
+ onPress={() => {
+ setChannel(channel);
+ navigation.navigate('Chat');
+ }}>
+ <View>
+ <Image
+ style={styles.avatar}
+ source={
+ member
+ ? {uri: member.user?.thumbnail_url}
+ : require('../../assets/images/avatar-placeholder.png')
+ }
+ />
+ {online && <View style={styles.online} />}
+ </View>
+ <View style={styles.content}>
+ <Text
+ style={[styles.name, unread ? styles.unread : {}]}
+ numberOfLines={1}>
+ {member?.user?.first_name} {member?.user?.last_name}
+ </Text>
+ <Text
+ style={[styles.lastMessage, unread ? styles.unread : {}]}
+ numberOfLines={1}>
+ {channel.state.messages.length > 0
+ ? channel.state.messages[channel.state.messages.length - 1].text
+ : ''}
+ </Text>
+ </View>
+ {unread && <View style={styles.purpleDot} />}
+ </TouchableOpacity>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'row',
+ height: Math.round(SCREEN_HEIGHT / 9),
+ width: Math.round(SCREEN_WIDTH * 0.85),
+ alignSelf: 'center',
+ alignItems: 'center',
+ },
+ avatar: {
+ width: normalize(60),
+ height: normalize(60),
+ borderRadius: normalize(62) / 2,
+ },
+ online: {
+ position: 'absolute',
+ backgroundColor: '#6EE7E7',
+ width: normalize(18),
+ height: normalize(18),
+ borderRadius: normalize(18) / 2,
+ borderColor: 'white',
+ borderWidth: 2,
+ bottom: 0,
+ right: 0,
+ },
+ content: {
+ flex: 1,
+ height: '60%',
+ flexDirection: 'column',
+ marginLeft: '5%',
+ },
+ name: {
+ fontWeight: '500',
+ fontSize: normalize(14),
+ lineHeight: normalize(17),
+ },
+ lastMessage: {
+ color: '#828282',
+ fontWeight: '500',
+ fontSize: normalize(12),
+ lineHeight: normalize(14),
+ paddingTop: '5%',
+ },
+ unread: {
+ fontWeight: '700',
+ color: 'black',
+ },
+ purpleDot: {
+ backgroundColor: '#8F01FF',
+ width: normalize(10),
+ height: normalize(10),
+ borderRadius: normalize(10) / 2,
+ marginLeft: '5%',
+ },
+});
+
+export default ChannelPreview;
diff --git a/src/components/messages/ChatHeader.tsx b/src/components/messages/ChatHeader.tsx
new file mode 100644
index 00000000..2bc096ec
--- /dev/null
+++ b/src/components/messages/ChatHeader.tsx
@@ -0,0 +1,84 @@
+import React, {useContext} from 'react';
+import {Image, StyleSheet, View} from 'react-native';
+import {Text} from 'react-native-animatable';
+import {useStore} from 'react-redux';
+import {ChatContext} from '../../App';
+import {ChatHeaderHeight, normalize, StatusBarHeight} from '../../utils';
+import {formatLastSeenText, getMember, isOnline} from '../../utils/messages';
+
+type ChatHeaderProps = {};
+
+const ChatHeader: React.FC<ChatHeaderProps> = () => {
+ const {channel} = useContext(ChatContext);
+ const state = useStore().getState();
+ const member = getMember(channel, state);
+ const online = isOnline(member?.user?.last_active);
+ const lastSeen = formatLastSeenText(member?.user?.last_active);
+
+ return (
+ <View style={styles.container}>
+ <View>
+ <Image
+ style={styles.avatar}
+ source={
+ member
+ ? {uri: member.user?.thumbnail_url}
+ : require('../../assets/images/avatar-placeholder.png')
+ }
+ />
+ {online && <View style={styles.online} />}
+ </View>
+ <View style={styles.content}>
+ <Text style={styles.name} numberOfLines={1}>
+ {member?.user?.first_name} {member?.user?.last_name}
+ </Text>
+ <Text style={styles.lastSeen}>{lastSeen}</Text>
+ </View>
+ </View>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ height: ChatHeaderHeight - StatusBarHeight,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingLeft: '15%',
+ },
+ avatar: {
+ width: normalize(40),
+ height: normalize(40),
+ borderRadius: normalize(40) / 2,
+ },
+ online: {
+ position: 'absolute',
+ backgroundColor: '#6EE7E7',
+ width: normalize(16),
+ height: normalize(16),
+ borderRadius: normalize(16) / 2,
+ borderColor: 'white',
+ borderWidth: 3,
+ top: 0,
+ right: 0,
+ },
+ content: {
+ flex: 1,
+ height: '80%',
+ justifyContent: 'space-between',
+ flexDirection: 'column',
+ marginLeft: '5%',
+ },
+ name: {
+ fontWeight: '700',
+ fontSize: normalize(15),
+ lineHeight: normalize(18),
+ },
+ lastSeen: {
+ color: '#828282',
+ fontWeight: '500',
+ fontSize: normalize(12),
+ lineHeight: normalize(14),
+ },
+});
+
+export default ChatHeader;
diff --git a/src/components/messages/MessagesHeader.tsx b/src/components/messages/MessagesHeader.tsx
new file mode 100644
index 00000000..660da97d
--- /dev/null
+++ b/src/components/messages/MessagesHeader.tsx
@@ -0,0 +1,59 @@
+import React, {Fragment, useContext} 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';
+import ComposeIcon from '../../assets/icons/compose.svg';
+import {ChatContext} from '../../App';
+
+type MessagesHeaderProps = {
+ createChannel: () => void;
+};
+
+const MessagesHeader: React.FC<MessagesHeaderProps> = ({createChannel}) => {
+ const {chatClient} = useContext(ChatContext);
+ const unread = chatClient.user?.total_unread_count as number;
+ return (
+ <View style={styles.header}>
+ <Text style={styles.headerText}>Messages</Text>
+ {unread && unread !== 0 ? (
+ <Text style={styles.unreadText}>
+ {unread > 99 ? '99+' : unread} unread
+ </Text>
+ ) : (
+ <Fragment />
+ )}
+ <View style={styles.flex} />
+ <TouchableOpacity style={styles.compose} onPress={createChannel}>
+ <ComposeIcon width={normalize(20)} height={normalize(20)} />
+ </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..e194093c
--- /dev/null
+++ b/src/components/messages/index.ts
@@ -0,0 +1,2 @@
+export {default as MessagesHeader} from './MessagesHeader';
+export {default as ChannelPreview} from './ChannelPreview';
diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx
index 9c33eabc..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,12 +60,14 @@ 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
*/
const [isBlocked, setIsBlocked] = useState<boolean>(false);
const [profileBodyHeight, setProfileBodyHeight] = useState(0);
+ const [socialsBarHeight, setSocialsBarHeight] = useState(0);
const [shouldBounce, setShouldBounce] = useState<boolean>(true);
const [refreshing, setRefreshing] = useState<boolean>(false);
@@ -88,6 +90,11 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
setProfileBodyHeight(height);
};
+ const onSocialsBarLayout = (e: LayoutChangeEvent) => {
+ const {height} = e.nativeEvent.layout;
+ setSocialsBarHeight(height);
+ };
+
useEffect(() => {
const isActuallyBlocked = blockedUsers.some(
(cur_user) => user.username === cur_user.username,
@@ -103,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]}
@@ -165,7 +159,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
/>
<TaggsBar
{...{y, profileBodyHeight, userXId, screenType}}
- whiteRing={undefined}
+ onLayout={onSocialsBarLayout}
/>
{canViewProfile(state, userXId, screenType) ? (
<PublicProfile
@@ -175,6 +169,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
screenType,
setScrollEnabled,
profileBodyHeight,
+ socialsBarHeight,
scrollViewRef,
}}
/>
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/Friends.tsx b/src/components/profile/Friends.tsx
index 44f6bb48..c1dca755 100644
--- a/src/components/profile/Friends.tsx
+++ b/src/components/profile/Friends.tsx
@@ -27,7 +27,7 @@ interface FriendsProps {
const Friends: React.FC<FriendsProps> = ({result, screenType, userId}) => {
const state: RootState = useStore().getState();
const dispatch = useDispatch();
- const {user: loggedInUser = NO_USER} = state;
+ const {user: loggedInUser = NO_USER} = state.user;
const navigation = useNavigation();
const [usersFromContacts, setUsersFromContacts] = useState<
ProfilePreviewType[]
@@ -39,7 +39,7 @@ const Friends: React.FC<FriendsProps> = ({result, screenType, userId}) => {
const permission = await checkPermission();
if (permission === 'authorized') {
let response = await usersFromContactsService(contacts);
- await setUsersFromContacts(response.existing_tagg_users);
+ setUsersFromContacts(response.existing_tagg_users);
} else {
console.log('Authorize access to contacts');
}
@@ -84,10 +84,10 @@ const Friends: React.FC<FriendsProps> = ({result, screenType, userId}) => {
return (
<>
- {loggedInUser.userId === userId && (
+ {loggedInUser.userId === userId && usersFromContacts.length !== 0 && (
<View style={styles.subheader}>
<View style={styles.addFriendHeaderContainer}>
- <Text style={[styles.subheaderText]}>Contacts on tagg</Text>
+ <Text style={[styles.subheaderText]}>Contacts on Tagg</Text>
</View>
<UsersFromContacts />
</View>
diff --git a/src/components/profile/PublicProfile.tsx b/src/components/profile/PublicProfile.tsx
index 88e0ecd1..1c49bff5 100644
--- a/src/components/profile/PublicProfile.tsx
+++ b/src/components/profile/PublicProfile.tsx
@@ -35,6 +35,7 @@ const PublicProfile: React.FC<ContentProps> = ({
screenType,
setScrollEnabled,
profileBodyHeight,
+ socialsBarHeight,
scrollViewRef,
}) => {
const dispatch = useDispatch();
@@ -99,11 +100,12 @@ 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],
profileBodyHeight,
+ socialsBarHeight,
});
setIsStageOnePromptClosed(true);
}
@@ -133,6 +135,7 @@ const PublicProfile: React.FC<ContentProps> = ({
navigation,
screenType,
profileBodyHeight,
+ socialsBarHeight,
scrollViewRef,
]),
);
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 06acadc1..4d567b25 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} 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} from 'react-redux';
import {
@@ -14,22 +19,22 @@ import {RootState} from '../../store/rootReducer';
import {ScreenType} from '../../types';
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;
}
const TaggsBar: React.FC<TaggsBarProps> = ({
y,
profileBodyHeight,
userXId,
screenType,
- whiteRing,
linkedSocials,
+ onLayout,
}) => {
const dispatch = useDispatch();
let [taggs, setTaggs] = useState<Object[]>([]);
@@ -37,7 +42,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({
const {user} = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.user,
);
-
+ const insetTop = useSafeAreaInsets().top;
/**
* Updates the individual social that needs update
* If username is empty, update nonintegrated socials like Snapchat and TikTok
@@ -77,12 +82,12 @@ const TaggsBar: React.FC<TaggsBarProps> = ({
isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1}
setTaggsNeedUpdate={setTaggsNeedUpdate}
setSocialDataNeedUpdate={handleSocialUpdate}
- whiteRing={whiteRing ? whiteRing : undefined}
+ whiteRing={false}
/>,
);
i++;
}
- if (!userXId && !whiteRing) {
+ if (!userXId) {
for (let social of unlinkedSocials) {
new_taggs.push(
<Tagg
@@ -95,7 +100,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({
userXId={userXId}
screenType={screenType}
user={user}
- whiteRing={whiteRing ? whiteRing : undefined}
+ whiteRing={false}
/>,
);
i++;
@@ -108,64 +113,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}]
- }>
+ <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..43294386 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/';
@@ -18,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/';
@@ -31,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/';
@@ -65,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/Routes.tsx b/src/routes/Routes.tsx
index c7b9aeee..819ca785 100644
--- a/src/routes/Routes.tsx
+++ b/src/routes/Routes.tsx
@@ -50,7 +50,7 @@ const Routes: React.FC = () => {
fcmService.setUpPushNotifications();
fcmService.sendFcmTokenToServer();
}
- });
+ }, []);
useEffect(() => {
const checkVersion = async () => {
@@ -61,7 +61,7 @@ const Routes: React.FC = () => {
}
};
checkVersion();
- });
+ }, []);
return userId && !newVersionAvailable ? <NavigationBar /> : <Onboarding />;
};
diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx
index 9b089634..64ad9198 100644
--- a/src/routes/main/MainStackNavigator.tsx
+++ b/src/routes/main/MainStackNavigator.tsx
@@ -73,6 +73,7 @@ export type MainStackParams = {
screenType: ScreenType;
momentCategory: string;
profileBodyHeight: number;
+ socialsBarHeight: number;
};
AnimatedTutorial: {
screenType: ScreenType;
@@ -92,6 +93,9 @@ export type MainStackParams = {
screenType: ScreenType;
};
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 d855f0df..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,20 +21,21 @@ import {
InviteFriendsScreen,
MomentCommentsScreen,
MomentUploadPromptScreen,
+ NewChatModal,
NotificationsScreen,
- ProfileScreen,
PrivacyScreen,
+ ProfileScreen,
RequestContactsAccess,
SearchScreen,
+ SettingsScreen,
SocialMediaTaggs,
SuggestedPeopleScreen,
SuggestedPeopleUploadPictureScreen,
SuggestedPeopleWelcomeScreen,
- SettingsScreen,
} from '../../screens';
import MutualBadgeHolders from '../../screens/suggestedPeople/MutualBadgeHolders';
import {ScreenType} from '../../types';
-import {AvatarHeaderHeight, SCREEN_WIDTH} from '../../utils';
+import {AvatarHeaderHeight, ChatHeaderHeight, SCREEN_WIDTH} from '../../utils';
import {MainStack, MainStackParams} from './MainStackNavigator';
/**
@@ -50,7 +53,6 @@ type MainStackRouteProps = RouteProp<MainStackParams, 'Profile'>;
interface MainStackProps {
route: MainStackRouteProps;
}
-
const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
const {screenType} = route.params;
@@ -62,6 +64,10 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
'true',
);
+ useEffect(() => {
+ loadResponseToAccessContacts();
+ }, []);
+
const loadResponseToAccessContacts = () => {
AsyncStorage.getItem('respondedToAccessContacts')
.then((value) => {
@@ -73,8 +79,6 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
});
};
- loadResponseToAccessContacts();
-
const initialRouteName = (() => {
switch (screenType) {
case ScreenType.Profile:
@@ -85,6 +89,8 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
return 'Notifications';
case ScreenType.SuggestedPeople:
return 'SuggestedPeople';
+ case ScreenType.Chat:
+ return 'ChatList';
}
})();
@@ -102,200 +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="PrivacyScreen"
+ component={PrivacyScreen}
+ options={{
+ ...headerBarOptions('white', 'Privacy'),
+ }}
+ />
+ <MainStack.Screen
+ name="AccountTypeScreen"
+ component={AccountType}
+ options={{
+ ...headerBarOptions('white', 'Account Type'),
+ }}
+ />
<MainStack.Screen
- name="Notifications"
- component={NotificationsScreen}
+ 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.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/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx
index e9208525..9b8427e7 100644
--- a/src/routes/tabs/NavigationBar.tsx
+++ b/src/routes/tabs/NavigationBar.tsx
@@ -54,6 +54,8 @@ const NavigationBar: React.FC = () => {
disabled={!focused}
/>
);
+ case 'Chat':
+ return <NavigationIcon tab="Chat" disabled={!focused} />;
case 'Profile':
return <NavigationIcon tab="Profile" disabled={!focused} />;
case 'SuggestedPeople':
@@ -93,6 +95,11 @@ const NavigationBar: React.FC = () => {
initialParams={{screenType: ScreenType.Notifications}}
/>
<Tabs.Screen
+ name="Chat"
+ component={MainStackScreen}
+ initialParams={{screenType: ScreenType.Chat}}
+ />
+ <Tabs.Screen
name="Profile"
component={MainStackScreen}
initialParams={{screenType: ScreenType.Profile}}
diff --git a/src/screens/chat/ChatListScreen.tsx b/src/screens/chat/ChatListScreen.tsx
new file mode 100644
index 00000000..daea9984
--- /dev/null
+++ b/src/screens/chat/ChatListScreen.tsx
@@ -0,0 +1,134 @@
+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 {TabsGradient} from '../../components';
+import {ChannelPreview, MessagesHeader} from '../../components/messages';
+import {MainStackParams} from '../../routes';
+import {RootState} from '../../store/rootReducer';
+import {
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalReactionType,
+ LocalUserType,
+} from '../../types';
+
+import NewChatModal from './NewChatModal';
+type ChatListScreenNavigationProp = StackNavigationProp<
+ MainStackParams,
+ 'ChatList'
+>;
+interface ChatListScreenProps {
+ navigation: ChatListScreenNavigationProp;
+}
+/*
+ * Screen that displays all of the user's active conversations.
+ */
+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;
+
+ 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.background}>
+ <SafeAreaView>
+ <StatusBar barStyle="dark-content" />
+ <MessagesHeader
+ createChannel={() => {
+ setChatModalVisible(true);
+ }}
+ />
+ {clientReady && (
+ <Chat client={chatClient}>
+ <View style={styles.chatContainer}>
+ <ChannelList<
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalReactionType,
+ LocalUserType
+ >
+ filters={memoizedFilters}
+ options={{
+ presence: true,
+ state: true,
+ watch: true,
+ }}
+ sort={{last_message_at: -1}}
+ maxUnreadCount={99}
+ Preview={ChannelPreview}
+ />
+ </View>
+ </Chat>
+ )}
+ <NewChatModal {...{modalVisible, setChatModalVisible}} />
+ </SafeAreaView>
+ <TabsGradient />
+ </View>
+ );
+};
+
+const styles = StyleSheet.create({
+ background: {
+ flex: 1,
+ backgroundColor: 'white',
+ },
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ placeholder: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ marginBottom: 10,
+ },
+ button: {
+ backgroundColor: '#CCE4FC',
+ padding: 15,
+ borderRadius: 5,
+ },
+ chatContainer: {
+ height: '100%',
+ marginTop: 10,
+ },
+});
+
+export default ChatListScreen;
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/ChatScreen.tsx b/src/screens/chat/ChatScreen.tsx
new file mode 100644
index 00000000..59c53c99
--- /dev/null
+++ b/src/screens/chat/ChatScreen.tsx
@@ -0,0 +1,53 @@
+import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs';
+import {StackNavigationProp} from '@react-navigation/stack';
+import React, {useContext} from 'react';
+import {StyleSheet} from 'react-native';
+import {SafeAreaView} from 'react-native-safe-area-context';
+import {
+ Channel,
+ Chat,
+ MessageInput,
+ MessageList,
+} from 'stream-chat-react-native';
+import {ChatContext} from '../../App';
+import ChatHeader from '../../components/messages/ChatHeader';
+import {MainStackParams} from '../../routes';
+import {isIPhoneX} from '../../utils';
+
+type ChatScreenNavigationProp = StackNavigationProp<MainStackParams, 'Chat'>;
+interface ChatScreenProps {
+ navigation: ChatScreenNavigationProp;
+}
+/*
+ * Screen that displays all of the user's active conversations.
+ */
+const ChatScreen: React.FC<ChatScreenProps> = () => {
+ const {channel, chatClient} = useContext(ChatContext);
+ const tabbarHeight = useBottomTabBarHeight();
+
+ return (
+ <SafeAreaView
+ style={[
+ styles.container,
+ // unable to figure out the padding issue, a hacky solution
+ {paddingBottom: isIPhoneX() ? tabbarHeight + 20 : tabbarHeight + 50},
+ ]}>
+ <ChatHeader />
+ <Chat client={chatClient}>
+ <Channel channel={channel} keyboardVerticalOffset={0}>
+ <MessageList onThreadSelect={() => {}} />
+ <MessageInput />
+ </Channel>
+ </Chat>
+ </SafeAreaView>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: 'white',
+ flex: 1,
+ },
+});
+
+export default ChatScreen;
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
new file mode 100644
index 00000000..328eb8bf
--- /dev/null
+++ b/src/screens/chat/index.ts
@@ -0,0 +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/screens/index.ts b/src/screens/index.ts
index 50ada3d1..44ae4b52 100644
--- a/src/screens/index.ts
+++ b/src/screens/index.ts
@@ -5,3 +5,4 @@ export * from './search';
export * from './suggestedPeople';
export * from './suggestedPeopleOnboarding';
export * from './badge';
+export * from './chat';
diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx
index 48e89f7a..71199c9b 100644
--- a/src/screens/main/NotificationsScreen.tsx
+++ b/src/screens/main/NotificationsScreen.tsx
@@ -330,7 +330,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'stretch',
justifyContent: 'space-between',
- width: SCREEN_WIDTH * 0.85,
+ width: SCREEN_WIDTH * 0.9,
},
headerText: {
fontWeight: '700',
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/IndividualMoment.tsx b/src/screens/profile/IndividualMoment.tsx
index 8c1dc327..871d62bf 100644
--- a/src/screens/profile/IndividualMoment.tsx
+++ b/src/screens/profile/IndividualMoment.tsx
@@ -27,7 +27,7 @@ interface IndividualMomentProps {
navigation: IndividualMomentNavigationProp;
}
-const ITEM_HEIGHT = SCREEN_HEIGHT * (9 / 10);
+const ITEM_HEIGHT = SCREEN_HEIGHT * 0.9;
const IndividualMoment: React.FC<IndividualMomentProps> = ({
route,
@@ -40,13 +40,13 @@ const IndividualMoment: React.FC<IndividualMomentProps> = ({
);
const {
user: {username},
- } = userXId
- ? useSelector((state: RootState) => state.userX[screenType][userXId])
- : useSelector((state: RootState) => state.user);
+ } = useSelector((state: RootState) =>
+ userXId ? state.userX[screenType][userXId] : state.user,
+ );
- const {moments} = userXId
- ? useSelector((state: RootState) => state.userX[screenType][userXId])
- : useSelector((state: RootState) => state.moments);
+ const {moments} = useSelector((state: RootState) =>
+ userXId ? state.userX[screenType][userXId] : state.moments,
+ );
const isOwnProfile = username === loggedInUsername;
const momentData = moments.filter(
diff --git a/src/screens/profile/InviteFriendsScreen.tsx b/src/screens/profile/InviteFriendsScreen.tsx
index a9fa1404..ad9e382e 100644
--- a/src/screens/profile/InviteFriendsScreen.tsx
+++ b/src/screens/profile/InviteFriendsScreen.tsx
@@ -203,7 +203,7 @@ const InviteFriendsScreen: React.FC<InviteFriendsScreenProps> = ({route}) => {
</Animated.View>
</View>
<View style={styles.subheader}>
- <Text style={styles.subheaderText}>Contacts on tagg</Text>
+ <Text style={styles.subheaderText}>Contacts on Tagg</Text>
<UsersFromContacts />
</View>
<View style={styles.subheader}>
diff --git a/src/screens/profile/MomentUploadPromptScreen.tsx b/src/screens/profile/MomentUploadPromptScreen.tsx
index f79c81b4..f0aaffc4 100644
--- a/src/screens/profile/MomentUploadPromptScreen.tsx
+++ b/src/screens/profile/MomentUploadPromptScreen.tsx
@@ -8,7 +8,7 @@ import {Moment} from '../../components';
import {Image} from 'react-native-animatable';
import {UPLOAD_MOMENT_PROMPT_ONE_MESSAGE} from '../../constants/strings';
import {PROFILE_CUTOUT_BOTTOM_Y} from '../../constants';
-import {isIPhoneX, normalize} from '../../utils';
+import {normalize} from '../../utils';
type MomentUploadPromptScreenRouteProp = RouteProp<
MainStackParams,
@@ -28,7 +28,12 @@ const MomentUploadPromptScreen: React.FC<MomentUploadPromptScreenProps> = ({
route,
navigation,
}) => {
- const {screenType, momentCategory, profileBodyHeight} = route.params;
+ const {
+ screenType,
+ momentCategory,
+ profileBodyHeight,
+ socialsBarHeight,
+ } = route.params;
return (
<View style={styles.container}>
<CloseIcon
@@ -61,9 +66,7 @@ const MomentUploadPromptScreen: React.FC<MomentUploadPromptScreenProps> = ({
externalStyles={{
container: {
...styles.momentContainer,
- top: isIPhoneX()
- ? profileBodyHeight + 615
- : profileBodyHeight + 500,
+ top: PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight + socialsBarHeight,
},
titleText: styles.momentHeaderText,
header: styles.momentHeader,
@@ -103,20 +106,21 @@ const styles = StyleSheet.create({
//Styles to adjust moment container
momentScrollContainer: {
backgroundColor: 'transparent',
+ marginTop: 10,
},
momentContainer: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'transparent',
- height: 170,
+ height: 175,
},
momentHeaderText: {
...StyleSheet.absoluteFillObject,
marginLeft: 12,
- marginTop: 10,
+ paddingVertical: 5,
},
momentHeader: {
+ marginTop: 7,
backgroundColor: 'transparent',
- paddingVertical: 20,
},
});
diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx
index 313e2f2c..6d9ef020 100644
--- a/src/screens/profile/ProfileScreen.tsx
+++ b/src/screens/profile/ProfileScreen.tsx
@@ -1,17 +1,8 @@
import React from 'react';
import {StatusBar} from 'react-native';
-import Animated from 'react-native-reanimated';
-import {Content, Cover, TabsGradient} from '../../components';
-import {RouteProp, useFocusEffect} from '@react-navigation/native';
+import {Content, TabsGradient} from '../../components';
+import {RouteProp} from '@react-navigation/native';
import {MainStackParams} from '../../routes/';
-import {resetScreenType} from '../../store/actions';
-import {useDispatch, useStore} from 'react-redux';
-import {DUMMY_USERID} from '../../store/initialStates';
-
-/**r
- * Profile Screen for a user's profile
- * including posts, messaging, and settings
- */
type ProfileScreenRouteProps = RouteProp<MainStackParams, 'Profile'>;
@@ -22,32 +13,11 @@ interface ProfileOnboardingProps {
const ProfileScreen: React.FC<ProfileOnboardingProps> = ({route}) => {
const {screenType} = route.params;
let {userXId} = route.params;
- const y = Animated.useValue(0);
- const dispatch = useDispatch();
-
- /**
- * 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 766bf798..376c4be0 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;
@@ -142,6 +143,7 @@ export enum ScreenType {
Search,
Notifications,
SuggestedPeople,
+ Chat,
}
/**
@@ -214,6 +216,7 @@ export interface ContentProps {
screenType: ScreenType;
setScrollEnabled: (enabled: boolean) => void;
profileBodyHeight: number;
+ socialsBarHeight: number;
scrollViewRef: React.MutableRefObject<null>;
}
@@ -287,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 LocalReactionType = Record<string, unknown>;
+export type LocalUserType = Record<string, unknown>;
+export type ChannelGroupedType = ChannelType<
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalResponseType,
+ LocalUserType
+>;
+
+export type ChatContextType = {
+ channel: ChannelGroupedType | undefined;
+ setChannel: React.Dispatch<
+ React.SetStateAction<
+ | ChannelType<
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalResponseType,
+ LocalUserType
+ >
+ | undefined
+ >
+ >;
+ chatClient: StreamChat;
+};
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;
+ }
+};
diff --git a/src/utils/layouts.ts b/src/utils/layouts.ts
index e2f1f0b1..4d0d557d 100644
--- a/src/utils/layouts.ts
+++ b/src/utils/layouts.ts
@@ -31,6 +31,7 @@ export const StatusBarHeight = Platform.select({
});
export const AvatarHeaderHeight = (HeaderHeight + StatusBarHeight) * 1.3;
+export const ChatHeaderHeight = (HeaderHeight + StatusBarHeight) * 1.1;
/**
* This is a function for normalizing the font size for different devices, based on iphone 8.
diff --git a/src/utils/messages.ts b/src/utils/messages.ts
new file mode 100644
index 00000000..d63f2b7a
--- /dev/null
+++ b/src/utils/messages.ts
@@ -0,0 +1,83 @@
+import moment from 'moment';
+import {RootState} from '../store/rootReducer';
+import {ChannelGroupedType} from '../types';
+
+/**
+ * Finds the difference in time in minutes
+ * @param lastActive given time e.g. "2021-04-08T19:07:09.361300983Z"
+ * @returns diff in minutes
+ */
+const _diffInMinutes = (lastActive: string | undefined) => {
+ if (!lastActive) {
+ return undefined;
+ }
+ return moment().diff(moment(lastActive), 'minutes');
+};
+
+/**
+ * Formats the last activity status.
+ * - "Active now" (≤ 5 minutes)
+ * - "Seen X minutes ago" (5 > x ≥ 59 minutes)
+ * - "Seen X hours ago" (x = [1, 2])
+ * - "Offline"
+ * @param lastActive given time e.g. "2021-04-08T19:07:09.361300983Z"
+ * @returns
+ */
+export const formatLastSeenText = (lastActive: string | undefined) => {
+ const diff = _diffInMinutes(lastActive);
+ if (!diff) {
+ return 'Offline';
+ }
+ if (diff <= 5) {
+ return 'Active now';
+ }
+ if (diff <= 59) {
+ return `Seen ${diff} minutes ago`;
+ }
+ if (diff <= 180) {
+ const hours = Math.floor(diff / 60);
+ return `Seen ${hours} hours ago`;
+ }
+ return 'Offline';
+};
+
+/**
+ * Checks if a lastActive timestamp is considered Online or not.
+ *
+ * A user is online if last active is ≤ 15 minutes.
+ *
+ * @param lastActive given time e.g. "2021-04-08T19:07:09.361300983Z"
+ * @returns True if active
+ */
+export const isOnline = (lastActive: string | undefined) => {
+ if (!lastActive) {
+ return false;
+ }
+ const diff = _diffInMinutes(lastActive);
+ if (!diff) {
+ return false;
+ }
+ return diff <= 15;
+};
+
+/**
+ * Gets the other member in the channel.
+ * @param channel the current chat channel
+ * @param state the current redux state
+ * @returns other member or undefined
+ */
+export const getMember = (
+ channel: ChannelGroupedType | undefined,
+ state: RootState,
+) => {
+ if (!channel) {
+ return undefined;
+ }
+ const loggedInUserId = state.user.user.userId;
+ const otherMembers = channel
+ ? Object.values(channel.state.members).filter(
+ (member) => member.user?.id !== loggedInUserId,
+ )
+ : [];
+ return otherMembers.length === 1 ? otherMembers[0] : undefined;
+};
diff --git a/src/utils/moments.ts b/src/utils/moments.ts
index 7428b1ac..87f062af 100644
--- a/src/utils/moments.ts
+++ b/src/utils/moments.ts
@@ -1,15 +1,17 @@
import moment from 'moment';
-//A util that calculates the difference between a given time and current time
-//Returns the difference in the largest possible unit of time (days > hours > minutes > seconds)
-
+/**
+ * Formats elapsed time from a given time.
+ * @param date_time given time
+ * @returns difference in the largest possible unit of time (days > hours > minutes > seconds)
+ */
export const getTimePosted = (date_time: string) => {
const datePosted = moment(date_time);
const now = moment();
var time = date_time;
var difference = now.diff(datePosted, 'seconds');
- //Creating elapsedTime string to display to user
+ // Creating elapsedTime string to display to user
// 0 to less than 1 minute
if (difference < 60) {
time = difference + ' seconds';
@@ -19,15 +21,19 @@ export const getTimePosted = (date_time: string) => {
difference = now.diff(datePosted, 'minutes');
time = difference + (difference === 1 ? ' minute' : ' minutes');
}
- //1 hour to less than 1 day
+ // 1 hour to less than 1 day
else if (difference >= 60 * 60 && difference < 24 * 60 * 60) {
difference = now.diff(datePosted, 'hours');
time = difference + (difference === 1 ? ' hour' : ' hours');
}
- //Any number of days
- else if (difference >= 24 * 60 * 60) {
+ // Any number of days
+ else if (difference >= 24 * 60 * 60 && difference < 24 * 60 * 60 * 3) {
difference = now.diff(datePosted, 'days');
time = difference + (difference === 1 ? ' day' : ' days');
}
+ // More than 3 days
+ else if (difference >= 24 * 60 * 60 * 3) {
+ time = datePosted.format('MMMM D, YYYY');
+ }
return time;
};