I am trying to create a live chat with vanilla JS and firebase.
I'm able to add and get the "newest" message render to the DOM with the following codes:
add message
async addChat(message) {
const chat = {
message,
createdAt: serverTimestamp(),
};
return await addDoc(this.chat, chat);
}
get message
getChat(syncChat) {
onSnapshot(this.chat, snapshot => {
snapshot.docChanges().forEach(shot => {
if (shot.type === "added") {
syncChat(shot.doc.data());
}
});
});
}
render message to the DOM
render(data) {
const html = `
<li class="chat-item">
<span class="chat-message">${data.message}</span>
<p class="chat-time">${formatDistanceToNow(data.createdAt.toDate(), {
addSuffix: true,
})}</p>
</li>
`;
this.chatList.innerHTML += html;
}
Like I said, I can render the chat message no problem, but I realize the there's a time delay when message is added to firebase and timestamp is created. So when the first new message is displayed on the DOM, the time actually return null, and I have to refresh the page in order to display the actually time.
I am just wondering if there's anyway to fix that? I tried looking through the firebase doc and got nothing. I also did a little bit of digging on the interest but most of them are using a framework and not vanilla js.
Thanks for the help.
SOLUTION
getChat(showChat) {
const q = query(
this.collection,
where("room", "==", this.room),
orderBy("createdAt", "asc")
);
this.unsub = onSnapshot(q, snapshot => {
snapshot.docChanges().forEach(snap => {
if (snap.type === "added") {
showChat(snap.doc.data({ serverTimestamps: "estimate" }));
}
});
});
}
Thanks for the help! I have decide to use the snapshotoptions, the estimate servertimestamps, and everything works great.
When you perform a write operation on a client, the Firestore SDK immediately invoked all local listeners with the new data - something typically referred to as latency compensation within Firebase.
So when you write a new document with a server-side timestamp, your onSnapshot listener gets called before the request is sent to the server, and at that point the value of the timestamp field will be null, since it hasn't been calculated yet.
Then when the server confirms that the write operation was completed, and what the timestamp value was, your onSnapshot listener gets called again, now with the correct value for the timestamp field. Since this is a change to the document you already got before, it is an event with type changed.
To display the update, your code needs to also handle events where the type is changed, and use that to update the DOM element that it added for the first event.
Alternatively you can ask the Firestore SDK to give you an estimated timestamp on the first event by passing an option to the data() call as shown here: Firebase Cloud Firestore how to set snapShotOptions
Related
I am trying to create a chat like app using Firestore and trying to list all messages in chat and updates it every time when a message is added.
I tried this way first to implement it.
mounted() {
docRef.collection('messages').orderBy('timestamp', 'desc').limit(1).onSnapshot((querySnapShot) => {
querySnapShot.forEach((doc) => {
if (!doc.metadata.hasPendingWrites) {
this.messages.push(doc.data())
}
})
})
}
This way seems efficient because this way only gets the latest message in collection. However, I found out this way has a problem. When a user refreshs a page, the user can't get the past messages.
So I changed it like this.
mounted() {
docRef.collection('messages').orderBy('timestamp', 'asc').onSnapshot((querySnapShot) => {
querySnapShot.docChanges().forEach((change) => {
if (change.type === 'added') {
this.messages.push(change.doc.data())
}
})
})
}
This way works as I expected. But this way needs a lot of requests because every time the collection is changed I need to read all the documents in the collection.
What is the efficient way to list messages in chat?
I thought it works if I get all the current messages first and set the listener for new messages but the listener is triggered immediately after I enter the page even though there is no change in the collection and read the latest message twice.
I ended up just using a flag to check whether the initial trigger is done. I don't know if this is smart way but this works.
// Get all current messages
docRef.collection('messages').orderBy('timestamp', 'asc').get().then((querySnapShot) => {
querySnapShot.forEach((doc) => {
this.messages.push(doc.data())
})
})
// Update for new messages (Skip the initial loading)
docRef.collection('messages').orderBy('timestamp', 'desc').limit(1).onSnapshot((querySnapShot) => {
querySnapShot.forEach((doc) => {
if (!doc.metadata.hasPendingWrites && this.isInitialDone) {
this.messages.push(doc.data())
}
this.isInitialDone = true
})
})
You are going to need to either set an appropriate limit, or add a filter to determine which ones you want.
If you want to go with a size limit, just change limit(1) in your first query to the number you actually want.
If you want to go with a filter, you should probably use the timestamp you already have in place in order to determine how far back in the past you would like to go.
Those are your two options for limiting the size of the results. You can use either one or both together, but there is no other way of limiting the size of the result set.
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).
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
How can I listen to a specific field change with firestore js sdk ?
In the documentation, they only seem to show how to listen for the whole document, if any of the "SF" field changes, it will trigger the callback.
db.collection("cities").doc("SF")
.onSnapshot(function(doc) {
console.log("Current data: ", doc && doc.data());
});
You can't. All operations in Firestore are on an entire document.
This is also true for Cloud Functions Firestore triggers (you can only receive an entire document that's changed in some way).
If you need to narrow the scope of some data to retrieve from a document, place that in a document within a subcollection, and query for that document individually.
As Doug mentioned above, the entire document will be received in your function. However, I have created a filter function, which I named field, just to ignore document changes when those happened in fields that I am not interested in.
You can copy and use the function field linked above in your code. Example:
export const yourCloudFunction = functions.firestore
.document('/your-path')
.onUpdate(
field('foo', 'REMOVED', (change, context) => {
console.log('Will get here only if foo was removed');
}),
);
Important: The field function is not avoiding your function to be executed if changes happened in other fields, it will just ignore when the change is not what you want. If your document is too big, you should probably consider Doug's suggestion.
Listen for the document, then set a conditional on the field you're interesting in:
firebase.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(word).collection('Pronunciations').doc('Castilian-female-IBM').onSnapshot(function(snapshot) {
if (snapshot.data().audioFiles) { // eliminates an error message
if (snapshot.data().audioFiles.length === 2) {
audioFilesReady++;
if (audioFilesReady === 3) {
$scope.showNextWord();
}
}
}
}, function(error) {
console.error(error);
});
I'm listening for a document for a voice (Castilian-female-IBM), which contains an array of audio files, in webm and mp3 formats. When both of those audio files have come back asynchronously then snapshot.data().audioFiles.length === 2. This increments a conditional. When two more voices come back (Castilian-male-IBM and Latin_American-female-IBM) then audioFilesReady === 3 and the next function $scope.showNextWord() fires.
Just out of the box what I do is watching before and after with the before and after method
const clientDataBefore = change.before.data();
console.log("Info database before ", clientDataBefore);
const clientDataAfter = change.after.data();
console.log("Info database after ", clientDataAfter );
For example now you should compare the changes for a specific field and do some actions or just return it.
Some more about before.data() and after.data() here
I have an app that uses firebase, the whole stack pretty much, functions, database, storage, auth, messaging, the whole 9. I want to keep the client end very lightweight. So if a user comments on a post and "tags" another user, let's say using the typical "#username" style tagging, I moved all of the heavy lifting to the firebase functions. That way the client doesn't have to figure out the user ID based on the username, and do everything else. It is setup using triggers, so when the above scenario happens I write to a "table" called "create_notifications" with some data like
{
type: "comment",
post_id: postID,
from: user.getUid(),
comment_id: newCommentKey,
to: taggedUser
}
Where the taggedUser is the username, the postID is the active post, the newCommentKey is retrieved from .push() on the comments db reference, and the user.getUid() is from the firebase auth class.
Now in my firebase functions I have a "onWrite" trigger for that specific table that gets all of the relevant information and sends out a notification to the poster of the post with all the relevant details. All of that is complete, what I am trying to figure out is... how do I delete the incoming event, that way I don't need any sort of cron jobs to clear out this table. I can just grab the event, do my needed calculations and data gathering, send the message, then delete the incoming event so it never even really exists in the database except for the small amount of time it took to gather the data.
A simplified sample of the firebase functions trigger is...
exports.createNotification = functions.database.ref("/create_notifications/{notification_id}").onWrite(event => {
const from = event.data.val().from;
const toName = event.data.val().to;
const notificationType = event.data.val().type;
const post_id = event.data.val().post_id;
var comment_id, commentReference;
if(notificationType == "comment") {
comment_id = event.data.val().comment_id;
}
const toUser = admin.database().ref(`users`).orderByChild("username").equalTo(toName).once('value');
const fromUser = admin.database().ref(`/users/${from}`).once('value');
const referencePost = admin.database().ref(`posts/${post_id}`).once('value');
return Promise.all([toUser, fromUser, referencePost]).then(results => {
const toUserRef = results[0];
const fromUserRef = results[1];
const postRef = results[2];
var newNotification = {
type: notificationType,
post_id: post_id,
from: from,
sent: false,
create_on: Date.now()
}
if(notificationType == "comment") {
newNotification.comment_id = comment_id;
}
return admin.database().ref(`/user_notifications/${toUserRef.key}`).push().set(newNotification).then(() => {
//NEED TO DELETE THE INCOMING "event" HERE TO KEEP DB CLEAN
});
})
}
So in that function in the final "return" of it, after it writes the finalized data to the "/user_notifications" table, I need to delete the event that started the whole thing. Does anyone know how to do that? Thank you.
First off, use .onCreate instead of .onWrite. You only need to read each child when they are first written, so this will avoid undesirable side effects. See the documentation here for more information on the available triggers.
event.data.ref() holds the reference where the event occurred. You can call remove() on the reference to delete it:
return event.data.ref().remove()
The simplest way to achieve this is through calling the remove() function offered by the admin sdk,
you could get the reference to the notification_id through the event, i.e event.params.notification_id then remove it when need be with admin.database().ref('pass in the path').remove(); and you are good to go.
For newer versions of Firebase, use:
return change.after.ref.remove()