I am using react-native-gifted-chat for my UI and for the backend, I am using AWSAppSync/GraphQL to store user data. I am not so sure how to go about displaying username of sender in my ChatApp and would need this feature for group chats. So far, this is my code.
import React from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
class ChatScreen extends React.Component {
state = {
messages: [],
username: ''
}
componentWillMount() {
this.setState({
messages: [
{
_id: 1,
text: 'Hello developer',
createdAt: new Date(),
user: {
_id: 0,
name: 'Default',
avatar: 'https://placeimg.com/140/140/any',
},
},
],
})
}
onSend(messages = []) {
this.setState(previousState => ({
messages: GiftedChat.append(previousState.messages, messages),
}))
}
render() {
return (
<GiftedChat
messages={this.state.messages}
renderUsernameOnMessage={true}
onSend={messages => this.onSend(messages)}
user={this.username}
/>
)
}
}
export default ChatScreen;
Did you try this?
<GiftedChat
messages={this.state.messages}
renderUsernameOnMessage={true}
onSend={messages => this.onSend(messages)}
user={this.state.username}
/>
In react native gifted chat user must be object containing the keys _id, name and avatar. _id will help it to separate sender and receiver. So yes, you have to set user with these properties. It not allowe only username.
So simple way to do this is that first fetch current user info from the server and store it into redux of asyncStorage. For example I am fetching current user from firebase as below :
export const getAccountInfo = () => {
return async dispatch => {
const cu = auth().currentUser;
const user = await firestore().collection('users').doc(cu.uid).get();
dispatch({
type: Types.INFO_FETCHED,
data: user.data()
});
};
};
Now, In my chat screen I set user as below :
<GiftedChat
...
...
user={{
_id: this.props.account?.user?.uid,
name: this.props.account?.data?.fullName,
avatar: this.props.account?.data?.avatar
}}
/>
Related
The bellow code runs somewhat to what I want. I'm able to get the correct data when i enter the component, but as soon as I send a message, it duplicates the previous messages. I'm quite new to React Native so just learning. Any help or suggestions would be appreciated!
1.) Fetch documents where 'selectedUser (local) == field 'to' in Firebase.
2.) Display chats from the found documents.
3.) Send message to screen and firebase.
4.) Profit.
useEffect(() => {
const q = query(collectionRef, orderBy('createdAt', 'desc'));
const unsubscribe = onSnapshot(q, querySnapshot => {
const id = querySnapshot.docs.map(doc => ({
_id: doc.data()._id,
createdAt: doc.data().createdAt.toDate(),
text: doc.data().text,
user: doc.data().user,
to: doc.data().to
}))
id.forEach((val)=>{
if(val.to == selectedUser[0]){
console.log(val.to)
DATA.push({
_id: val._id,
createdAt: val.createdAt,
text: val.text,
user: val.user,
to: val.to,
}
);
}
})
// console.log(DATA)
setMessages(DATA);
// console.log(Message)
});
return () => unsubscribe();
}, []);
const onSend = useCallback((messages = []) => {
const to = selectedUser.toString();
const { _id, createdAt, text, user } = messages[0]
addDoc(collection(db, 'Chats'), { _id, createdAt, text, user, to });
}, []);
return (
<SafeAreaView style={styles.container}>
<Text style={styles.ChattingWith}>Chatting with: {selectedUser[[0]]}</Text>
<View style={styles.MainFeed}>
<GiftedChat
messages={messages}
showAvatarForEveryMessage={true}
onSend={messages => onSend(messages)}
user={{
_id: auth?.currentUser?.email,
avatar: auth?.currentUser?.photoURL
}}
/>
</View>
</SafeAreaView>
)
}
What Firestore has when 'onSend' function is called:
can you try replacing
id.forEach((val)=>{
if(val.to == selectedUser[0]){
console.log(val.to)
DATA.push({
_id: val._id,
createdAt: val.createdAt,
text: val.text,
user: val.user,
to: val.to,
}
);
}
})
setMessages(DATA);
with
const newData = id.filter((val)=> val.to == selectedUser[0])
setMessages(newData);
I am trying to create a chat feature in my react native app. I am using react-native-gifted-chat and saving the messages in firestore. Here is the behavior that is occurring:
When I send a message, ALL the messages re render, some of them are duplicates, as you can see I only have 3 messages sent so far, but all these duplicates are making me wonder why the entire thing is re-rendering and why there are duplicates when it does re-render.
The code:
class Chat extends React.Component {
constructor(props) {
super(props)
this.state = {
messages: [],
currentUser: null,
isLoading: true,
messageID: ""
}
}
//---------------------------------------------------------------
async componentDidMount (){
// get user info from firestore
let userUID = Firebase.auth().currentUser.uid
await Firebase.firestore().collection("users").doc(userUID).get()
.then(doc => {
data = doc.data()
this.setState({
currentUser: {
name: data.username,
avatar: data.profilePic,
_id: doc.id,
},
})
})
const messages = []
await Firebase.firestore().collection("chat")
.orderBy("createdAt", "desc")
.limit(50)
.onSnapshot(querySnapshot => {
querySnapshot.forEach((res) => {
const {
user,
text,
createdAt,
} = res.data();
messages.push({
key: res._id,
user,
text,
createdAt,
});
})
this.setState({
messages,
isLoading: false,
});
})
}
//Load 50 more messages when the user scrolls
//
//Add a message to firestore
onSend = async(message) => {
await Firebase.firestore().collection("chat")
.add({
user: {
_id: this.state.currentUser._id,
name: this.state.currentUser.name,
avatar: this.state.currentUser.avatar,
},
})
.then(ref => this.setState({messageID: ref.id}))
await Firebase.firestore().collection("chat")
.doc(this.state.messageID)
.set({
_id: this.state.messageID,
text: message[0].text,
createdAt: message[0].createdAt
}, { merge: true })
}
render() {
if(this.state.isLoading){
return(
<View style = {{backgroundColor: '#000000', flex: 1}}>
<ActivityIndicator size="large" color="#9E9E9E"/>
</View>
)
}
return (
<View style={{backgroundColor: '#000000', flex: 1}}>
<GiftedChat
showUserAvatar={true}
renderUsernameOnMessage={true}
messages={this.state.messages}
onSend={message => this.onSend(message)}
scrollToBottom
/>
</View>
)
}
}
Some notes:
Every time the component mounts, the messages array pushes the messages to the state array.
The component mounts when I send a message, thus re-rendering the array of messages
Each message ID is unique and generated by firebase using "Add"
Let me know how I can fix this issue! thanks
Duplication is because of just single line
const messages = []
Move this line inside listener, i.e.onSnapShot()
await Firebase.firestore().collection("chat")
.orderBy("createdAt", "desc")
.limit(50)
.onSnapshot(querySnapshot => {
const messages = []
// rest of your code which is having forEach loop
});
The issue was that messages object was created only once when the component loaded, and you were pushing elements to that object only.
I understand that this.props.isValidUser gets updated after action dispatches the axios promise. if the user is not valid is shows message. If the user is valid user, I want to navigate to another screen to enter pin. How do I navigate to another screen after I get axios result from action?
types.js
export const VALIDATE_USER = "VALIDATE_USER";
export const VALIDATE_PIN = "VALIDATE_PIN";
export const GET_ERRORS = "GET_ERRORS";
Reducer.js
import { VALIDATE_USER, VALIDATE_PIN, GET_ERRORS } from "../actions/types.js";
export default function (state = initialState, action) {
switch (action.type) {
case VALIDATE_USER:
return {
...state,
isValidUser: (action.payload == true) ? true : false,
Id: action.employeeId
};
case VALIDATE_PIN:
return {
...state,
isValidPin: action.payload,
action: "VALIDATE_PIN",
};
default:
return state;
}
}
action.js
import { GET_ERRORS, VALIDATE_USER, VALIDATE_PIN, } from "./types";
export const validateUser = (empId) => dispatch => {
axios.get(`${API}/api/Account/ValidateMobileAppUser?employeeId=${empId}`)
.then(res => {
dispatch({
type: VALIDATE_USER,
payload: res.data,
Id: empId,
});
})
.catch(err => {
dispatch({
type: VALIDATE_USER,
payload: false,
Id: empId
});
});
};
Login.js
import PropTypes from "prop-types";
import { validateUser } from "../actions/authActions";
class Login extends PureComponent {
constructor() {
super();
this.state = {
employeeId: "",
pin: "",
isValidUser: false,
};
this.onValidateUser = this.onValidateUser.bind(this);
this.onEmployeeId = this.onEmployeeId.bind(this);
}
onEmployeeId(employeeId) {
this.setState({ employeeId });
}
onValidateUser() {
this.props.validateUser(this.state.employeeId);
}
render() {
const { loading } = this.props.loading;
return (
<KeyboardAvoidingView style={styles.login} >
<ScrollView showsVerticalScrollIndicator={false}>
<Block padding={[10, theme.sizes.base * 2]} onPress={Keyboard.dismiss}>
<Block middle>
<Input
placeholder={this.state.placeholder}
keyboardType={this.state.keyboardType}
style={[styles.input]}
value={this.state.employeeId}
onChangeText={this.onEmployeeId}
/>
{(this.props.isValidUser == false) ? (
<Text center style={{ color: "#C00000", marginTop: 15, fontSize: 14 }}>
Employee Id not registered. Please contact HR.
</Text>
) : ""}
<Button
gradient
style={styles.loginButton}
onPress={this.onValidateUser}
>
<Text white center>
Login
</Text>
</Button>
</Block>
<Button
onPress={() => this.onGoToStep(1)}
style={{
borderWidth: 1,
borderRadius: 30,
borderColor: "#E46932"
}}
>
<Text gray caption center style={{ color: "#E46932" }}>
Don't have an account? Sign Up
</Text>
</Button>
</Block>
</ScrollView>
</KeyboardAvoidingView>
);
}
}
Login.propTypes = {
validateUser: PropTypes.func.isRequired,
errors: PropTypes.object.isRequired
};
function reducerCallback(state, ownProps) {
if (state.auth.isValidUser == true) {
ownProps.navigation.navigate("mPin", { Id: state.auth.employeeId, type: "LOGIN" });
}
}
const mapStateToProps = (state, ownProps) => ({
auth: reducerCallback(state, ownProps),
isValidUser: state.auth.isValidUser,
errors: state.errors
});
export default connect(
mapStateToProps,
{
validateUser,
}
)(Login);
this.props.isValidUser == false tells me if the user is valid or not. But if the user is valid I'm navigating to another screen using reducerCallback() function. I'm not aware if this is the correct way to do so. My question is how to I navigate to another screen after I get return result from async axios action and How to I set local state using setState when I get callback from axios dispatch. Please guide
Try to below code:
login.js:
onValidateUser() {
this.props.validateUser({
empId: this.state.employeeId,
onSuccess: () => {
//Navigate to other screen
},
onFailure: () => {
//Alert error message
},
});
}
Action.js:
export const validateUser = ({empId, onSuccess, onFailure}) => dispatch => {
axios
.get(
`${API}/api/Account/ValidateMobileAppUser?employeeId=${empId}`
)
.then(res => {
dispatch({
type: VALIDATE_USER,
payload: res.data,
Id: empId,
});
onSuccess();
})
.catch(err => {
dispatch({
type: VALIDATE_USER,
payload: false,
Id: empId
});
onFailure()
});
};
On iOS this has never been an issue, but a lot of my users are attempting to create a firebase user, then I write that newly created user's info in the realtime database. It's hit or miss, some users it works successfully, sometimes it takes more than one try. Let me add that I have only been on this project for a short time and I can already tell best practices are not being used. The Following is the code:
Using crashlytics, I am seeing the folllwing error:
Fatal Exception: com.facebook.react.common.JavascriptException
null is not an object (evaluating 't.navigator.dispatch'), stack: #364:2006 value#49:1280 #605:1154 value#49:1280 #590:497 value#49:1280 value#28:3311 #28:822 value#28:2565 value#28:794 value#-1
screens/login.js
import React, { Component } from 'react';
import { ... } from 'react-native';
import { connect } from 'react-redux';
import { authActions, ... } from '../redux/actions';
import firebase from 'react-native-firebase';
class Login extends Component {
static navigationOptions = () => ({
headerMode: 'none',
header: null,
});
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
forceCheckEmail: false,
forceCheckPassword: false,
};
}
componentWillReceiveProps(newProps) {
const { props } = this;
const {
error,
isBusy,
dismissError,
screenProps: {
modal: {
setContent,
clearContent,
setDismissHandler,
},
},
} = newProps;
if (props.error !== error || props.isBusy !== isBusy) {
const modalContent =
isBusy ? <Spinner text='One moment...' /> :
error ? <ErrorPopup message={error} /> :
null;
if (modalContent) {
setContent(modalContent, undefined, this.ref);
setDismissHandler(() => {
this.setState({ showForgotBlock: true })
dismissError();
});
} else {
clearContent();
}
}
}
handleLogin() {
Keyboard.dismiss();
this.props.login({
email: this.state.email,
password: this.state.password,
});
}
render() {
const {
keyboardIsVisible,
email,
password,
forceCheckEmail,
forceCheckPassword,
showForgotBlock,
} = this.state;
const {
...
navigation: {
navigate
}
} = this.props;
const emailValid = validateEmail(email);
const passwordValid = password.length > 5;
const loginEnabled = email !== '' && emailValid && passwordValid;
const forgotPasswordBlock = showForgotBlock ? (
<TouchableOpacity
onPress={() => restorePassword(email)}
style={{marginTop: -20, marginBottom: 10}}
>
<Text style={{color: '#777'}}>
Forgot your password?
</Text>
</TouchableOpacity>
): null;
firebase.analytics().setCurrentScreen('login', 'login');
return (
...
<TextInput
style={[styles.input, forceCheckEmail && !emailValid ? styles.failedInput : null]}
autoCorrect={false}
placeholder="Email"
onBlur={() => this.setState({ forceCheckEmail: true })}
autoCapitalize="none"
keyboardType="email-address"
placeholderTextColor={color.INPUT_TEXT}
onChangeText={email => this.setState({ email })}
value={email}
/>
<TextInput
style={[styles.input, forceCheckPassword && !passwordValid ? styles.failedInput : null]}
autoCorrect={false}
placeholder="Password"
onBlur={() => this.setState({ forceCheckPassword: true })}
placeholderTextColor={color.INPUT_TEXT}
secureTextEntry
onChangeText={password => this.setState({ password })}
value={password}
/>
...
<TouchableOpacity
style={[styles.button, styles.buttonPrimary]}
onPress={() => navigate('SignUp')}
>
<Text style={styles.buttonPrimaryText}>
SIGN UP
</Text>
</TouchableOpacity>
...
export default connect(
state => ({
...
}),
{
login: data => authActions.login(data),
...
},
)(Login);
actions/auth.js
import { createActions } from 'redux-feline-actions';// I question this dependency
import firebase from 'react-native-firebase';
import FBSDK from 'react-native-fbsdk';
const usersDB = firebase.database().ref('users');
const newUserData = {
point: 0,
savedNumbers: [],
};
export default createActions({
...
register: ({ name, email, phone, password }) => ({
useReducer: 'auth',
payload: firebase.auth()
.createUserWithEmailAndPassword(email, password)
.then(({user: { uid, email }}) => usersDB
.child(uid)
.set({
...newUserData,
name,
email,
phone,
id: uid,
})
.then(err => err || ({
...newUserData,
name,
email,
phone,
id: uid,
}))),
}),
...
stores/auth.js
import Immutable, { Map } from 'immutable';
import createAsyncStores from 'cat-stores'; // I also question this one
export default createAsyncStores({
auth: {
begin: state => state
.set('isBusy', true),
complete: (state, { payload }) => state
.set('isBusy', false)
.set('user', Immutable.fromJS(payload)),
error: {
default: (state, { payload }) => state
.set('error', payload.message)
.set('isBusy', false)
.set('user', null), // Android users keep getting this result I believe
},
},
...
},
Map({
isBusy: false,
error: null,
user: null,
redirectTo: null,
theme: Map(),
settings: Map(),
themeIsLoaded: false,
settingsAreLoaded: false,
}));
I expect the user to not have an issue with creating and saving new user info on Android, just like on iOS.
I am a newbie in react native, I am creating one react native application with redux. I am only on the login page. Currently, I implemented redux successfully and getting response from API when login credentials are right and getting error if credentials are wrong.
Basically, I have 2 questions in which I am stuck.
How to navigate to new screen when login is successfully?
Where to store auth token globally so I can identify user is logged in or not on any page?
Below are the response i get when login is successfully
Below are the code for my files.
screens/Login.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { LoginComponent } from '../components/LoginComponent';
// import { Button } from 'react-native-material-design';
import { View, Text, TextInput, StatusBar, KeyboardAvoidingView } from 'react-native';
import { Button } from 'react-native-elements';
import PropTypes from 'prop-types';
import { connectAlert } from '../components/Alert';
import { login, handleEmailChange, handlePasswordChange } from '../actions/user';
class Login extends Component {
static propTypes = {
navigation: PropTypes.object,
// dispatch: PropTypes.func,
user: PropTypes.object,
email: PropTypes.string,
password: PropTypes.string,
alertWithType: PropTypes.func,
loginError: PropTypes.string
}
componentWillReceiveProps(nextProps) {
// if (nextProps.loginError && nextProps.loginError !== this.props.loginError) {
if (nextProps.loginError ) {
this.props.alertWithType("error", "Error occurred in login", nextProps.loginError)
}
}
handleSubmit = () => {
this.props.login(this.props.email, this.props.password)
}
handleChange = (type, e) => {
if (type === "email"){
this.props.dispatch(handleEmailChange(e))
}
else{
this.props.dispatch(handlePasswordChange(e))
}
}
render(){
return(
<View style={{ flex: 1, justifyContent: 'center' }}>
<StatusBar barStyle="default" translucent={false} />
<KeyboardAvoidingView behavior='padding'>
<Text style={{textAlign: 'center', fontWeight: '800'}}> AfterClix Login </Text>
<TextInput
autoCapitalize="none"
onSubmitEditing={() => this.passwordInput.focus()}
autoCorrect={false}
keyboardType='email-address'
returnKeyType="next"
placeholder='Email'
// onChangeText={event => this.handleChange("email",event)}
onChangeText={this.props.changeEmailValue}
placeholderTextColor='rgb(65, 146, 244)'/>
<TextInput
returnKeyType="go"
ref={(input)=> this.passwordInput = input}
placeholder='Password'
placeholderTextColor='rgb(65, 146, 244)'
onChangeText={this.props.changePasswordValue}
// onChangeText={event => this.handleChange("password",event)}
secureTextEntry/>
<Button raised title="Login" backgroundColor="rgb(65, 146, 244)" color="#FFFFFF" onPress={this.handleSubmit}/>
</KeyboardAvoidingView>
</View>
)
}
}
const mapStateToProps = (state) => {
return {
// user: state.authentication.user
email: state.user.email,
password: state.user.password,
loginError: state.user.error
}
}
const mapDispatchToProps = dispatch => {
return {
changeEmailValue: (text) => dispatch({type: 'CHANGE_EMAIL_VALUE', text}),
changePasswordValue: (text) => dispatch({type: 'CHANGE_PASSWORD_VALUE', text}),
login: (email,password) => dispatch({type: 'LOGIN', email, password}),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(connectAlert(Login));
actions/user.js
export const LOGIN = "LOGIN";
export const AUTHENTICATION_RESULT = "AUTHENTICATION_RESULT";
export const AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR";
export const CHANGE_EMAIL_VALUE = "CHANGE_EMAIL_VALUE";
export const CHANGE_PASSWORD_VALUE = "CHANGE_PASSWORD_VALUE";
export const login = (email, password) => ({
type: LOGIN,
email: email,
password: password
})
export const handleEmailChange = (value) => ({
type: CHANGE_EMAIL_VALUE,
email: value
})
export const handlePasswordChange = (value) => ({
type: CHANGE_PASSWORD_VALUE,
password: value
})
reducers/user.js
import {LOGIN, AUTHENTICATION_RESULT, AUTHENTICATION_ERROR, CHANGE_EMAIL_VALUE, CHANGE_PASSWORD_VALUE} from '../actions/user'
const initialState = {
// user: {
// email: '',
// password: '',
// error: null,
// }
email: '',
password: '',
error: null,
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case LOGIN:
return {
...state,
email: state.email,
password: state.password
}
case CHANGE_EMAIL_VALUE:
// return {...state, email: action.email };
return {...state, email: action.text };
case CHANGE_PASSWORD_VALUE:
return {...state, password: action.text };
case AUTHENTICATION_RESULT:
console.log("Result", action.result.data)
return {...state, email: action.result.data.user.email, password: action.result.data.user.password };
// return {
// ...state,
// user: {
// ...state,
// [action.result.base]: {
// ...action.result,
// }
// }
// }
case AUTHENTICATION_ERROR:
return {
...state,
error: action.error,
}
default:
return state;
}
}
export default reducer;
config/saga.js
import { takeEvery, select, call, put } from 'redux-saga/effects';
import { LOGIN, AUTHENTICATION_RESULT, AUTHENTICATION_ERROR } from '../actions/user';
const authenticateUser = (email, password) => fetch('apiURL/oauth/token', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
password: password,
grant_type: "password"
}),
});
function* fetchUser(action){
try{
console.log("Email", action.email)
console.log("Password", action.password)
const response = yield call(authenticateUser, action.email, action.password)
const result = yield response.json();
if (result.error) {
yield put({ type: AUTHENTICATION_ERROR, error: result.error });
} else {
yield put({ type: AUTHENTICATION_RESULT, result });
}
}
catch (e) {
yield put({ type: AUTHENTICATION_ERROR, error: e.message });
}
}
export default function* rootSaga() {
yield takeEvery(LOGIN, fetchUser);
}
First of all you need to import a library which provides navigation support.
Ques 1. How to navigate to new screen when login is successfull?
Answer. Choose and integrate one of the libaries given below:
React Navigation
React Native Navigation
React Native Router Flux
Ques 2. Where to store auth token globally so I can identify user is logged in or not on any page?
Answer. You can make use of AsyncStorage to store and access the user info when your app restarts and decide whether to navigate to home or login screen. When store is not cleared (app is not closed fully), then use can use store to access the authenticated user
yield setItem('user', JSON.stringify(result.user)); // save user in asyncStorage - permanent
yield put({ type: AUTHENTICATION_RESULT, result }); // save user in store - temporary
yield put(NavigationActions.navigate({ routeName: 'drawerStack' })); //and then navigate to home
const setItem = async (name, data) => {
try {
await AsyncStorage.setItem(name, JSON.stringify(data));
console.log('data stored');
} catch (error) {
// Error saving data
console.log('AsyncStorage save error: ' + error.message);
}
};
you can store the result in store as well as AsyncStorage and access it any where in the app.
Using your existing logic, you can add a condition on componentWillReceiveProps(). Something on the lines of -
componentWillReceiveProps(nextProps) {
// if (nextProps.loginError && nextProps.loginError !== this.props.loginError) {
if (nextProps.loginError ) {
this.props.alertWithType("error", "Error occurred in login", nextProps.loginError)
}
else if(nextProps.email!=='') {
//this means you have logged in. Your navigation logic will be present here.
}
For navigation, you'll need to integerate navigation libraries for this.props.navigation.navigate(...) to work
EDIT
If you desire to include another variable that defines the user logged in state, then you can use it in your componentwillreceiveprops method like
componentWillReceiveProps(nextProps) {
// if (nextProps.loginError && nextProps.loginError !== this.props.loginError) {
if (nextProps.loginError ) {
this.props.alertWithType("error", "Error occurred in login", nextProps.loginError)
}
else if(nextProps.isAuthenticated) {
//this means you have logged in. Your navigation logic will be present here.
}
Note - you'll have to update isAuthenticated on login success and login failure redux actions for it to behave correctly.