diff options
author | Ivan Chen <ivan@tagg.id> | 2021-04-09 19:55:26 -0400 |
---|---|---|
committer | Ivan Chen <ivan@tagg.id> | 2021-04-09 19:55:26 -0400 |
commit | 0a480048b41a80e8569ce57064d1b9716c3d25e3 (patch) | |
tree | 4f1118560c10dcdfa32e99d2b73c3d7814d7904d /src | |
parent | 17de7d8312b10f84af2178f769ff92bf96ab47f5 (diff) | |
parent | 9d5ad9bea36c0b2abffd04b25126d18158017137 (diff) |
Merge branch 'master' into tma784-style-message-input
# Conflicts:
# src/screens/chat/ChatListScreen.tsx
# src/screens/chat/ChatScreen.tsx
Diffstat (limited to 'src')
32 files changed, 453 insertions, 156 deletions
diff --git a/src/assets/badges/brown/iff.png b/src/assets/badges/brown/iff.png Binary files differnew file mode 100644 index 00000000..02f9a1c8 --- /dev/null +++ b/src/assets/badges/brown/iff.png diff --git a/src/assets/badges/brown/sailing.png b/src/assets/badges/brown/sailing.png Binary files differnew file mode 100644 index 00000000..1fde1c39 --- /dev/null +++ b/src/assets/badges/brown/sailing.png diff --git a/src/components/common/AcceptDeclineButtons.tsx b/src/components/common/AcceptDeclineButtons.tsx index 167148f0..7bb62fd4 100644 --- a/src/components/common/AcceptDeclineButtons.tsx +++ b/src/components/common/AcceptDeclineButtons.tsx @@ -58,7 +58,7 @@ const styles = StyleSheet.create({ backgroundColor: TAGG_LIGHT_BLUE, }, rejectButton: { - borderWidth: 1, + borderWidth: 2, backgroundColor: 'white', borderColor: TAGG_LIGHT_BLUE, }, diff --git a/src/components/common/BasicButton.tsx b/src/components/common/BasicButton.tsx new file mode 100644 index 00000000..1fe29cd9 --- /dev/null +++ b/src/components/common/BasicButton.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import {StyleProp, StyleSheet, Text, View, ViewStyle} from 'react-native'; +import {TAGG_LIGHT_BLUE} from '../../constants'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {normalize} from '../../utils'; + +interface BasicButtonProps { + title: string; + onPress: () => void; + solid?: boolean; + externalStyles?: Record<string, StyleProp<ViewStyle>>; +} +const BasicButton: React.FC<BasicButtonProps> = ({ + title, + onPress, + solid, + externalStyles, +}) => { + return ( + <View style={[styles.container, externalStyles?.container]}> + <TouchableOpacity + style={[ + styles.genericButtonStyle, + solid ? styles.solidButton : styles.outlineButton, + ]} + onPress={onPress}> + <Text + style={[ + styles.buttonTitle, + solid + ? styles.solidButtonTitleColor + : styles.outlineButtonTitleColor, + ]}> + {title} + </Text> + </TouchableOpacity> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + height: '100%', + flexDirection: 'column', + justifyContent: 'space-around', + }, + genericButtonStyle: { + justifyContent: 'center', + alignItems: 'center', + borderRadius: 3, + padding: 0, + width: '100%', + height: '100%', + }, + solidButton: { + padding: 0, + backgroundColor: TAGG_LIGHT_BLUE, + }, + outlineButton: { + borderWidth: 2, + backgroundColor: 'white', + borderColor: TAGG_LIGHT_BLUE, + }, + solidButtonTitleColor: { + color: 'white', + }, + outlineButtonTitleColor: { + color: TAGG_LIGHT_BLUE, + }, + buttonTitle: { + fontSize: normalize(15), + fontWeight: '700', + letterSpacing: 1, + }, +}); + +export default BasicButton; diff --git a/src/components/common/FriendsButton.tsx b/src/components/common/FriendsButton.tsx index 46421bd1..6ddad93f 100644 --- a/src/components/common/FriendsButton.tsx +++ b/src/components/common/FriendsButton.tsx @@ -100,7 +100,7 @@ const styles = StyleSheet.create({ button: { justifyContent: 'center', alignItems: 'center', - width: SCREEN_WIDTH * 0.4, + width: SCREEN_WIDTH * 0.42, aspectRatio: 154 / 33, borderWidth: 2, borderColor: TAGG_LIGHT_BLUE, diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 8499dbfa..b5fd0542 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -23,3 +23,4 @@ export {default as AcceptDeclineButtons} from './AcceptDeclineButtons'; export {default as FriendsButton} from './FriendsButton'; export {default as TaggSquareButton} from './TaggSquareButton'; export {default as GradientBorderButton} from './GradientBorderButton'; +export {default as BasicButton} from './BasicButton'; diff --git a/src/components/messages/ChatHeader.tsx b/src/components/messages/ChatHeader.tsx index 2bc096ec..67a7f1fe 100644 --- a/src/components/messages/ChatHeader.tsx +++ b/src/components/messages/ChatHeader.tsx @@ -1,39 +1,71 @@ +import {useNavigation} from '@react-navigation/native'; import React, {useContext} from 'react'; import {Image, StyleSheet, View} from 'react-native'; import {Text} from 'react-native-animatable'; -import {useStore} from 'react-redux'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {useDispatch, useStore} from 'react-redux'; import {ChatContext} from '../../App'; -import {ChatHeaderHeight, normalize, StatusBarHeight} from '../../utils'; +import {ScreenType} from '../../types'; +import { + ChatHeaderHeight, + fetchUserX, + normalize, + StatusBarHeight, + userXInStore, +} from '../../utils'; import {formatLastSeenText, getMember, isOnline} from '../../utils/messages'; -type ChatHeaderProps = {}; +type ChatHeaderProps = { + screenType: ScreenType; +}; -const ChatHeader: React.FC<ChatHeaderProps> = () => { +const ChatHeader: React.FC<ChatHeaderProps> = (props) => { + const {screenType} = props; const {channel} = useContext(ChatContext); + const dispatch = useDispatch(); + const navigation = useNavigation(); const state = useStore().getState(); const member = getMember(channel, state); const online = isOnline(member?.user?.last_active); const lastSeen = formatLastSeenText(member?.user?.last_active); + const toProfile = async () => { + if (member && member.user && member.user.username) { + if (!userXInStore(state, screenType, member.user.id)) { + await fetchUserX( + dispatch, + {userId: member.user.id, username: member.user.username}, + screenType, + ); + } + navigation.navigate('Profile', { + userXId: member.user.id, + screenType, + }); + } + }; + 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> + <TouchableOpacity style={styles.tappables} onPress={toProfile}> + <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> + </TouchableOpacity> </View> ); }; @@ -41,10 +73,13 @@ const ChatHeader: React.FC<ChatHeaderProps> = () => { const styles = StyleSheet.create({ container: { height: ChatHeaderHeight - StatusBarHeight, - flexDirection: 'row', - alignItems: 'center', paddingLeft: '15%', }, + tappables: { + alignItems: 'center', + flexDirection: 'row', + width: '100%', + }, avatar: { width: normalize(40), height: normalize(40), diff --git a/src/components/messages/MessagesHeader.tsx b/src/components/messages/MessagesHeader.tsx index 660da97d..1bd9b55a 100644 --- a/src/components/messages/MessagesHeader.tsx +++ b/src/components/messages/MessagesHeader.tsx @@ -1,10 +1,10 @@ -import React, {Fragment, useContext} from 'react'; +import React, {Fragment, useContext, useEffect, useState} 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'; +import ComposeIcon from '../../assets/icons/compose.svg'; +import {normalize} from '../../utils'; type MessagesHeaderProps = { createChannel: () => void; @@ -12,11 +12,30 @@ type MessagesHeaderProps = { const MessagesHeader: React.FC<MessagesHeaderProps> = ({createChannel}) => { const {chatClient} = useContext(ChatContext); - const unread = chatClient.user?.total_unread_count as number; + const [unread, setUnread] = useState(0); + + useEffect(() => { + const newCount = chatClient.user?.total_unread_count as number; + if (newCount) { + setUnread(newCount); + } + const listener = chatClient?.on((e) => { + if (e.total_unread_count) { + setUnread(e.total_unread_count); + } + }); + + return () => { + if (listener) { + listener.unsubscribe(); + } + }; + }, [chatClient]); + return ( <View style={styles.header}> <Text style={styles.headerText}>Messages</Text> - {unread && unread !== 0 ? ( + {unread !== 0 ? ( <Text style={styles.unreadText}> {unread > 99 ? '99+' : unread} unread </Text> diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index 05098d14..0052b61d 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. */ diff --git a/src/components/profile/Friends.tsx b/src/components/profile/Friends.tsx index c1dca755..b754b71a 100644 --- a/src/components/profile/Friends.tsx +++ b/src/components/profile/Friends.tsx @@ -191,7 +191,7 @@ const styles = StyleSheet.create({ height: '55%', borderColor: TAGG_LIGHT_BLUE, borderWidth: 2, - borderRadius: 2, + borderRadius: 3, padding: 0, backgroundColor: TAGG_LIGHT_BLUE, }, @@ -212,7 +212,7 @@ const styles = StyleSheet.create({ height: '55%', borderColor: TAGG_LIGHT_BLUE, borderWidth: 2, - borderRadius: 2, + borderRadius: 3, padding: 0, }, unfriendButtonTitle: { diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index b49e71a3..527036f6 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -1,5 +1,12 @@ -import React from 'react'; -import {LayoutChangeEvent, Linking, StyleSheet, Text, View} from 'react-native'; +import React, {useContext} from 'react'; +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'; @@ -9,12 +16,23 @@ import { updateUserXFriends, updateUserXProfileAllScreens, } from '../../store/actions'; +import {canViewProfile} from '../../utils/users'; import {NO_PROFILE} from '../../store/initialStates'; import {RootState} from '../../store/rootReducer'; import {ScreenType} from '../../types'; -import {getUserAsProfilePreviewType} from '../../utils'; -import {FriendsButton} from '../common'; +import { + connectChatAccount, + createChannel, + getUserAsProfilePreviewType, + SCREEN_HEIGHT, + SCREEN_WIDTH, +} from '../../utils'; +import {FriendsButton, BasicButton} from '../common'; import ToggleButton from './ToggleButton'; +import {ChatContext} from '../../App'; +import {useNavigation} from '@react-navigation/core'; +import {ChatListScreen} from '../../screens'; +import {ERROR_UNABLE_CONNECT_CHAT} from '../../constants/strings'; interface ProfileBodyProps { onLayout: (event: LayoutChangeEvent) => void; @@ -30,6 +48,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, ); @@ -46,8 +67,10 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({ profile, ); + const {chatClient, setChannel} = useContext(ChatContext); + const state: RootState = useStore().getState(); - const dispatch = useDispatch(); + const loggedInUserId = state.user.user.userId; const handleAcceptRequest = async () => { await dispatch(acceptFriendRequest({id, username, first_name, last_name})); @@ -60,6 +83,32 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({ dispatch(updateUserXProfileAllScreens(id, state)); }; + const canMessage = () => { + if ( + userXId && + !isBlocked && + (friendship_status === 'no_record' || + friendship_status === 'friends' || + (friendship_status === 'requested' && + friendship_requester_id === loggedInUserId)) && + canViewProfile(state, userXId, screenType) + ) { + return true; + } else { + return false; + } + }; + + const onPressMessage = async () => { + if (chatClient.user && userXId) { + const channel = await createChannel(loggedInUserId, userXId, chatClient); + setChannel(channel); + navigation.navigate('Chat'); + } else { + Alert.alert(ERROR_UNABLE_CONNECT_CHAT); + } + }; + return ( <View onLayout={onLayout} style={styles.container}> <Text style={styles.username}>{`@${username}`}</Text> @@ -85,17 +134,31 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({ /> </View> )} - {userXId && !isBlocked && ( - <View style={styles.buttonsContainer}> - <FriendsButton - userXId={userXId} - screenType={screenType} - friendship_requester_id={friendship_requester_id} - onAcceptRequest={handleAcceptRequest} - onRejectRequest={handleDeclineFriendRequest} - /> - </View> - )} + <View style={styles.simpleRowContainer}> + {userXId && !isBlocked && ( + <View style={styles.buttonsContainer}> + <FriendsButton + userXId={userXId} + screenType={screenType} + friendship_requester_id={friendship_requester_id} + onAcceptRequest={handleAcceptRequest} + onRejectRequest={handleDeclineFriendRequest} + /> + {canMessage() && ( + <BasicButton + title={'Message'} + onPress={onPressMessage} + externalStyles={{ + container: { + width: SCREEN_WIDTH * 0.42, + aspectRatio: 154 / 33, + }, + }} + /> + )} + </View> + )} + </View> </View> ); }; @@ -107,11 +170,15 @@ const styles = StyleSheet.create({ paddingTop: '3.5%', paddingBottom: '2%', }, + simpleRowContainer: {flexDirection: 'row'}, buttonsContainer: { flex: 1, paddingTop: '3.5%', paddingBottom: '2%', width: '50%', + height: SCREEN_HEIGHT * 0.1, + flexDirection: 'row', + justifyContent: 'space-between', }, container: { paddingVertical: '1%', diff --git a/src/components/profile/ToggleButton.tsx b/src/components/profile/ToggleButton.tsx index 236d811c..4697d744 100644 --- a/src/components/profile/ToggleButton.tsx +++ b/src/components/profile/ToggleButton.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {StyleSheet, Text} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; import {TAGG_LIGHT_BLUE} from '../../constants'; -import {getToggleButtonText, SCREEN_WIDTH} from '../../utils'; +import {getToggleButtonText, normalize, SCREEN_WIDTH} from '../../utils'; type ToggleButtonProps = { toggleState: boolean; @@ -34,15 +34,17 @@ const styles = StyleSheet.create({ button: { justifyContent: 'center', alignItems: 'center', - width: SCREEN_WIDTH * 0.4, + width: SCREEN_WIDTH * 0.42, height: SCREEN_WIDTH * 0.08, borderColor: TAGG_LIGHT_BLUE, - borderWidth: 3, - borderRadius: 5, + borderWidth: 2, + borderRadius: 3, marginRight: '2%', }, text: { - fontWeight: 'bold', + fontWeight: '700', + fontSize: normalize(15), + letterSpacing: 1, }, buttonColor: { backgroundColor: TAGG_LIGHT_BLUE, diff --git a/src/components/suggestedPeople/SPTaggsBar.tsx b/src/components/suggestedPeople/SPTaggsBar.tsx index adac6dcf..29c58cce 100644 --- a/src/components/suggestedPeople/SPTaggsBar.tsx +++ b/src/components/suggestedPeople/SPTaggsBar.tsx @@ -70,7 +70,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ setTaggsNeedUpdate={setTaggsNeedUpdate} setSocialDataNeedUpdate={handleSocialUpdate} whiteRing={true} - allowNavigation={allowTaggsNavigation} + screenType={screenType} />, ); i++; @@ -88,7 +88,7 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ userXId={userXId} user={user} whiteRing={true} - allowNavigation={allowTaggsNavigation} + screenType={screenType} />, ); i++; diff --git a/src/components/taggs/Tagg.tsx b/src/components/taggs/Tagg.tsx index 4e4987fb..5d26539b 100644 --- a/src/components/taggs/Tagg.tsx +++ b/src/components/taggs/Tagg.tsx @@ -17,13 +17,15 @@ import { registerNonIntegratedSocialLink, } from '../../services'; import {SmallSocialIcon, SocialIcon, SocialLinkModal} from '../common'; -import {UserType} from '../../types'; +import {ScreenType, UserType} from '../../types'; import { ERROR_LINK, ERROR_UNABLE_TO_FIND_PROFILE, SUCCESS_LINK, } from '../../constants/strings'; -import {normalize} from '../../utils'; +import {canViewProfile, normalize} from '../../utils'; +import {RootState} from '../../store/rootReducer'; +import {useStore} from 'react-redux'; interface TaggProps { social: string; @@ -34,7 +36,7 @@ interface TaggProps { userXId: string | undefined; user: UserType; whiteRing: boolean | undefined; - allowNavigation?: boolean; + screenType: ScreenType; } const Tagg: React.FC<TaggProps> = ({ @@ -44,11 +46,12 @@ const Tagg: React.FC<TaggProps> = ({ setTaggsNeedUpdate, setSocialDataNeedUpdate, userXId, + screenType, user, whiteRing, - allowNavigation = true, }) => { const navigation = useNavigation(); + const state: RootState = useStore().getState(); const [modalVisible, setModalVisible] = useState(false); const youMayPass = isLinked || userXId; @@ -72,19 +75,21 @@ const Tagg: React.FC<TaggProps> = ({ const modalOrAuthBrowserOrPass = async () => { if (youMayPass) { - if (INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1) { - navigation.push('SocialMediaTaggs', { - socialMediaType: social, - userXId, - }); - } else { - getNonIntegratedURL(social, user.userId).then((socialURL) => { - if (socialURL) { - Linking.openURL(socialURL); - } else { - Alert.alert(ERROR_UNABLE_TO_FIND_PROFILE); - } - }); + if (canViewProfile(state, userXId, screenType)) { + if (INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1) { + navigation.navigate('SocialMediaTaggs', { + socialMediaType: social, + userXId, + }); + } else { + getNonIntegratedURL(social, user.userId).then((socialURL) => { + if (socialURL) { + Linking.openURL(socialURL); + } else { + Alert.alert(ERROR_UNABLE_TO_FIND_PROFILE); + } + }); + } } } else { if (isIntegrated) { @@ -147,8 +152,7 @@ const Tagg: React.FC<TaggProps> = ({ <View style={whiteRing ? styles.spcontainer : styles.container}> <TouchableOpacity style={styles.iconTap} - onPress={modalOrAuthBrowserOrPass} - disabled={!allowNavigation}> + onPress={modalOrAuthBrowserOrPass}> <SocialIcon style={styles.icon} social={social} whiteRing /> {pickTheRightRingHere()} </TouchableOpacity> diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx index a5003fbb..4d567b25 100644 --- a/src/components/taggs/TaggsBar.tsx +++ b/src/components/taggs/TaggsBar.tsx @@ -7,7 +7,7 @@ import Animated, { useDerivedValue, } from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {useDispatch, useSelector, useStore} from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; import { INTEGRATED_SOCIAL_LIST, PROFILE_CUTOUT_BOTTOM_Y, @@ -17,7 +17,6 @@ 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 './Tagg'; const {View, ScrollView} = Animated; @@ -37,15 +36,12 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ linkedSocials, onLayout, }) => { + const dispatch = useDispatch(); 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(); const insetTop = useSafeAreaInsets().top; /** * Updates the individual social that needs update @@ -80,13 +76,13 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ key={i} social={social} userXId={userXId} + screenType={screenType} user={user} isLinked={true} isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1} setTaggsNeedUpdate={setTaggsNeedUpdate} setSocialDataNeedUpdate={handleSocialUpdate} whiteRing={false} - allowNavigation={allowTaggsNavigation} />, ); i++; @@ -102,9 +98,9 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ setTaggsNeedUpdate={setTaggsNeedUpdate} setSocialDataNeedUpdate={handleSocialUpdate} userXId={userXId} + screenType={screenType} user={user} whiteRing={false} - allowNavigation={allowTaggsNavigation} />, ); i++; diff --git a/src/constants/api.ts b/src/constants/api.ts index 43294386..cb45b238 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -61,6 +61,10 @@ export const UPDATE_BADGES_ENDPOINT: string = // Register as FCM device export const FCM_ENDPOINT: string = API_URL + 'fcm/'; +// Retrieve Stream Chat token +export const CHAT_ENDPOINT: string = API_URL + 'chat/'; +export const CHAT_TOKEN_ENDPOINT: string = CHAT_ENDPOINT + 'get_token/'; + // Register Social Link (Non-integrated) export const LINK_SNAPCHAT_ENDPOINT: string = API_URL + 'link-sc/'; export const LINK_TIKTOK_ENDPOINT: string = API_URL + 'link-tt/'; diff --git a/src/constants/badges.ts b/src/constants/badges.ts index b4cecefb..d7e9357b 100644 --- a/src/constants/badges.ts +++ b/src/constants/badges.ts @@ -23,10 +23,12 @@ export const _brownBadgeImages = { football: require('../assets/badges/brown/football.png'), gymnastics: require('../assets/badges/brown/gymnastics.png'), hockey: require('../assets/badges/brown/hockey.png'), + iff: require('../assets/badges/brown/iff.png'), impulse: require('../assets/badges/brown/impulse.png'), kappa_delta: require('../assets/badges/brown/kappa_delta.png'), lacrosse: require('../assets/badges/brown/lacrosse.png'), latin_at_brown: require('../assets/badges/brown/latin_at_brown.png'), + sailing: require('../assets/badges/brown/sailing.png'), soccer: require('../assets/badges/brown/soccer.png'), softball: require('../assets/badges/brown/softball.png'), tap: require('../assets/badges/brown/tap.png'), @@ -167,6 +169,10 @@ const _brownBadges = [ badgeName: 'Brown Baseball', badgeImage: _brownBadgeImages.baseball, }, + { + badgeName: 'Brown Sailing', + badgeImage: _brownBadgeImages.sailing, + }, ], }, @@ -266,6 +272,10 @@ const _brownBadges = [ badgeName: 'Impulse', badgeImage: _brownBadgeImages.impulse, }, + { + badgeName: 'Ivy Film Festival', + badgeImage: _brownBadgeImages.iff, + }, ], }, { diff --git a/src/constants/strings.ts b/src/constants/strings.ts index 300ceb90..bdb94fba 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -9,6 +9,7 @@ export const ERROR_AUTHENTICATION = 'An error occurred during authentication. Pl export const ERROR_BADGES_EXCEED_LIMIT = 'You can\'t have more than 5 badges!'; export const ERROR_CATEGORY_CREATION = 'There was a problem creating your categories. Please refresh and try again.'; export const ERROR_CATEGORY_UPDATE = 'There was a problem updating your categories. Please refresh and try again'; +export const ERROR_CHAT_CONNECTION = `Unable to establish chat connection`; export const ERROR_DELETE_CATEGORY = 'There was a problem while deleting category. Please try again'; export const ERROR_DELETE_MOMENT = 'Unable to delete moment, please try again later!'; export const ERROR_DELETED_OBJECT = 'Oh sad! Looks like the comment / moment was deleted by the user'; @@ -44,6 +45,7 @@ export const ERROR_SOMETHING_WENT_WRONG = 'Oh dear, don’t worry someone will b export const ERROR_SOMETHING_WENT_WRONG_REFRESH = "Ha, looks like this one's on us, please refresh and try again"; export const ERROR_SOMETHING_WENT_WRONG_RELOAD = "You broke it, Just kidding! we don't know what happened... Please reload the app and try again"; export const ERROR_TWILIO_SERVER_ERROR = 'mhm, looks like that is an invalid phone number or our servers are down, please try again in a few mins'; +export const ERROR_UNABLE_CONNECT_CHAT = 'Unable to connect chat'; export const ERROR_UNABLE_TO_FIND_PROFILE = 'We were unable to find this profile. Please check username and try again'; export const ERROR_UNABLE_TO_VIEW_PROFILE = 'Unable to view this profile'; export const ERROR_UPLOAD = 'An error occurred while uploading. Please try again!'; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 819ca785..5ce0c771 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(); @@ -53,6 +58,10 @@ const Routes: React.FC = () => { }, []); useEffect(() => { + connectChatAccount(loggedInUserId, chatClient); + }, [loggedInUserId]); + + useEffect(() => { const checkVersion = async () => { const liveVersions = await getCurrentLiveVersions(); if (liveVersions && !liveVersions.includes(DeviceInfo.getVersion())) { diff --git a/src/screens/chat/ChatListScreen.tsx b/src/screens/chat/ChatListScreen.tsx index c11f4d1d..0cbc7592 100644 --- a/src/screens/chat/ChatListScreen.tsx +++ b/src/screens/chat/ChatListScreen.tsx @@ -1,8 +1,7 @@ -import AsyncStorage from '@react-native-community/async-storage'; import {useBottomTabBarHeight} from '@react-navigation/bottom-tabs'; import {StackNavigationProp} from '@react-navigation/stack'; import React, {useContext, useEffect, useMemo, useState} from 'react'; -import {SafeAreaView, StatusBar, StyleSheet, View} from 'react-native'; +import {Alert, SafeAreaView, StatusBar, StyleSheet, View} from 'react-native'; import {useStore} from 'react-redux'; import {ChannelList, Chat} from 'stream-chat-react-native'; import {ChatContext} from '../../App'; @@ -19,7 +18,7 @@ import { LocalReactionType, LocalUserType, } from '../../types'; -import {HeaderHeight} from '../../utils'; +import {connectChatAccount, HeaderHeight} from '../../utils'; import NewChatModal from './NewChatModal'; type ChatListScreenNavigationProp = StackNavigationProp< @@ -35,8 +34,6 @@ interface ChatListScreenProps { const ChatListScreen: React.FC<ChatListScreenProps> = () => { const {chatClient} = useContext(ChatContext); const [modalVisible, setChatModalVisible] = useState(false); - - const [clientReady, setClientReady] = useState(false); const state: RootState = useStore().getState(); const loggedInUserId = state.user.user.userId; const tabbarHeight = useBottomTabBarHeight(); @@ -59,21 +56,16 @@ const ChatListScreen: React.FC<ChatListScreenProps> = () => { }; 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); + connectChatAccount(loggedInUserId, chatClient) + .then((success) => { + if (!success) { + Alert.alert('Something wrong with chat'); + } + }) + .catch((err) => { + console.log('Error connecting to chat: ', err); + Alert.alert('Something wrong with chat'); }); - } }, []); return ( @@ -85,31 +77,29 @@ const ChatListScreen: React.FC<ChatListScreenProps> = () => { setChatModalVisible(true); }} /> - {clientReady && ( - <Chat client={chatClient} style={chatTheme}> - <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> - )} + <Chat client={chatClient} style={chatTheme}> + <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 /> diff --git a/src/screens/chat/ChatScreen.tsx b/src/screens/chat/ChatScreen.tsx index baf1e23d..3d791f3c 100644 --- a/src/screens/chat/ChatScreen.tsx +++ b/src/screens/chat/ChatScreen.tsx @@ -14,6 +14,7 @@ import UpArrowIcon from '../../assets/icons/up_arrow.svg'; import ChatHeader from '../../components/messages/ChatHeader'; import {TAGG_LIGHT_BLUE} from '../../constants'; import {MainStackParams} from '../../routes'; +import {ScreenType} from '../../types'; import {isIPhoneX} from '../../utils'; type ChatScreenNavigationProp = StackNavigationProp<MainStackParams, 'Chat'>; @@ -48,7 +49,7 @@ const ChatScreen: React.FC<ChatScreenProps> = () => { // unable to figure out the padding issue, a hacky solution {paddingBottom: isIPhoneX() ? tabbarHeight + 20 : tabbarHeight + 50}, ]}> - <ChatHeader /> + <ChatHeader screenType={ScreenType.Chat} /> <Chat client={chatClient} style={chatTheme}> <Channel channel={channel} diff --git a/src/screens/onboarding/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx index 9d5fbe4d..ab5ff3be 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; diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx index e160b4b7..6bc0ac9d 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) { diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index dd2bb2e4..6d9abf82 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 { @@ -28,7 +29,7 @@ import {OnboardingStackParams} from '../../routes/onboarding'; import {fcmService} from '../../services'; import {RootState} from '../../store/rootReducer'; import {BackgroundGradientType, UniversityType} from '../../types'; -import {normalize, userLogin} from '../../utils'; +import {connectChatAccount, normalize, userLogin} from '../../utils'; import UpdateRequired from './UpdateRequired'; type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>; @@ -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({ @@ -160,7 +162,6 @@ 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) { @@ -168,6 +169,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { try { userLogin(dispatch, {userId: data.UserID, username}); fcmService.sendFcmTokenToServer(); + connectChatAccount(data.UserID, chatClient); } catch (err) { Alert.alert(ERROR_INVALID_LOGIN); } diff --git a/src/services/ChatService.ts b/src/services/ChatService.ts new file mode 100644 index 00000000..e9b1c284 --- /dev/null +++ b/src/services/ChatService.ts @@ -0,0 +1,22 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import {CHAT_TOKEN_ENDPOINT} from '../constants/api'; + +export const loadChatTokenService = async () => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch(CHAT_TOKEN_ENDPOINT, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const status = response.status; + if (status === 200) { + const data = await response.json(); + return data.chatToken; + } + return ''; + } catch (error) { + console.log('Error loading chat token in service'); + } +}; diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index 3ebd4190..c7d0d5a7 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -233,3 +233,4 @@ export const suggestedPeopleAnimatedTutorialFinished = ( console.log('Error while updating suggested people linked state: ', error); } }; + diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index 02331eb6..7fd3ac5a 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -117,6 +117,7 @@ export const EMPTY_SCREEN_TO_USERS_LIST: Record< [ScreenType.Search]: EMPTY_USERX_LIST, [ScreenType.Notifications]: EMPTY_USERX_LIST, [ScreenType.SuggestedPeople]: EMPTY_USERX_LIST, + [ScreenType.Chat]: EMPTY_USERX_LIST, }; export const INITIAL_CATEGORIES_STATE = { diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts index 9ff9ba01..a8789c1d 100644 --- a/src/store/reducers/userReducer.ts +++ b/src/store/reducers/userReducer.ts @@ -90,5 +90,6 @@ export const { setReplyPosted, setSuggestedPeopleImage, clearHeaderAndProfileImages, + // setChatClientReady, } = userDataSlice.actions; export const userDataReducer = userDataSlice.reducer; diff --git a/src/utils/common.ts b/src/utils/common.ts index 0900a084..7ae36dc6 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -173,21 +173,3 @@ 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/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/messages.ts b/src/utils/messages.ts index d63f2b7a..dd29f317 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -1,6 +1,9 @@ +import AsyncStorage from '@react-native-community/async-storage'; import moment from 'moment'; +import {loadChatTokenService} from '../services/ChatService'; import {RootState} from '../store/rootReducer'; import {ChannelGroupedType} from '../types'; +import {StreamChat} from 'stream-chat'; /** * Finds the difference in time in minutes @@ -81,3 +84,59 @@ export const getMember = ( : []; return otherMembers.length === 1 ? otherMembers[0] : undefined; }; + +export const connectChatAccount = async ( + loggedInUserId: string, + chatClient: StreamChat, +) => { + try { + await getChatToken(); + const chatToken = await AsyncStorage.getItem('chatToken'); + if (!chatClient.user && chatToken) { + await chatClient.connectUser( + { + id: loggedInUserId, + }, + chatToken, + ); + return true; + } else if (chatClient.user) { + return true; + } else { + console.log('Unable to connect to stream. Empty chat token'); + return false; + } + } catch (err) { + console.log('Error while connecting user to Stream: ', err); + return false; + } +}; + +export const getChatToken = async () => { + try { + const currentChatToken = await AsyncStorage.getItem('chatToken'); + if (currentChatToken === null) { + const chatToken = await loadChatTokenService(); + await AsyncStorage.setItem('chatToken', chatToken); + } + } catch (err) { + console.log(err); + } +}; + +export const createChannel = async ( + loggedInUser: string, + id: string, + chatClient: any, +) => { + 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/users.ts b/src/utils/users.ts index 22c1c1f0..7148eb79 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -12,14 +12,12 @@ 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, |