UPS11-Accept/Decline Connection Requests (pull request #7)

This commit is contained in:
Omar Mihilmy 2019-02-23 21:38:40 +00:00
parent 54d9a76131
commit 5263e032b4
11 changed files with 377 additions and 77 deletions

1
.gitignore vendored
View File

@ -62,3 +62,4 @@ storybook
test.js
ios/Tagfer.xcodeproj/project.pbxproj
package-lock.json
src/screens/contacts/data.js

View File

@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>2</string>
<string>3</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>

View File

@ -8,6 +8,7 @@ const contactsTabBarOptions = {
upperCaseLabel: false,
activeTintColor: 'white',
inactiveTintColor: '#53ACF0',
lazy: true,
labelStyle: {
margin: 1,
fontSize: 13,

View File

@ -1,11 +1,9 @@
import React from 'react';
import { Avatar } from 'react-native-elements';
import { isEmpty } from '../../utils/aux';
const TagferAvatar = ({ size, photoURL, style, color }) => {
const uri = !isEmpty(photoURL) ? { uri: photoURL } : null;
const icon = isEmpty(photoURL) ? { name: 'user', type: 'entypo' } : null;
const uri = photoURL ? { uri: photoURL } : null;
const icon = !photoURL ? { name: 'user', type: 'entypo' } : null;
return (
<Avatar

View File

@ -13,7 +13,7 @@ const UserListItem = ({ profile, selected, onPress }) => {
return (
<ListItem
roundAvatar
avatar={<TagferAvatar size='medium' photoURL={photoURL} />}
avatar={<TagferAvatar size='medium' photoURL={photoURL} color={colors.lightGrey} />}
title={fullName}
subtitle={subtitle}
subtitleNumberOfLines={3}
@ -24,5 +24,4 @@ const UserListItem = ({ profile, selected, onPress }) => {
/>);
};
export default UserListItem
;
export default UserListItem;

View File

@ -1,4 +1,4 @@
const baseurl = 'https://us-central1-tagfer-inc.cloudfunctions.net/api';
const baseurl = 'http://localhost:3000';
const endpoints = {
login: {
method: 'POST',
@ -39,6 +39,22 @@ const endpoints = {
signup: {
method: 'PUT',
url: `${baseurl}/auth/signup`
},
getConnectionRequests: {
method: 'GET',
url: `${baseurl}/connections/me/requests`
},
sendConnectionRequest: (profileN) => ({
method: 'PUT',
url: `${baseurl}/connections/me/${profileN}`
}),
acceptConnectionRequest: (profileN) => ({
method: 'POST',
url: `${baseurl}/connections/me/${profileN}`
}),
removeConnectionRequest: {
method: 'DELETE',
url: `${baseurl}/connections/me`
}
};

View File

@ -1,49 +0,0 @@
import { createStackNavigator, createAppContainer } from 'react-navigation';
import WelcomeScreen from '../screens/onboaring/WelcomeScreen';
// AUTH IMPORTS
import LoginScreen from '../screens/auth/LoginScreen';
import SignupScreenOne from '../screens/auth/SignupScreen1';
import SignupScreenTwo from '../screens/auth/SignupScreen2';
import SignupScreenThreeA from '../screens/auth/SignupScreen3a';
import SignupScreenThreeB from '../screens/auth/SignupScreen3b';
import SignupScreenFour from '../screens/auth/SignupScreen4';
import SignupScreenFive from '../screens/auth/SignupScreen5';
import SignupScreenSix from '../screens/auth/SignupScreen6';
import TwitterWebView from '../screens/auth/TwitterWebView';
import LinkedInWebView from '../screens/auth/LinkedInWebView';
import ForgotPasswordScreen from '../screens/auth/ForgotPasswordScreen';
// PROFILE IMPORTS
import ProfileQRCodeScanner from '../screens/profile/ProfileQRCodeScanner';
import ProfileSearchScreen from '../screens/profile/ProfileSearchScreen';
const MainNavigator = createStackNavigator({
welcome: WelcomeScreen,
login: LoginScreen,
forgotPassword: ForgotPasswordScreen,
signup1: SignupScreenOne,
signup2: SignupScreenTwo,
signup3a: SignupScreenThreeA,
signup3b: SignupScreenThreeB,
signup4: SignupScreenFour,
signup5: SignupScreenFive,
signup6: SignupScreenSix,
profileQRScan: ProfileQRCodeScanner,
profileSearch: ProfileSearchScreen
});
const RootNavigator = createStackNavigator(
{
Main: {
screen: MainNavigator,
navigationOptions: { header: null }
},
twitterWebView: TwitterWebView,
linkedInWebView: LinkedInWebView
},
{
mode: 'modal'
});
const AppNavigator = createAppContainer(RootNavigator);
export default AppNavigator;

View File

@ -6,6 +6,7 @@ import { UserSchema, ProfileSchema, InvitesSchema, ContactsSchema, SignupConfig,
var authRealm;
var signupRealm;
var sessionId;
// Loads the signup state; a `then` should be chained at the component level to save the state
export async function loadSignupState(screen) {
@ -37,9 +38,15 @@ export async function saveAuthState(sessionId) {
}
export async function getAuthState() {
if (!!sessionId) {
return sessionId;
}
try {
const realm = await getRealm(0);
return realm.objectForPrimaryKey(SessionSchema.name, primaryKey);
const auth = realm.objectForPrimaryKey(SessionSchema.name, primaryKey);
sessionId = !!auth ? auth.sessionId : undefined;
return sessionId;
} catch (error) {
console.log(error);
}

View File

@ -1,21 +1,265 @@
import React from 'react';
import { Text, View } from 'react-native';
import { View, Text, TouchableOpacity, FlatList } from 'react-native';
import { ListItem, Icon } from 'react-native-elements';
import { fetchTagferApi } from '../../utils/fetch';
import TagferAvatar from '../../components/new/TagferAvatar';
import colors from '../../config/colors.json';
import endpoints from '../../config/apiEndpoints';
import { showFlashErrorMessage } from '../../utils/errorHandler';
export default class RequestsScreen extends React.Component {
static navigationOptions = ({ navigation }) => {
const title = navigation.getParam('title', 'Unconfirmed');
return { title };
}
constructor(props) {
super(props);
this.state = {
filter: 'rcvd',
currRequests: [],
loading: false
};
this.rcvdRequests = [];
this.sentRequests = [];
this.onRcvdPress = this.onRcvdPress.bind(this);
this.onSentPress = this.onSentPress.bind(this);
}
componentDidMount() {
this.onLoad();
}
onRcvdPress() {
this.setState({ filter: 'rcvd', currRequests: this.rcvdRequests });
}
onSentPress() {
this.setState({ filter: 'sent', currRequests: this.sentRequests });
}
onAccept(index) {
const fromTagferId = this.rcvdRequests[index].tagferId;
const fromProfileN = this.rcvdRequests[index].profileN;
const response = fetchTagferApi(endpoints.acceptConnectionRequest(1), { fromTagferId, fromProfileN });
if (response.error) {
return showFlashErrorMessage(response.error);
}
this.rcvdRequests.splice(index, 1);
this.setState({ currRequests: this.rcvdRequests });
this.updateTitle();
}
async onReject(index) {
const { filter } = this.state;
const fromTagferId = filter === 'rcvd' ? this.rcvdRequests[index].tagferId : undefined;
const toTagferId = filter === 'sent' ? this.sentRequests[index].tagferId : undefined;
const response = await fetchTagferApi(endpoints.removeConnectionRequest, { fromTagferId, toTagferId });
if (response.error) {
return showFlashErrorMessage(response.error);
}
if (this.state.filter === 'rcvd') {
this.rcvdRequests.splice(index, 1);
this.setState({ currRequests: this.rcvdRequests });
} else {
this.sentRequests.splice(index, 1);
this.setState({ currRequests: this.sentRequests });
}
this.updateTitle();
}
async onLoad() {
this.setState({ loading: true });
const response = await fetchTagferApi(endpoints.getConnectionRequests);
if (response.error) {
this.setState({ loading: false });
return showFlashErrorMessage(response.error);
}
this.rcvdRequests = response.received;
this.sentRequests = response.sent;
this.updateTitle();
const currRequests = this.state.filter === 'rcvd' ? this.rcvdRequests : this.sentRequests;
this.setState({ currRequests, loading: false });
}
updateTitle() {
let numOfRequests = this.rcvdRequests.length + this.sentRequests.length;
if (numOfRequests > 100) { numOfRequests = '99+'; }
this.props.navigation.setParams({ title: `Unconfirmed(${numOfRequests})` });
}
// Render a single entry in the list
renderItem(profile, index) {
return (
<RequestListItem
type={this.state.filter}
profile={profile}
onAccept={() => this.onAccept(index)}
onReject={() => this.onReject(index)}
/>
);
}
export default class UnconfirmedConnetcionsScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Unconfirmed Connections</Text>
<FilterButtons
filter={this.state.filter}
rcvdCount={this.rcvdRequests.length}
sentCount={this.sentRequests.length}
onRcvdPress={this.onRcvdPress}
onSentPress={this.onSentPress}
/>
<FlatList
data={this.state.currRequests}
extraData={this.state}
renderItem={({ item, index }) => this.renderItem(item, index)}
keyExtractor={(item) => item.tagferId}
style={{ flex: 1, marginHorizontal: 10, marginTop: 10, borderRadius: 5 }}
refreshing={this.state.loading}
onRefresh={() => this.onLoad()}
/>
</View>
);
}
}
const FilterButtons = ({ filter, rcvdCount = 0, sentCount = 0, onRcvdPress, onSentPress }) => {
let rcvdButtonStyle;
let rcvdButtonText;
let sentButtonStyle;
let sentButtonText;
if (filter === 'sent') {
rcvdButtonStyle = styles.rcvdButtonNotSelected;
sentButtonStyle = styles.sentButtonSelected;
rcvdButtonText = styles.notSelectedText;
sentButtonText = styles.selectedText;
} else {
rcvdButtonStyle = styles.rcvdButtonSelected;
sentButtonStyle = styles.sentButtonNotSelected;
rcvdButtonText = styles.selectedText;
sentButtonText = styles.notSelectedText;
}
return (
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 10 }}>
<TouchableOpacity style={rcvdButtonStyle} onPress={onRcvdPress}>
<Text style={rcvdButtonText}>Requests received ({rcvdCount})</Text>
</TouchableOpacity>
<TouchableOpacity style={sentButtonStyle} onPress={onSentPress}>
<Text style={sentButtonText}>Requests sent ({sentCount})</Text>
</TouchableOpacity>
</View>
);
};
const RequestListItem = ({ type, profile, onAccept, onReject }) => {
const { tagferId, fullName, jobTitle, companyName, photoURL } = profile;
const subtitle = `@${tagferId}\n${jobTitle} at ${companyName}`;
return (
<ListItem
roundAvatar
avatar={<TagferAvatar size='medium' photoURL={photoURL} color={colors.lightGrey} />}
title={fullName}
subtitle={subtitle}
subtitleNumberOfLines={3}
subtitleStyle={{ fontSize: 12, fontWeight: 'normal' }}
titleStyle={{ fontSize: 16, color: 'black' }}
rightIcon={<ActionButtons type={type} onAccept={onAccept} onReject={onReject} />}
containerStyle={{ backgroundColor: 'white' }}
/>
);
};
const ActionButtons = ({ type, onAccept, onReject }) => {
const sentActionButtons = (
<Icon
name='ios-close-circle-outline'
type='ionicon' size={45}
color='#7389AE'
onPress={onReject}
/>
);
const rcvdActionButtons = (
<View style={{ flexDirection: 'row' }}>
<Icon
name='ios-close-circle-outline'
type='ionicon' size={45}
color='#7389AE'
containerStyle={{ marginRight: 10 }}
onPress={onReject}
/>
<Icon
name='ios-checkmark-circle-outline'
type='ionicon' size={45}
color={colors.addGreen}
onPress={onAccept}
/>
</View>
);
return type === 'sent' ? sentActionButtons : rcvdActionButtons;
};
const styles = {
container: {
flex: 1,
backgroundColor: colors.offWhite
},
rcvdButtonSelected: {
marginLeft: 40,
height: 30,
paddingHorizontal: 15,
justifyContent: 'center',
alignItems: 'center'
borderRadius: 15,
backgroundColor: '#AAAFC4'
},
rcvdButtonNotSelected: {
marginLeft: 40,
height: 30,
paddingHorizontal: 15,
justifyContent: 'center',
borderRadius: 15
},
sentButtonSelected: {
marginRight: 40,
height: 30,
paddingHorizontal: 15,
justifyContent: 'center',
borderRadius: 15,
backgroundColor: '#AAAFC4'
},
sentButtonNotSelected: {
marginRight: 40,
height: 30,
paddingHorizontal: 15,
justifyContent: 'center',
borderRadius: 15
},
selectedText: {
fontSize: 13,
fontWeight: 'bold',
color: 'white'
},
notSelectedText: {
fontSize: 14,
fontWeight: 'normal',
color: '#6A6A6A'
}
};

View File

@ -1,21 +1,85 @@
import React from 'react';
import { Text, View } from 'react-native';
import { View, FlatList } from 'react-native';
import { fetchAuth, fetchTagferApi } from '../../utils/fetch';
import { showFlashErrorMessage } from '../../utils/errorHandler';
import colors from '../../config/colors.json';
import endpoints from '../../config/apiEndpoints';
import UserListItem from '../../components/new/UserListItem';
export default class SuggestedContactsScreen extends React.Component {
static navigationOptions = ({ navigation }) => {
const title = navigation.getParam('title', 'Suggested');
return { title };
}
constructor(props) {
super(props);
this.state = {
users: [],
loading: true
};
}
componentDidMount() {
this.onLoad();
}
// Load suggested contacts
async onLoad() {
const resp = await fetchAuth(endpoints.suggestProfiles);
this.setState({ loading: false, users: resp.profiles || [] });
if (resp.error) {
return showFlashErrorMessage(resp.error);
}
this.updateTitle();
}
// WARN: this might break as we are mutating state
async onConnect(index) {
const response = await fetchTagferApi(endpoints.sendConnectionRequest(1), { toTagferId: this.state.users[index].tagferId });
if (response.error) {
return showFlashErrorMessage(response.error);
}
this.state.users.splice(index, 1);
this.updateTitle();
if (this.state.users.length === 0) {
this.onLoad();
}
}
// Update count in title
updateTitle() {
let count = this.state.users.length;
if (count > 100) { count = '99+'; }
this.props.navigation.setParams({ title: `Suggested(${count})` });
}
// Render a single entry in the list
renderItem(profile, index) {
return <UserListItem profile={profile} onPress={() => this.onConnect(index)} />;
}
export default class SuggestedConnectionsScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Suggested Connections</Text>
<View style={{ flex: 1, backgroundColor: colors.offWhite }}>
{/* SUGGESTED CONTACTS */}
<FlatList
data={this.state.users}
extraData={this.state}
renderItem={({ item, index }) => this.renderItem(item, index)}
keyExtractor={(item) => item.tagferId}
style={{ flex: 1, marginHorizontal: 10, marginTop: 10, borderRadius: 2 }}
refreshing={this.state.loading}
onRefresh={() => this.onLoad()}
/>
</View>
);
}
}
const styles = {
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
};

View File

@ -1,6 +1,8 @@
import appConfig from '../config/appConfig.json';
import errors from '../config/errors.js';
import { getAuthState } from '../realm/actions/AuthActions';
export function fetchAuth(endpoint, body) {
const request = {
method: endpoint.method,
@ -13,3 +15,20 @@ export function fetchAuth(endpoint, body) {
return fetch(endpoint.url, request).then(res => res.json()).catch(() => ({ error: errors.APP_NETWORK_ERROR }));
}
// Fetch wrapper for authorized calls with valid session id. Returns either a JSON object documented in the wiki or an error.
export async function fetchTagferApi(endpoint, body) {
const sessionId = await getAuthState();
const request = {
method: endpoint.method,
headers: {
Authorization: sessionId,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
};
return fetch(endpoint.url, request).then(res => res.json()).catch(() => ({ error: errors.APP_NETWORK_ERROR }));
}