aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/App.tsx15
-rw-r--r--src/assets/icons/compose.svg1
-rw-r--r--src/components/messages/ChannelPreview.tsx131
-rw-r--r--src/components/messages/ChatHeader.tsx84
-rw-r--r--src/components/messages/MessagesHeader.tsx20
-rw-r--r--src/components/messages/index.ts1
-rw-r--r--src/components/profile/Content.tsx13
-rw-r--r--src/components/profile/ProfileBody.tsx48
-rw-r--r--src/routes/Routes.tsx12
-rw-r--r--src/routes/main/MainStackScreen.tsx7
-rw-r--r--src/screens/chat/ChatListScreen.tsx61
-rw-r--r--src/screens/chat/ChatScreen.tsx21
-rw-r--r--src/screens/main/NotificationsScreen.tsx2
-rw-r--r--src/screens/onboarding/CategorySelection.tsx6
-rw-r--r--src/screens/onboarding/InvitationCodeVerification.tsx6
-rw-r--r--src/screens/onboarding/Login.tsx6
-rw-r--r--src/store/actions/user.ts16
-rw-r--r--src/store/initialStates.ts1
-rw-r--r--src/store/reducers/userReducer.ts4
-rw-r--r--src/types/types.ts22
-rw-r--r--src/utils/index.ts1
-rw-r--r--src/utils/layouts.ts1
-rw-r--r--src/utils/messages.ts106
-rw-r--r--src/utils/users.ts12
24 files changed, 495 insertions, 102 deletions
diff --git a/src/App.tsx b/src/App.tsx
index 9510c193..b8d64461 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,13 +1,14 @@
import {NavigationContainer} from '@react-navigation/native';
import React, {useState} from 'react';
import {Provider} from 'react-redux';
-import {Channel as ChannelType, StreamChat} from 'stream-chat';
+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,
@@ -21,17 +22,7 @@ import {
export const ChatContext = React.createContext({} as ChatContextType);
const App = () => {
- const [channel, setChannel] = useState<
- ChannelType<
- LocalAttachmentType,
- LocalChannelType,
- LocalCommandType,
- LocalEventType,
- LocalMessageType,
- LocalResponseType,
- LocalUserType
- >
- >();
+ const [channel, setChannel] = useState<ChannelGroupedType>();
const chatClient = StreamChat.getInstance<
LocalAttachmentType,
LocalChannelType,
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/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
index d8445580..660da97d 100644
--- a/src/components/messages/MessagesHeader.tsx
+++ b/src/components/messages/MessagesHeader.tsx
@@ -1,23 +1,31 @@
-import * as React from 'react';
+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>
- <Text style={styles.unreadText}>2 unread</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}>
- <Text>Compose</Text>
+ <TouchableOpacity style={styles.compose} onPress={createChannel}>
+ <ComposeIcon width={normalize(20)} height={normalize(20)} />
</TouchableOpacity>
</View>
);
diff --git a/src/components/messages/index.ts b/src/components/messages/index.ts
index 2d6bb581..e194093c 100644
--- a/src/components/messages/index.ts
+++ b/src/components/messages/index.ts
@@ -1 +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 05098d14..0d2a0331 100644
--- a/src/components/profile/Content.tsx
+++ b/src/components/profile/Content.tsx
@@ -1,4 +1,10 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react';
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
import {LayoutChangeEvent, RefreshControl, StyleSheet} from 'react-native';
import Animated, {
useSharedValue,
@@ -31,6 +37,7 @@ import ProfileCutout from './ProfileCutout';
import ProfileHeader from './ProfileHeader';
import PublicProfile from './PublicProfile';
import {useScrollToTop} from '@react-navigation/native';
+import {ChatContext} from '../../App';
interface ContentProps {
userXId: string | undefined;
@@ -52,6 +59,8 @@ const Content: React.FC<ContentProps> = ({userXId, screenType}) => {
);
const state: RootState = useStore().getState();
+ const {chatClient} = useContext(ChatContext);
+
/*
* Used to imperatively scroll to the top when presenting the moment tutorial.
*/
@@ -75,7 +84,7 @@ const Content: React.FC<ContentProps> = ({userXId, screenType}) => {
const refrestState = async () => {
setRefreshing(true);
if (!userXId) {
- await userLogin(dispatch, loggedInUser);
+ await userLogin(dispatch, loggedInUser, chatClient);
} else {
await fetchUserX(dispatch, user, screenType);
}
diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx
index 1a8d1e1a..dc68446b 100644
--- a/src/components/profile/ProfileBody.tsx
+++ b/src/components/profile/ProfileBody.tsx
@@ -1,5 +1,12 @@
import React, {useContext} from 'react';
-import {LayoutChangeEvent, Linking, StyleSheet, Text, View} from 'react-native';
+import {
+ Alert,
+ LayoutChangeEvent,
+ Linking,
+ StyleSheet,
+ Text,
+ View,
+} from 'react-native';
import {normalize} from 'react-native-elements';
import {useDispatch, useSelector, useStore} from 'react-redux';
import {TAGG_DARK_BLUE, TOGGLE_BUTTON_TYPE} from '../../constants';
@@ -20,9 +27,8 @@ import {
} from '../../utils';
import {FriendsButton, BasicButton} from '../common';
import ToggleButton from './ToggleButton';
-// import {ChatContext} from '../../App';
-// import {useNavigation} from '@react-navigation/core';
-// import AsyncStorage from '@react-native-community/async-storage';
+import {ChatContext} from '../../App';
+import {useNavigation} from '@react-navigation/core';
interface ProfileBodyProps {
onLayout: (event: LayoutChangeEvent) => void;
@@ -38,6 +44,9 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
userXId,
screenType,
}) => {
+ const dispatch = useDispatch();
+ const navigation = useNavigation();
+
const {profile = NO_PROFILE, user} = useSelector((state: RootState) =>
userXId ? state.userX[screenType][userXId] : state.user,
);
@@ -54,10 +63,10 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
profile,
);
+ const {chatClientReady} = useSelector((state: RootState) => state.user);
+ const {chatClient, setChannel} = useContext(ChatContext);
+
const state: RootState = useStore().getState();
- const dispatch = useDispatch();
- // const navigation = useNavigation();
- // const {chatClient, setChannel} = useContext(ChatContext);
const loggedInUserId = state.user.user.userId;
const handleAcceptRequest = async () => {
@@ -88,22 +97,15 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({
};
const onPressMessage = async () => {
- // TODO: Use function from util to create the channel and then navigate to screen
- // const channelName = username + ' and ' + state.user.user.username;
- // const chatToken = await AsyncStorage.getItem('chatToken');
- // await chatClient.connectUser(
- // {
- // id: loggedInUserId,
- // },
- // chatToken,
- // );
- // const channel = chatClient.channel('messaging', {
- // name: channelName,
- // members: [loggedInUserId, String(userXId)],
- // });
- // channel.create();
- // navigation.navigate('Chat');
- console.log('Navigate to ChatScreen');
+ if (!chatClientReady) {
+ Alert.alert('Something wrong with chat');
+ }
+ const channel = chatClient.channel('messaging', {
+ members: [loggedInUserId, String(userXId)],
+ });
+ channel.create();
+ setChannel(channel);
+ navigation.navigate('Chat');
};
return (
diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx
index 819ca785..173a6a6c 100644
--- a/src/routes/Routes.tsx
+++ b/src/routes/Routes.tsx
@@ -1,15 +1,17 @@
+import AsyncStorage from '@react-native-community/async-storage';
import messaging from '@react-native-firebase/messaging';
-import React, {useEffect, useState} from 'react';
+import React, {useContext, useEffect, useState} from 'react';
import DeviceInfo from 'react-native-device-info';
import SplashScreen from 'react-native-splash-screen';
-import {useDispatch, useSelector} from 'react-redux';
+import {useDispatch, useSelector, useStore} from 'react-redux';
+import {ChatContext} from '../App';
import {fcmService, getCurrentLiveVersions} from '../services';
import {
updateNewNotificationReceived,
updateNewVersionAvailable,
} from '../store/actions';
import {RootState} from '../store/rootReducer';
-import {userLogin} from '../utils';
+import {userLogin, connectChatAccount} from '../utils';
import Onboarding from './onboarding';
import NavigationBar from './tabs';
@@ -17,6 +19,9 @@ const Routes: React.FC = () => {
const {
user: {userId},
} = useSelector((state: RootState) => state.user);
+ const state: RootState = useStore().getState();
+ const loggedInUserId = state.user.user.userId;
+ const {chatClient} = useContext(ChatContext);
const [newVersionAvailable, setNewVersionAvailable] = useState(false);
const dispatch = useDispatch();
@@ -49,6 +54,7 @@ const Routes: React.FC = () => {
if (userId) {
fcmService.setUpPushNotifications();
fcmService.sendFcmTokenToServer();
+ connectChatAccount(loggedInUserId, chatClient, dispatch);
}
}, []);
diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx
index 8068b893..48c57920 100644
--- a/src/routes/main/MainStackScreen.tsx
+++ b/src/routes/main/MainStackScreen.tsx
@@ -34,7 +34,7 @@ import {
} 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';
/**
@@ -306,7 +306,10 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
<MainStack.Screen
name="Chat"
component={ChatScreen}
- options={{headerShown: true}}
+ options={{
+ ...headerBarOptions('black', ''),
+ headerStyle: {height: ChatHeaderHeight},
+ }}
/>
</MainStack.Navigator>
);
diff --git a/src/screens/chat/ChatListScreen.tsx b/src/screens/chat/ChatListScreen.tsx
index 9011ed4a..dbdb7994 100644
--- a/src/screens/chat/ChatListScreen.tsx
+++ b/src/screens/chat/ChatListScreen.tsx
@@ -1,13 +1,23 @@
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 {Alert, SafeAreaView, StatusBar, StyleSheet, View} from 'react-native';
+import {useSelector, useStore} from 'react-redux';
import {ChannelList, Chat} from 'stream-chat-react-native';
import {ChatContext} from '../../App';
-import {MessagesHeader} from '../../components/messages';
+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';
type ChatListScreenNavigationProp = StackNavigationProp<
MainStackParams,
@@ -19,9 +29,11 @@ interface ChatListScreenProps {
/*
* Screen that displays all of the user's active conversations.
*/
-const ChatListScreen: React.FC<ChatListScreenProps> = ({navigation}) => {
- const {chatClient, setChannel} = useContext(ChatContext);
- const [clientReady, setClientReady] = useState(false);
+const ChatListScreen: React.FC<ChatListScreenProps> = () => {
+ const {chatClient} = useContext(ChatContext);
+ const chatClientReady = useSelector(
+ (state: RootState) => state.user.chatClientReady,
+ );
const state: RootState = useStore().getState();
const loggedInUserId = state.user.user.userId;
@@ -34,22 +46,10 @@ const ChatListScreen: React.FC<ChatListScreenProps> = ({navigation}) => {
);
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);
- });
+ if (!chatClientReady) {
+ Alert.alert('Something wrong with chat');
}
- }, []);
+ }, [chatClientReady]);
return (
<View style={styles.background}>
@@ -65,26 +65,33 @@ const ChatListScreen: React.FC<ChatListScreenProps> = ({navigation}) => {
channel.create();
}}
/>
- {clientReady && (
+ {chatClientReady && (
<Chat client={chatClient}>
<View style={styles.chatContainer}>
- <ChannelList
+ <ChannelList<
+ LocalAttachmentType,
+ LocalChannelType,
+ LocalCommandType,
+ LocalEventType,
+ LocalMessageType,
+ LocalReactionType,
+ LocalUserType
+ >
filters={memoizedFilters}
- onSelect={(channel) => {
- setChannel(channel);
- navigation.navigate('Chat');
- }}
options={{
presence: true,
state: true,
watch: true,
}}
sort={{last_message_at: -1}}
+ maxUnreadCount={99}
+ Preview={ChannelPreview}
/>
</View>
</Chat>
)}
</SafeAreaView>
+ <TabsGradient />
</View>
);
};
diff --git a/src/screens/chat/ChatScreen.tsx b/src/screens/chat/ChatScreen.tsx
index eeb1a7d6..59c53c99 100644
--- a/src/screens/chat/ChatScreen.tsx
+++ b/src/screens/chat/ChatScreen.tsx
@@ -1,7 +1,8 @@
import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs';
-import {StackNavigationProp, useHeaderHeight} from '@react-navigation/stack';
+import {StackNavigationProp} from '@react-navigation/stack';
import React, {useContext} from 'react';
-import {StyleSheet, View} from 'react-native';
+import {StyleSheet} from 'react-native';
+import {SafeAreaView} from 'react-native-safe-area-context';
import {
Channel,
Chat,
@@ -9,7 +10,9 @@ import {
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 {
@@ -20,24 +23,30 @@ interface ChatScreenProps {
*/
const ChatScreen: React.FC<ChatScreenProps> = () => {
const {channel, chatClient} = useContext(ChatContext);
- const headerHeight = useHeaderHeight();
const tabbarHeight = useBottomTabBarHeight();
return (
- <View style={[styles.container, {paddingBottom: tabbarHeight}]}>
+ <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={headerHeight}>
+ <Channel channel={channel} keyboardVerticalOffset={0}>
<MessageList onThreadSelect={() => {}} />
<MessageInput />
</Channel>
</Chat>
- </View>
+ </SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
+ flex: 1,
},
});
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/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx
index 9d5fbe4d..1407575c 100644
--- a/src/screens/onboarding/CategorySelection.tsx
+++ b/src/screens/onboarding/CategorySelection.tsx
@@ -1,6 +1,6 @@
import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
-import React, {useEffect, useState} from 'react';
+import React, {useContext, useEffect, useState} from 'react';
import {
Alert,
Platform,
@@ -12,6 +12,7 @@ import {
} from 'react-native';
import {ScrollView} from 'react-native-gesture-handler';
import {useDispatch, useSelector} from 'react-redux';
+import {ChatContext} from '../../App';
import PlusIcon from '../../assets/icons/plus_icon-01.svg';
import {Background, MomentCategory} from '../../components';
import {MOMENT_CATEGORIES} from '../../constants';
@@ -49,6 +50,7 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({
* Same component to be used for category selection while onboarding and while on profile
*/
const {screenType, user} = route.params;
+ const {chatClient} = useContext(ChatContext);
const isOnBoarding: boolean =
screenType === CategorySelectionScreenType.Onboarding;
const {userId, username} = user;
@@ -168,7 +170,7 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({
dispatch(updateIsOnboardedUser(true));
const token = await getTokenOrLogout(dispatch);
await postMomentCategories(selectedCategories, token);
- userLogin(dispatch, {userId: userId, username: username});
+ userLogin(dispatch, {userId: userId, username: username}, chatClient);
} else {
dispatch(
updateMomentCategories(
diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx
index e160b4b7..774a7a11 100644
--- a/src/screens/onboarding/InvitationCodeVerification.tsx
+++ b/src/screens/onboarding/InvitationCodeVerification.tsx
@@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-community/async-storage';
import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
-import React from 'react';
+import React, {useContext} from 'react';
import {Alert, KeyboardAvoidingView, StyleSheet, View} from 'react-native';
import {Text} from 'react-native-animatable';
import {
@@ -27,6 +27,7 @@ import {
import {OnboardingStackParams} from '../../routes';
import {BackgroundGradientType} from '../../types';
import {SCREEN_WIDTH, userLogin} from '../../utils';
+import {ChatContext} from '../../App';
type InvitationCodeVerificationRouteProp = RouteProp<
OnboardingStackParams,
@@ -58,6 +59,7 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
setValue,
});
const dispatch = useDispatch();
+ const {chatClient} = useContext(ChatContext);
const handleInvitationCodeVerification = async () => {
if (value.length === 6) {
@@ -77,7 +79,7 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
const username = route.params.username;
await AsyncStorage.setItem('userId', userId);
await AsyncStorage.setItem('username', username);
- userLogin(dispatch, {userId, username});
+ userLogin(dispatch, {userId, username}, chatClient);
} else {
Alert.alert(ERROR_INVALID_INVITATION_CODE);
}
diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx
index dd2bb2e4..4f2b6a64 100644
--- a/src/screens/onboarding/Login.tsx
+++ b/src/screens/onboarding/Login.tsx
@@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-community/async-storage';
import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
-import React, {useEffect, useRef} from 'react';
+import React, {useContext, useEffect, useRef} from 'react';
import {
Alert,
Image,
@@ -14,6 +14,7 @@ import {
} from 'react-native';
import SplashScreen from 'react-native-splash-screen';
import {useDispatch, useSelector} from 'react-redux';
+import {ChatContext} from '../../App';
import {Background, TaggInput, TaggSquareButton} from '../../components';
import {LOGIN_ENDPOINT, usernameRegex} from '../../constants';
import {
@@ -47,6 +48,7 @@ interface LoginProps {
const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => {
// ref for focusing on input fields
const inputRef = useRef();
+ const {chatClient} = useContext(ChatContext);
// login form state
const [form, setForm] = React.useState({
@@ -166,7 +168,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => {
if (statusCode === 200 && data.isOnboarded) {
//Stores token received in the response into client's AsynStorage
try {
- userLogin(dispatch, {userId: data.UserID, username});
+ userLogin(dispatch, {userId: data.UserID, username}, chatClient);
fcmService.sendFcmTokenToServer();
} catch (err) {
Alert.alert(ERROR_INVALID_LOGIN);
diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts
index 3ebd4190..0ed57fe6 100644
--- a/src/store/actions/user.ts
+++ b/src/store/actions/user.ts
@@ -11,6 +11,7 @@ import {getTokenOrLogout} from '../../utils';
import {
clearHeaderAndProfileImages,
profileCompletionStageUpdated,
+ setChatClientReady,
setIsOnboardedUser,
setNewNotificationReceived,
setNewVersionAvailable,
@@ -233,3 +234,18 @@ export const suggestedPeopleAnimatedTutorialFinished = (
console.log('Error while updating suggested people linked state: ', error);
}
};
+
+export const updateChatClientReady = (
+ chatClientReady: boolean,
+): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async (
+ dispatch,
+) => {
+ try {
+ dispatch({
+ type: setChatClientReady.type,
+ payload: {chatClientReady},
+ });
+ } catch (error) {
+ console.log(error);
+ }
+};
diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts
index 02331eb6..546c57a9 100644
--- a/src/store/initialStates.ts
+++ b/src/store/initialStates.ts
@@ -41,6 +41,7 @@ export const EMPTY_PROFILE_PREVIEW_LIST = <ProfilePreviewType[]>[];
export const NO_USER_DATA = {
user: <UserType>NO_USER,
+ chatClientReady: <boolean>false,
profile: <ProfileInfoType>NO_PROFILE,
avatar: <string | undefined>undefined,
cover: <string | undefined>undefined,
diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts
index 9ff9ba01..0b958cac 100644
--- a/src/store/reducers/userReducer.ts
+++ b/src/store/reducers/userReducer.ts
@@ -75,6 +75,9 @@ const userDataSlice = createSlice({
state.avatar = '';
state.cover = '';
},
+ setChatClientReady: (state, action) => {
+ state.chatClientReady = action.payload.chatClientReady;
+ },
},
});
@@ -90,5 +93,6 @@ export const {
setReplyPosted,
setSuggestedPeopleImage,
clearHeaderAndProfileImages,
+ setChatClientReady,
} = userDataSlice.actions;
export const userDataReducer = userDataSlice.reducer;
diff --git a/src/types/types.ts b/src/types/types.ts
index 1a352808..376c4be0 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -298,20 +298,20 @@ 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:
- | ChannelType<
- LocalAttachmentType,
- LocalChannelType,
- LocalCommandType,
- LocalEventType,
- LocalMessageType,
- LocalResponseType,
- LocalUserType
- >
- | undefined;
+ channel: ChannelGroupedType | undefined;
setChannel: React.Dispatch<
React.SetStateAction<
| ChannelType<
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 739e6fb8..4ff9afac 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -4,3 +4,4 @@ export * from './common';
export * from './users';
export * from './friends';
export * from './search';
+export * from './messages';
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..b2162d34
--- /dev/null
+++ b/src/utils/messages.ts
@@ -0,0 +1,106 @@
+import AsyncStorage from '@react-native-community/async-storage';
+import moment from 'moment';
+import {updateChatClientReady} from '../store/actions';
+import {AppDispatch} from '../store/configureStore';
+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;
+};
+
+export const connectChatAccount = async (
+ loggedInUserId: string,
+ chatClient,
+ dispatch: AppDispatch,
+) => {
+ try {
+ const chatToken = await AsyncStorage.getItem('chatToken');
+ await chatClient.connectUser(
+ {
+ id: loggedInUserId,
+ },
+ chatToken,
+ );
+ dispatch(updateChatClientReady(true));
+ } catch (err) {
+ dispatch(updateChatClientReady(false));
+ console.log('Error while connecting user to Stream: ', err);
+ }
+};
diff --git a/src/utils/users.ts b/src/utils/users.ts
index 22c1c1f0..ec09198d 100644
--- a/src/utils/users.ts
+++ b/src/utils/users.ts
@@ -12,18 +12,17 @@ import {
logout,
} from '../store/actions';
import {NO_SOCIAL_ACCOUNTS} from '../store/initialStates';
-import {userLoggedIn} from '../store/reducers';
import {loadUserMomentCategories} from './../store/actions/momentCategories';
import {loadUserX} from './../store/actions/userX';
import {AppDispatch} from './../store/configureStore';
import {RootState} from './../store/rootReducer';
import {
ProfilePreviewType,
- CategoryPreviewType,
ProfileInfoType,
ScreenType,
UserType,
} from './../types/types';
+import {connectChatAccount} from './messages';
const loadData = async (dispatch: AppDispatch, user: UserType) => {
await Promise.all([
@@ -44,7 +43,11 @@ const loadData = async (dispatch: AppDispatch, user: UserType) => {
* @param dispatch This is the dispatch object from the redux store
* @param user The user if at all any
*/
-export const userLogin = async (dispatch: AppDispatch, user: UserType) => {
+export const userLogin = async (
+ dispatch: AppDispatch,
+ user: UserType,
+ chatClient?,
+) => {
try {
let localUser = {...user};
if (!user.userId) {
@@ -64,6 +67,9 @@ export const userLogin = async (dispatch: AppDispatch, user: UserType) => {
AsyncStorage.setItem('username', user.username),
]);
}
+ if (chatClient) {
+ connectChatAccount(localUser.userId, chatClient, dispatch);
+ }
await loadData(dispatch, localUser);
} catch (error) {
console.log(error);