diff options
Diffstat (limited to 'src')
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); |