Firebase Firestore onSnapshot() doesn't react to changes in a subcollection - javascript

I'm currently working on a react-native app with expo that uses firebase firestore as a database. In order to implement a chatting feature where users can have 1 on 1 conversations with other users, I've set up onSnapshot listeners in the code below that runs on app start. Essentially, what it should be doing is creating a listener for conversations that involve the user, and for each of those conversations, adds another listener for its 'messages' subcollection.
const newListener = (convoRef.where("pIds", "array-contains", u.uid)
.onSnapshot(convSnap => {
convos = new Map();
convSnap.forEach((conv) => {
if (!(conv.get("toDelete") == true)) {
convos.set(conv.id, conv)
const textsRef = firebase.firestore().collection('Conversations').doc(conv.id).collection('messages')
const newConvoListener = (textsRef
.orderBy('timestamp','desc')
.onSnapshot(querySnapshot => {
//processes the new messages. omitted for brevity
}, err => {"Error fetching messages" + err}))
activeListeners.push(newConvoListener);
}
})
}, error => console.log("problem fetching conversation snapshot: " + error)
))
activeListeners.push(newListener);
Elsewhere in the app, users can start conversations with others, which is done with this code. It essentially creates a new document representing a new conversation between the two users, then creates another document within the 'messages' subcollection of the aforementioned document that acts as a dummy message representing the start of their conversation.
await db.collection('Conversations').add({
pIds: [uid,potentialMatchID],
pNames: [username,potentialMatchName],
pPics: [uPic ? uPic : null, potentialMatchPic ? potentialMatchPic : null],
toDelete: false,
}).catch((error) => {console.log("problem " + error + "\n" + pIds + pNames + pPics)})
.then((newConversation) => {
db.collection('Conversations').doc(newConversation.id).collection('messages').add({
recipient: potentialMatchID,
liked: false,
text: uid,
placeholder: true,
timestamp: new firebase.firestore.Timestamp.now()
});
}).catch((error) => {console.log("problem adding conversation" + error)});
The issue I am having occurs when a new conversation is created between two users. Let's call the user that initiated the creation of the conversation (by running the code above) device A, while the receiving user is device B.
For device A, the conversations listener reacts to the change as expected, and creates a listener for the newly created conversation's 'messages' subcollection.
That subcollection listener correctly gets the dummy message during the initial fetch when the listener is being created, but then does not react to any further messages within the new conversation. New documents within the 'messages' subcollection do not cause the listener to activate, whether they be sent from device B or device A (they are still able to add new messages to the subcollection, but the listener never notices that they do so - the changes are only reflected in the console and on device B) or directly added to the subcollection via the console. The app has to be restarted for the listeners to start reacting appropriately to changes in firestore.
The listeners on device B all work perfectly. Their listeners correctly identify the new conversation, create a 'messages' listener, receive the initial dummy message, and then correctly react to new incoming/outgoing messages. Similarly, the conversations that were already there for device A continue to behave normally, and if a third device starts a conversation with device A, device A responds appropriately.
So, I'm pretty sure the issue has to do with the fact that device A is creating a listener for a collection that they themselves created just moments ago. I think it might have something to do with local writes being registered by listeners instantly before they go out to firestore, but not sure.
On the firebase end, it's set up like this:
Conversations Collection -> individual documents representing a conversation and its metadata -> Messages Subcollection -> individual documents representing a message within the parent conversation

Related

Firestore - Listen infinite list onSnapshot()

I have a "My Chats" screen with a "ChatList" component in my web app.
Each "ChatListItem" has the user avatar, the date of the last message, the content of the last message (which can be deleted) etc.
It is represented like this in my DB
ChatRoom doc = {
lastMessage: {
date,
content,
senderId
},
members: {id1: { username, avatar }, id2: { username, avatar }},
membersIds: [id1, id2],
read: false,
}
And stored on Firestore following this NoSQL structure:
/
chatsCollection/
chatDoc1
chatDoc2
...
This is how I am listening all the current user chats:
export function listenMyChats(limitToLast, onNext, onError) {
const currentUserId = getCurrentUser().uid;
const query = firestore
.collection("chats")
.where("membersIds", "array-contains", currentUserId)
.orderBy("lastMessage.date", "asc");
return query.limitToLast(limitToLast).onSnapshot(onNext, onError);
}
If I limit the listener results to 20, then it will be not possible to handle the changes of for example, the 80th item of my ChatList.
Imagine that the other user deletes the last message he sent, updating the chatroom doc data to:
ChatRoom doc = {
lastMessage: {
date, <---- DATE HAS NOT CHANGED!
deleted: true, <-----
senderId
},
members: {id1: { username, avatar }, id2: { username, avatar }},
membersIds: [id1, id2],
read: false,
}
As you can see, the last message's date has not changed, and I am listening the 20 first items ordered by the last message's date.
This will cause a problem. If the doc is in the 21st position of my ChatList, I will not be able to update the GUI with the respective new data, as the listener will not detect any changes.
I have thought to combine the listener with a pull-to-refresh system inside the "My Chats" screen. But, is there any other approach to handle all the items without deleting the limitToLast(20)?
The issue probably is that you are trying to model your data in a different way from how Firestore is designed.
you are treating documents which come through the listener as events. But in fact what you should do is construct a query which shows exactly what data you want to see in the UI, e.g. the most recent 20 chats which aren't deleted.
The Firestore SDK will then tell you whenever the set of documents matched by the query changes. Then you need to update the UI to match the new set of documents in the watch list. If a chat is deleted, the Firestore SDK will still let you know because it tracks documents disappearing from the query results, and documents appearing.
Another option, perhaps more similar to what you are doing now, would be to have actual events stored in a collection somewhere. A deletion event would include the timestamp at which it was deleted, so you can be confident that looking at the most recent documents will be likely to include the changes you are interested in.
For more information related to “firestore infinite scroll” you can refer to the stackoverflow case and document where a brief description about the infinite scroll has been mentioned and how you can do it in your application.

How to add a collection of documents to a newly created document in firebase?

I have built a messaging app using react and firebase. When a user creates a new group chat, a new document a new chat id (made randomly by Firebase) is created, with the date that the chat was created. The participants of the group chat are added to a new 'participants' collection which is added to the new chat document afterwards. Here is my code:
db.collection('chats').add({ //add generates new chat document and id
created: firebase.firestore.FieldValue.serverTimestamp() // when new chat was created
})
.then((docRef) => { //docRef.id contains newly created chat Id
console.log("docRef is" + docRef.id) // this works, and new chatId is created
db.collection("chats")
.doc(docRef.id) //this isn't working, firebase is creating a new document with new random id, instead of using the docRef.id created above which represents the new chat.
.collection("participants")
.doc(auth.currentUser.id)
.set(currentUser)
db.collection("chats")
.doc(docRef.id) //not working
.collection("participants")
.doc(user.id)
.set(otherUser)
...
docRef.id works and is returned correctly as shown by the console.log. However, when I try to reference it in the following statements it doesn't work and Firebase creates a brand new document, instead of adding to the one I want.
Not sure what I'm doing wrong! Thanks.
The code looks fine to me. Are you sure the same function isn't running again which is creating new chats? Also you are not awaiting those promises returned by set(). Try refactoring to this:
db.collection('chats').add({
created: firebase.firestore.FieldValue.serverTimestamp()
}).then(async (docRef) => {
// async ^^
const newChatId = docRef.id
console.log("New chat ID:" + newChatId)
const newChatRef = db.collection("chats").doc(docRef.id)
await newChatRef.collection("participants").doc(auth.currentUser.id).set(currentUser)
await newChatRef.collection("participants").doc(user.id).set(otherUser)
})
Addionally, if you are adding multiple documents once, try using Batched Writes so they all are either added or not if any one of them fails.

Listen to Firebase Firestore data changes for current data only?

Let's say I have a Firebase firestore database table called "Articles".
I pull in a list of articles whenever I open the app, and through a FlatList component, whenever I reach the end, I pull in more articles using startAt() and endAt()
I also have the ability to like or comment on articles, so whenever I do so, the current articles in the screen get updated (like counter, comment counter gets updated).
However, I also have a separate backend that adds additional articles to the database after 6 hours interval.
If I subscribe to changes in the Articles database, the startAt() and endAt() will get messed up if new data is added when the user is scrolling through the Flatlist (right?). Instead, I only want to notify the user whenever new articles are added, and display a "New Posts Available" button at the top whenever there are any, and if user clicks on it, it just refreshes the list.
However, I still want to listen to changes in likes and comments of the article, without adding any new articles to the flatlist when the backend adds them.
How do I go about solving this?
You can check for the type of event i.e. if the document was created, modified or deleted as mentioned in documentation. In case a new document is added, the type will be "created". Then just don't append them to your Flatlist. Any updated to likes count will be a "modified" event so you can just update that in the list. Here's an example copied from the docs:
db.collection("cities").where("state", "==", "CA")
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New city: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified city: ", change.doc.data());
}
if (change.type === "removed") {
console.log("Removed city: ", change.doc.data());
}
});
});
Important: The first query snapshot contains added events for all existing documents that match the query. This is because you're getting a set of changes that bring your query snapshot current with the initial state of the query. This allows you, for instance, to directly populate your UI from the changes you receive in the first query snapshot, without needing to add special logic for handling the initial state.
So the first time the user opens you app and at the same time the new documents were being added, they may end up in the list. If you still want to be strict with the time these docs were added, you can try adding a condition which says documents returned in the query should be created before a particular timestamp.
.where("addedAt", "<=", <the-timestamp>)

How to query database with firebase cloud functions

I am trying to query my firestore database using cloud functions.
I want to trigger an email notification every time a new reading in my database is under the value of 10.
Here is the relevant database structure for reference: database structure.
The "readings" field is an array and each "reading" is a map which holds the fields "date" and "value".
Currently I am at the point where I can send an email notification every time a new user is created however I want this to work for the database. I am unsure how to query for the "readings" array and then for each individual reading.
Here is my code so far which sends an email when a new user is created
exports.sendNotification = functions.auth.user().onCreate((user) => {
const mailOptions = {
from: '"Spammy Corp." <noreply#firebase.com>',
to:"fakeEmail#btopenworld.com",
text: "TEST"
};
return mailTransport.sendMail(mailOptions)
.then(() => console.log("It worked"))
.catch((error) =>
console.error('There was an error while sending the email:', error));
});
See: https://firebase.google.com/docs/firestore/extend-with-functions
For example, to fire on all new readings added to that first child:
exports.sendEmail = functions.firestore
.document('sensor/UGt.../readings')
.onCreate((snap, context) => {
const newValue = snap.data();
const value = newValue.value;
if (value < 10) {
// send email
}
});
In further comments you mentioned listening for new readings in all sensor elements, not just your first one. This is unfortunately not possible in an efficient / simple way (source). Instead you will have to listen to all onUpdate events on /sensor/, check if the update is adding a reading, then check the value & send your email.
It may be easier to call the cloud function directly from wherever adds the reading, depending on how many times the /sensor/ path is going to be updated for other reasons (since every time this happens, it's a waste of resources).

React Native Chat Using Mongodb

I'm pretty new to react native and I am currently developing a chat/forum application. Recently, I have been having some trouble trying to create a direct message section for my app. I don't know how to connect my database to my frontend. Here is the issue:
I use a mongodb database that consists of two collections: messages and conversations.
Each conversation has a unique Id and each message has a chatId that corresponds to the conversation that it belongs to.
In my react native app, inside a Direct Message component, I have a flatlist that displays the different chats.
When the Direct Message component willMount() I call an async function, getChats(), that fetches the chats from the database that the current user is part of. The fetched chats are then set to the state.
Then, inside getChats(), after the chats are set to the state, I have a for loop that basically loops through the entire this.state.chats array and then calls a function getMessages(this.state.chats[i].id) which fetches all the messages that share the same chatIds as the chats ids. The fetched messages are then added to this.state.messages.
Finally, a flatlist with the props,
keyExtractor={(item)=>item._id}
data ={this.state.chats}
renderItem={({item})=>this._renderChats(item)}
extraData = {this.state}
,renders the chats.
I want to be able to show the latest messages content and sender in the chat View, however, an error saying that the messages content is undefined.
I think this may be due to the fact that messages aren't set to the state before the chats are rendered, but I'm not sure.
How would you solve this? Would you change the frontend or the backend? Would you change both? Should I share my code to make it easier to understand the problem?
Thanks in advance.
Very clear explanation of the problem! In short:
How to build a chat page with conversations overview (showing last message for each conversation) - using React Native, MongoDB, NodeJS and Express
Some notes:
use consistent naming, ie. rename chatId to conversationId and chats to conversations
try to minimize the internet requests - because they're resource intensive and slow
right now, your algorithm makes conversations.count+1 requests each time the page is opened, could be just 1
for the first page, you only need the last message in each conversation
load the rest of the messages for a conversation when its page is opened
thereby, you don't need the extraData field
(though caching, see additional notes)
eg. using GraphQL query('currentUser.conversations { text, messages(limit 1), ... }')
eg. using rest + mongoose:
// controller
// see https://stackoverflow.com/questions/32207457/node-js-mongoose-populate-limit
// populate the first item in conversation.messages
const conversations = ()=> db.Conversations
.find(...)
.populate({ path: 'messages', options: { limit: 1, sort: { created: -1} }}))
.exec(...)
// router
router.get('/conversations', ({user}, res)=>
getConversations({user})
.then(data=> res.send({data}))
.catch(error=> res.send({error}))
)
(this assumes messages are a virtual property on conversations)
// schema
const messageSchema = new Schema({
text: String, ...
conversationId: {type: Schema.ObjectId, ref: 'conversation'}
})
const conversationSchema = new Schema({
participants: [{type: Schema.ObjectId, ref: 'user'}]
})
// add a "virtual" field to conversation called messages
// https://stackoverflow.com/questions/43882577/mongoosejs-virtual-populate
conversationSchema.virtual('messages', {
ref: 'message',
localField: '_id',
foreignField: 'conversationId'
})
// make sure the "virtual" fields are included in conversion
conversationSchema.set('toObject', { virtuals: true });
conversationSchema.set('toJSON', { virtuals: true });
assume that all data is broken; eg. the app should not crash if the message data is missing. Check that the property exists before accessing it, and convert it into the expected data type before proceeding.
Make sure the errors aren't "swallowed"; eg. make sure to always have a .catch and log the error if you have a .then, and paste the error messages to the question, if any.
Uncaught TypeError: Cannot read property 'c' of undefined, when doing a.b.c, where you know that a is an object, can be avoided by first checking; if (a.b) use(a.b.c), or with shortcircuit: a.b && use(a.b.c)
correct hypothesis, what happend was:
ConversationsPage initialized, state.conversations is Empty
willMount called -> fetchAllConversations started, takes time, async
willMount ends -> render called -> empty list rendered
fetchAllConversations' first request finishes -> setState{conversations} -> render called again -> full list rendered (in your case, it crashed because the last message field was missing)
fetchAllConversations invokes fetchMessagesForConversations, which makes many api requests, and possibly calls setState multiple times (ineffective), which in turn causes re-render
don't forget the loading state
state = {loading: false}
render () {
const {loading} = this.state
return <FlatList renderHeader={()=> loading? <ActivityIndicator .../>: null} .../>
}
instead, a simple fix would be to call setState after all messages have been loaded:
async fetchAllConversations () {
const conversations = await api.conversations()
await Promise.all(conversations.map(c=> c.messages = await api.messagesForConversationId(c._id)))
// similar to for (let i=0; i<conversations.length; i++) {
// const c = conversations[i]; c.messages = await api.messagesForConversationId(c._id)}
return conversations
}
async reload () {
this.setState({loading: true})
this.fetchAllConversations()
.then(conversations=> this.setState({conversations}))
.catch(showError)
// stop loading both on error and success
.then(()=> this.setState({loading: false}))
}
state = {loading: false, conversations: []}
willMount () { this.reload() }
though a better solution would be to replace fetchAllConversations from above, assuming the use of virual property and population mentioned above server-side:
async fetchAllConversationsIncludingLastMessage () {
const conversations = await api.conversationsWithLastMessage()
return conversations
}
this would reduce the flow to:
ConversationsPage initialized, state.conversations is Empty
willMount called -> reload started, takes time, async
willMount ends -> render called -> loading indicator rendered
reload's only request finishes -> setState{conversations} -> render called again -> full list rendered
Additional notes:
look into Docker for simplified server setup (ie. to get MongoDB + Node.js running together)
I presume you've got a middleware that does the authentication, + correct authorisation logic in the query (eg. only find the conversations/messages the authorised user should have access to)
ie. db.Conversation.find({participants: {$includes: req.user._id}}) // pseudocode
ie. in messages, first see if the conversation with that id has the user as participant
how do you handle pagination? (eg. to prevent slow data fetching and slow UI when there are many posts) (tips: use a "cursor" instead of "offset" - prevents duplication issue etc)
Use some library to cache the data locally to improve perceived and actual loading time.
Central state management (using eg. Mobx, Redux, Apollo...) solves some of that
if you're going to use REST anyhow, make an API wrapper helper + look into mobx
otherwise, check out GraphQL and Apollo or similar

Categories

Resources