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.
Related
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>)
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
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
I'm having a big deal - the meteor app I've been developing the last weeks is finally online. But, for an update, I need to add a field to my users profile.
I thought that walling a methods with the following code would work :
updateUsrs_ResetHelps: function(){
if(Meteor.users.update({}, {
$set: {
'profile.helps': []
}
}))
console.log("All users profile updated : helps reset");
else
throw new Meteor.Error(500, 'Error 500: updateUsrs_ResetHelps',
'the update couldn\'t be performed');
}
The problem is that my users have the classic Meteor.accounts document, whith emails, _id, services, profile, etc... but, in the profile, they don't have a .helps fields. I need to create it.
For the future users, I've modified the accounts creation function to add this fields when they sign up, but for the 200 users I already got signed up, I do really need a solution.
EDIT : Might it be because of the selector in the update ? Is a simple {} selector valid to update all the users / documents of the collection at once ?
From the Mongo documentation (http://docs.mongodb.org/manual/reference/method/db.collection.update/):
By default, the update() method updates a single document. Set the
Multi Parameter to update all documents that match the query criteria.
If you've already taken care of adding the field for new users and you just need to fix the old ones, why not just do it one time directly in the database?
Run meteor to start your application, then meteor mongo to connect to the database. Then run an update on records where the field doesn't already exist. Something like:
db.users.update({"profile.helps": {"$exists": false}}, {"$set": {"profile.helps": []}}, {multi:true})
The Mongo documentation specifies the multi parameter as:
Optional. If set to true, updates multiple documents that meet the
query criteria. If set to false, updates one document. The default
value is false.
I want to create the function so when a logged in user scrolls through blog posts, they can click a 'x' button which removes the post from the results instantly, and that post will not yield in future search results for that particular user.
This is what I have so far and it works in terms of the 'x' button retrieving the doc ID of the post. I then inserted this id into an array field called...
not_intereted[],
This field is in the user profile.
Accounts.createUser({
email: template.find("#signup-email").value,
password: template.find("#signup-password").value,
profile: {
not_interested: []} });
Each user is subscribed to the entire post collection.
AllPosts = new Meteor.Collection('allposts')
For each post rendered, there is a delete button which fires the following event when clicked. This event successfully retrieves the selected document _id into the variable 'a'. This variable successfully is passed to the method.
Template.postBoard.events({
'click a.close': function(e){
e.preventDefault();
Session.set("selectedPostId", this._id);
var a = Session.get('selectedPostId');
Meteor.call('addToremoveList', a);
}
});
On the server:
Meteor.methods({
'addToremoveList': function(a){
Meteor.users.update( { _id: Meteor.userId() }, { $addToSet: { 'profile.not_interested': a }} );
}
});
What I have so far works. I am able to store the document ID that the user wishes to delete. The ID is stored into an array located in the users profile field named not_interested.
QUESTION: How can I eliminate these not_interested documents when invoking `AllPosts.find();
Im unsure how to implement mongos $ne function.
Thank you.
You need to use the mongo nin operator: http://docs.mongodb.org/manual/reference/operator/query/nin/#op._S_nin
Keep all users subscribed to the entire posts collection. Then, on the client, select the posts you want to display (perhaps using iron router's data functionality) by doing:
selectedPosts: AllPosts.find({_id: {$nin: Meteor.user().profile.not_interested} })
(I didn't test this, but it should work. Please leave a comment if it doesn't.)