I’m using Firestore real time updates to create realtime chats in my React Native app. I read this may not be the best way to build a chat, but I decided to do so cause I’m using Firebase already and the chat is not the main purpose of the app.
So, in the context of a real time chat, how would I optimize the Firestore connection?
It usually works really well but I have experienced a few problems so far:
message comes in slowly
message doesn’t show after being sent
Push notification arrives before the message
These problems usually occur when internet connection is not great (though Whatsapp messages still work fine), but sometimes also on a good connection…
Here is how I query the data (real-time listener added in componentDidMount, removed in componenWillUnmount):
onLogUpdate = (querySnapshot) => {
allMessages = this.state.allMessages
cb = (allMsgs) => {
allMsgs.sort((a,b) => {
a = new Date(a.timestamp);
b = new Date(b.timestamp);
return a>b ? -1 : a<b ? 1 : 0;
})
messages = _.takeRight(allMsgs, this.state.messageLimit)
this.setState({ loading: false, messages, allMessages: allMsgs })
}
async.map(querySnapshot._changes, (change, done) => {
if (change.type === "added") {
const msgData = change._document.data()
if (msgData.origin == 'user') {
this.usersRef.doc(msgData.byWhom).get()
.then(usr => {
msgData.user = usr.data()
done(null, msgData)
})
.catch(err => { console.log('error getting user in log:', err) })
} else {
done(null, msgData)
}
} else {
done(null, 0)
}
}, (err, results) => {
const res = results.filter(el => { return el != 0 })
allMessages = _.concat(allMessages, res)
cb(allMessages)
})
}
And this is how I add new messages:
// in utils.js
exports.addLogMessage = (msgObj, moment_id, callback) => {
logMessagesRef.add(msgObj)
.then(ref => {
momentsRef.doc(moment_id).get()
.then(doc => {
const logMsgs = doc.data().logMessages ? doc.data().logMessages : []
logMsgs.push(ref.id)
momentsRef.doc(moment_id).update({ logMessages: logMsgs })
})
.then(() => {
if (callback) {
callback()
}
})
})
.catch(err => { console.log('error sending logMessage:', err) })
}
// in chat Screen
sendLogMessage = () => {
if (this.state.newMessage.length > 0) {
firebase.analytics().logEvent('send_log_message')
this.setState({ newMessage: '' })
const msgObj = {
text: this.state.newMessage,
origin: 'user',
timestamp: Date.now(),
byWhom: this.state.user._id,
toWhichMoment: this.state.moment._id
}
addLogMessage(msgObj, this.state.moment._id)
}
}
Any suggestions would be highly appreciated :)
I was working on something similar and after i submitted the data it wouldnt show in the ListView. I solved it by pushing the new entry into the preexisting state that was being mapped, and since setState() calls the render() method, it worked just fine. I did something like this:
sendLogMessage().then(newData => {
joined = this.state.data.concat(newData);
this.setState({ data: joined})
})
This is ofcourse assuming that you're using this.state.data.map to render the chat log. This will work when the message that I send cant be seen by me and as for the messages updating as the database updates, you may wanna make use of .onDataChange callback provided by Firebase API. I hope I helped.
Related
Working on creating a function that receives an object. This object contains key, value pairs of usernames.
I iterate through the key value pairs and for each pair, create a Twilio Conference Room topic, I assign key+value to this topic name
Inside the for loop, I add each pair as a participant in a Twilio Conference Room, create a conference room for each and presumably redirect each pair to their respective conference rooms.
However, the intended action of redirecting each pair to their respective Twilio Conference rooms is not occurring. I instead get the errors:
Uncaught TypeError: Cannot read properties of null (reading 'connect')
I initially start off by creating a device object in a previous component:
const setupTwilio = (nickname) => {
fetch(`http://127.0.0.1:8000/voice_chat/token/${nickname}`)
.then(response => console.log('setupTwilio returns: ',response.json()))
.then(data => {
const twilioToken = JSON.parse(data).token;
const device = new Device(twilioToken);
device.updateOptions(twilioToken, {
codecPreferences: ['opus', 'pcmu'],
fakeLocalDTMF: true,
maxAverageBitrate: 16000,
maxCallSignalingTimeoutMs: 30000
});
device.on('error', (device) => {
console.log("error: ", device)
});
setState({... state, device, twilioToken, nickname})
})
.catch((error) => {
console.log(error)
})
};
const handleSubmit = e => {
e.preventDefault();
const nickname = user.username;
setupTwilio(nickname);
const rooms = state.rooms;
updateWaitingRoomStatus()
setState((state) => {
return {...state, rooms }
});
sendWaitingRoomUsersToRedisCache()
history.push('/rooms');
}
From here the user then navigates to the next component/page. This is the waiting room. In this component, a useEffect creates said room and redirects each pair:
useEffect((() => {
handleRoomCreate()
}), [availableOnlineUsers])
const createRoomHandler = (curr,matched) => {
const userData = {'roomName': curr+matched, 'participantLabel': [curr, matched] }
axios.post('http://127.0.0.1:8000/voice_chat/rooms', userData )
.then(res => {
console.log('axios call has been hit', res.data)
})
}
const handleRoomCreate = () => {
for ( let [k, v] of new Map( Object.entries(randomizeAndMatch(availableOnlineUsers.current)))){
const createdRoomTopic = k+v
setState({ ...state, createdRoomTopic})
const selectedRoom = {
room_name: state.createdRoomTopic, participants: [k,v]
};
setState({ ...state, selectedRoom})
const rooms = state.rooms;
const roomId = rooms.push(selectedRoom)
createRoomHandler(k,v);
history.push(`/rooms/${roomId}`)
}
}
Here is a sample of the data I get from randomizeAndMatch(availableOnlineUsers.current), the function that returns an object with pairs of users:
{testingUser: 'emmTest', testing22: 'AnotherUser'}
This is the voice_chat/rooms API endpoint:
...
def post(self, request, *args, **kwargs):
room_name = request.POST.get("roomName", "default")
participant_label = request.POST.get("participantLabel", "default")
response = VoiceResponse()
dial = Dial()
dial.conference(
name=room_name,
participant_label=participant_label,
start_conference_on_enter=True,
)
print(dial)
response.append(dial)
return HttpResponse(response.to_xml(), content_type="text/xml")
I know the above view works when it comes to creating a conference call and presume I wouldn't have to make any changes to this view to create multiple conference calls since I would be iteratively posting to this endpoint
The error appears to point to this Rooms, specifically that there is no room_name in state? In Rooms I grab the selectedRoom room_name from state
const roomName = state.selectedRoom.room_name;
useEffect(() => {
const params = {
roomName: roomName, participantLabel: nickname
};
if (!call) {
const callPromise = device.connect({ params });
callPromise.then((call) => {
setCall(call);
});
}
I do this because I need params to actually start the conference call. Any ideas what I am doing wrong?
UPDATE
Realized I wasn't resolving the promise and creating a device object
Removing the console log I had in my setupTwilio function resolved this:
const setupTwilio = (nickname) => {
fetch(`http://127.0.0.1:8000/voice_chat/token/${nickname}`)
.then(response => response.json())
.then(data => {
const twilioToken = JSON.parse(data).token;
const device = new Device(twilioToken);
device.updateOptions(twilioToken, {
codecPreferences: ['opus', 'pcmu'],
fakeLocalDTMF: true,
maxAverageBitrate: 16000,
maxCallSignalingTimeoutMs: 30000
});
device.on('error', (device) => {
console.log("error: ", device)
});
setState({... state, device, twilioToken, nickname})
})
.catch((error) => {
console.log(error)
})
};
However, I am not getting the error:
error: ConnectionError: ConnectionError (53000): Raised whenever a signaling connection error occurs that is not covered by a more specific error code.
at ConnectionError.TwilioError [as constructor] (twilioError.ts:48:1)
at new ConnectionError (generated.ts:340:1)
at PStream._handleTransportMessage (pstream.js:176:1)
at WSTransport.emit (events.js:153:1)
at WSTransport._this._onSocketMessage (wstransport.ts:453:1)
How do I batch multiple operations? I found this resource for Firestore https://firebase.google.com/docs/firestore/manage-data/transactions does database have something similar.
I wanted to push a new message collection when chat is created.
export const newChat = () => {
return push(ref(db, "chats"),{
...
})
.then((data) => {
update(ref(db, `chats/${data.key}`), {
chat_id: key,
});
//push(ref(db, `messages/${data.key}`), {
//text: "hello"
//});
})
.catch((error) => {
return error;
});
};
What you're describing can be accomplished with:
const newRef = push(ref(db, "chats"));
set(newRef, {
chat_id: newRef.key,
...restOfYourObject
});
The call to push does not cause any network traffic yet, as push IDs are a pure client-side operation.
I have a firebase serviceworker that shows notifications when a message is pushed from Firebase Cloud Messaging (FCM).
It also publishes a post so that my React App can update accordingly.
/* eslint-env worker */
/* eslint no-restricted-globals: 1 */
/* global firebase */
/* global clients */
import config from './config'
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js')
const { FIREBASE_MESSAGING_SENDER_ID } = config
firebase.initializeApp({ messagingSenderId: FIREBASE_MESSAGING_SENDER_ID })
const messaging = firebase.messaging()
messaging.setBackgroundMessageHandler(payload => {
const title = payload.data.title
const options = {
body: payload.data.body,
icon: payload.data.icon,
data: payload.data,
}
clients.matchAll({ includeUncontrolled: true }).then(clientz => {
clientz.forEach(client => {
sendMessageToClient(client, 'NEW_USER_NOTIFICATON')
})
})
return self.registration.showNotification(title, options)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}
This all works fine, but I have added it for context.
What I want to do is have a click function that focuses on the correct window/tab and navigates to a link that is passed to it. Or if the tab is not open, open a new window and go to the link.
This is the code I have so far, added to the above file.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.claim()
.then(() => self.clients
.matchAll({
type: 'window',
})
)
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
return matchingClient.navigate(link)
.then(() => matchingClient.focus())
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
So, I realise that the chained navigate and focus inside a then is probably bad practice, but for now, I am just trying to get it to work. Then I will try and come up with a clever solution.
So the problem with my code is that the clients.claim() doesn't seem to be working. The matchAll doesn't return anything to the next then, the argument is an empty array.
I could simply add the includeUncontrolled: true option to the matchAll, but the navigate command only works on a controlled client instance.
If I try the often referenced Google example for claiming and navigation, it works fine:
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim().then(() => {
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/matchAll
return self.clients.matchAll({type: 'window'});
}).then(clients => {
return clients.map(client => {
// Check to make sure WindowClient.navigate() is supported.
if ('navigate' in client) {
return client.navigate('activated.html');
}
});
}));
});
So I am stuck.
The serviceworker is activated immediately, so I assume that it claim a client at any point after that.
Have I fallen for a random ServiceWorker Gotcha?
Can the claim only be used and navigated to on the handling of an activation event?
I would appreciate any help available.
Cheers
I couldn't get this to work.
But I thought it would be worth documenting my workaround.
I could not get client.navigate to work in the notificationclick event handler.
So instead I just sent a postMessage containing the URL to be picked up in my app to trigger the redirect there, without any client claiming anywhere.
self.addEventListener('notificationclick', event => {
const clickedNotification = event.notification
const link = clickedNotification.data.link
clickedNotification.close()
const promiseChain = self.clients.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then(windowClients => {
let matchingClient = null
windowClients.forEach(client => {
if (client.url.includes(matching_url)) {
matchingClient = client
}
})
if (matchingClient) {
sendMessageToClient(matchingClient, { type: 'USER_NOTIFICATION_CLICKED', link })
return matchingClient.focus()
}
return clients.openWindow(link)
})
event.waitUntil(promiseChain)
})
const sendMessageToClient = (client, message) => {
const messageChannel = new MessageChannel()
client.postMessage(message, [messageChannel.port2])
}
I am trying to write a function in Cloud Functions that triggers every time a user gets created and which then saves that user into a list of users and finally increments a user counter.
However I am not sure if I am using promises correctly.
exports.saveUser = functions.auth.user().onCreate(event => {
const userId = event.data.uid
const saveUserToListPromise = db.collection("users").doc(userId).set({
"userId" : userId
})
var userCounterRef = db.collection("users").doc("userCounter");
const transactionPromise = db.runTransaction(t => {
return t.get(userCounterRef)
.then(doc => {
// Add one user to the userCounter
var newUserCounter = doc.data().userCounter + 1;
t.update(userCounterRef, { userCounter: newUserCounter });
});
})
.then(result => {
console.log('Transaction success!');
})
.catch(err => {
console.log('Transaction failure:', err);
});
return Promise.all([saveUserToListPromise, transactionPromise])
})
I want to make sure that even if many users register at once that my userCounter is still correct and that the saveUser function won't be terminated before the transaction and the save to the list has happened.
So I tried this out and it works just fine however I don't know if this is the correct way of achieving the functionality that I want and I also don't know if this still works when there are actually many users triggering that function at once.
Hope you can help me.
Thanks in advance.
The correct way to perform multiple writes atomically in a transaction is to perform all the writes with the Transaction object (t here) inside the transaction block. This ensures at all of the writes succeed, or none.
exports.saveUser = functions.auth.user().onCreate(event => {
const userId = event.data.uid
return db.runTransaction(t => {
const userCounterRef = db.collection("users").doc("userCounter")
return t.get(userCounterRef).then(doc => {
// Add one user to the userCounter
t.update(userCounterRef, { userCounter: FirebaseFirestore.FieldValue.increment(1) })
// And update the user's own doc
const userDoc = db.collection("users").doc(userId)
t.set(userDoc, { "userId" : userId })
})
})
.then(result => {
console.info('Transaction success!')
})
.catch(err => {
console.error('Transaction failure:', err)
})
})
I am trying to set the last card added to the stripe as the default via firebase functions though I can't seem to get it to work.
// Add a payment source (card) for a user by writing a stripe payment source token to Realtime database
exports.addPaymentSource = functions.database.ref('/users/{userId}/sources/{pushId}/token').onWrite(event => {
const source = event.data.val();
if (source === null) return null;
return admin.database().ref(`/users/${event.params.userId}/customer_id`).once('value').then(snapshot => {
return snapshot.val();
}).then(customer => {
return stripe.customers.createSource(customer, {source});
return stripe.customers.update(customer, {default_source: source});
}).then(response => {
return event.data.adminRef.parent.set(response);
}, error => {
return event.data.adminRef.parent.child('error').set(userFacingMessage(error)).then(() => {
// return reportError(error, {user: event.params.userId});
consolg.log(error, {user: event.params.userId});
});
});
});
You're trying to return two things in this one function. That isn't going to work. It should create the source, but it won't update it.
return stripe.customers.createSource(customer, {source});
return stripe.customers.update(customer, {default_source: source});