how to stop repetition of previous post - javascript

In the displayLikedPosts there is a function called getLikedPosts. this getLikedPosts function filter the posts ID and insert posts into an array. Now after running forEach loop inside the displayLikedPosts ---first time it adds a new and single post(ex:post-1) but second time it displays the previous post twice and the new post (ex: post-1,post-1,newpost). But my purpose is to display only the previous one and the new posts(ex:post-1,newpost,newpost2,newpost4)
code
const displayLikedPosts = () => {
const likedPosts = getLikedPosts();
likedPosts.forEach((post) => {
const div = createPost(post);
document.getElementById("liked").appendChild(div);
});
};

Clear thr content inside #liked before appending new elements
Pseudo Code
const displayLikedPosts = () => {
const likedPosts = getLikedPosts();
document.getElementById("liked")innerHTML = "";
likedPosts.forEach((post) => {
const div = createPost(post);
document.getElementById("liked").appendChild(div);
});
};

You must first delete the children of your parent DOM node, then add the
elements again.
There are many ways to delete the children, which are discussed in this thread. You may find that some options are better (and/ or more modern) than others.
Here is a small minimal example for your use case using setInterval() to prove that the list does not contain duplicates.
/**
* Display posts
* #param {string[]} posts posts to display
*/
function displayPosts(posts) {
const parent = document.getElementById("liked");
// delete children
deleteChildren(parent);
// add items to the list
posts.forEach((post) => {
const listItem = document.createElement("li");
listItem.textContent = post;
parent.appendChild(listItem);
});
console.log("rendered")
}
/**
* Deletes all children of a DOM node.
* #param {HTMLElement} parent parent element
*/
function deleteChildren(parent) {
while (parent.firstChild) {
parent.firstChild.remove();
}
}
window.addEventListener("DOMContentLoaded", (event) => {
// posts to display
const posts = ["post 1", "post 2", "post 3", "post 4"];
displayPosts(posts)
// call method every 3 seconds to demonstrate it works
setInterval(() => displayPosts(posts), 3 * 1000);
});
<ul id="liked">
</ul>

Related

Firestore listener removes a message from pagination when adding a new message in React Native

I am trying to do Firestore reactive pagination. I know there are posts, comments, and articles saying that it's not possible but anyways...
When I add a new message, it kicks off or "removes" the previous message
Here's the main code. I'm paginating 4 messages at a time
async getPaginatedRTLData(queryParams: TQueryParams, onChange: Function){
let collectionReference = collection(firestore, queryParams.pathToDataInCollection);
let collectionReferenceQuery = this.modifyQueryByOperations(collectionReference, queryParams);
//Turn query into snapshot to track changes
const unsubscribe = onSnapshot(collectionReferenceQuery, (snapshot: QuerySnapshot) => {
snapshot.docChanges().forEach((change: DocumentChange<DocumentData>) => {
//Now save data to format later
let formattedData = this.storeData(change, queryParams)
onChange(formattedData);
})
})
this.unsubscriptions.push(unsubscribe)
}
For completeness this is how Im building my query
let queryParams: TQueryParams = {
limitResultCount: 4,
uniqueKey: '_id',
pathToDataInCollection: messagePath,
orderBy: {
docField: orderByKey,
direction: orderBy
}
}
modifyQueryByOperations(
collectionReference: CollectionReference<DocumentData> = this.collectionReference,
queryParams: TQueryParams) {
//Extract query params
let { orderBy, where: where_param, limitResultCount = PAGINATE} = queryParams;
let queryCall: Query<DocumentData> = collectionReference;
if(where_param) {
let {searchByField, whereFilterOp, valueToMatch} = where_param;
//collectionReferenceQuery = collectionReference.where(searchByField, whereFilterOp, valueToMatch)
queryCall = query(queryCall, where(searchByField, whereFilterOp, valueToMatch) )
}
if(orderBy) {
let { docField, direction} = orderBy;
//collectionReferenceQuery = collectionReference.orderBy(docField, direction)
queryCall = query(queryCall, fs_orderBy(docField, direction) )
}
if(limitResultCount) {
//collectionReferenceQuery = collectionReference.limit(limitResultCount)
queryCall = query(queryCall, limit(limitResultCount) );
}
if(this.lastDocInSortedOrder) {
//collectionReferenceQuery = collectionReference.startAt(this.lastDocInSortedOrder)
queryCall = query(queryCall, startAt(this.lastDocInSortedOrder) )
}
return queryCall
}
See the last line removed is removed when I add a new message to the collection. Whats worse is it's not consistent. I debugged this and Firestore is removing the message.
I almost feel like this is a bug in Firestore's handling of listeners
As mentioned in the comments and confirmed by you the problem you are facing is occuring due to the fact that some values of the fields that your are searching in your query changed while the listener was still active and this makes the listener think of this document as a removed one.
This is proven by the fact that the records are not being deleted from Firestore itself, but are just being excluded from the listener.
This can be fixed by creating a better querying structure, separating the old data from new data incoming from the listener, which you mentioned you've already done in the comments as well.

How to click an element that we get from http request using SetInterval?

window.addEventListener("load" , () => {
setInterval(() => {
let xhttp = new XMLHttpRequest();
xhttp.open("GET", "./messagedUsers.php", true);
xhttp.onload = () => {
if(xhttp.readyState === XMLHttpRequest.DONE) {
if(xhttp.status === 200) {
let data = xhttp.response;
sideActiveUsers.innerHTML = data;
}
}
}
xhttp.send();
} , 500);
messageInputContainer.focus();
})
let userInfo = document.querySelectorAll(".user-info");
userInfoClick(userInfo); // function that does operation using the nodeList userInfo
In the above code , what I'm trying to do is send a HttpRequest to the php and get a div container as a response from there. As a response , I do get the div container. The div I got from there has a class name "user-info". And I want to click it and further , apply some css to the all the elements it holds(as there will be lot of "user-info" container coming from the response.). What I'm unable to figure out is "user-info" container can be applied styling through css(not JS). But if I try to use some JS for the node with className "user-info" , nothing happens as the nodeList seems to be empty.
And I've also tried using querySelector inside the setInterval for "user-info". But that makes the function outside userInfoClick(userInfo) takes a userInfo that is not defined.
And if I use both querySelector and the function that takes the variable that holds that nodeList(userInfoClick), I am able to click the element coming back from php. But its effect is stopped after every 500 ms(as the setInterval is set for 500ms).
I want to be able to click the element coming back from php and click it whose effect doesn't change after every 500ms(along with the element getting from php every 500ms).
Help me out guys.
userInfoClick function:
const userInfoClick = (userInfo) => {
let userinfo = Array.from(userInfo);
userinfo.forEach((el , ind) => {
el.addEventListener("click" , () => {
for(let i=0 ; i<userInfo.length ; i++)
{
if(ind!==i)
{
userinfo[i].style.backgroundColor = "#dddddd";
}
}
el.style.backgroundColor = "#fff";
})
})
}
You cannot bind an event handler to an element that does not yet exist. The querySelectorAll call will return an empty collection, as the assignment to innerHTML will only happen in some future.
In this case, where you have the interval, it is better to use event delegation. This means that you listen to click events at a higher level in the DOM tree -- on an element that is always there and is not dynamically created. In your case this could be sideActiveUsers.
So change your code as follows:
Remove this line (as it will return a useless empty collection):
let userInfo = document.querySelectorAll(".user-info");
And change the userInfoClick function, which should be called only once, without any arguments:
function userInfoClick() {
sideActiveUsers.addEventListener("click" , (e) => {
const el = e.target;
// We're only interested in user-info clicks:
if (!el.classList.contains("user-info")) return;
// Since we clicked on a user-info, the following will now have matches:
for (const sibling of sideActiveUsers.querySelectorAll(".user-info")) {
sibling.style.backgroundColor = "#dddddd";
}
el.style.backgroundColor = "#fff";
});
}

Sort Descending for Javascript cards

I am trying to create a sorting method that sorts the cards in the DOM from Z to A when I press a button. So far, I have created the logic to sort the array correctly but can't seem to render it out.
I have a Drink Class and a DrinkCard Class, and the DrinkCard does the actual card creation, and the Drink creates the Drink.
I feel like calling the Drink class would help render sorted array to the DOM, but not sure how I would do that. Drawing blanks.
This is what I have so far
UPDATE I updated with the suggestion below, but I don't have a rendered-content id anywhere. So, I used the querySelector on the class .card and this is the current error.
Uncaught DOMException: Failed to execute 'appendChild' on 'Node': The new child element contains the parent.
at Drink.render (file:///Users/austinredmond/dev/caffeine_me/frontend/src/models/drink.js:28:17)
at file:///Users/austinredmond/dev/caffeine_me/frontend/src/index.js:43:38
at Array.forEach (<anonymous>)
at HTMLInputElement.<anonymous> (file:///Users/austinredmond/dev/caffeine_me/frontend/src/index.js:43:17)
render # drink.js:28
(anonymous) # index.js:43
(anonymous) # index.js:43
sortDesc.addEventListener("click", () => {
const sortedArray = allDrinks.sort((a, b) => {
const nameA = a.name.toLowerCase(),
nameB = b.name.toLowerCase()
if (nameA < nameB) //sort string ascending
return 1
if (nameA > nameB)
return -1
return 0 //default return value (no sorting)
})
const node = document.querySelector('.card');
sortedArray.forEach(card => card.render(node));
})
Drink Class
class Drink {
constructor(data) {
// Assign Attributes //
this.id = data.id
this.name = data.name
this.caffeine = data.caffeine
this.comments = []
this.card = new DrinkCard(this, this.comments)
}
// Searches allDrinks Array and finds drink by id //
static findById(id) {
return allDrinks.find(drink => drink.id === id)
}
// Delete function to Delete from API //
delete = () => {
api.deleteDrink(this.id)
delete this
}
render(element) {
// this method will render each card; el is a reference to a DOM node
console.log(element)
element.appendChild(this.card.cardContent);
}
}
DrinkCard Class
class DrinkCard {
constructor(drink, comments) {
// Create Card //
const card = document.createElement('div')
card.setAttribute("class", "card w-50")
main.append(card)
card.className = 'card'
// Add Nameplate //
const drinkTag = document.createElement('h3')
drinkTag.innerText = drink.name
card.append(drinkTag)
// Add CaffeinePlate //
const caffeineTag = document.createElement('p')
caffeineTag.innerText = `Caffeine Amount - ${drink.caffeine}`
card.append(caffeineTag)
// Adds Create Comment Input Field //
const commentInput = document.createElement("input");
commentInput.setAttribute("type", "text");
commentInput.setAttribute("class", "input-group mb-3")
commentInput.setAttribute("id", `commentInput-${drink.id}`)
commentInput.setAttribute("placeholder", "Enter A Comment")
card.append(commentInput);
// Adds Create Comment Button //
const addCommentButton = document.createElement('button')
addCommentButton.innerText = "Add Comment"
addCommentButton.setAttribute("class", "btn btn-primary btn-sm")
card.append(addCommentButton)
addCommentButton.addEventListener("click", () => this.handleAddComment())
// Add Comment List //
this.commentList = document.createElement('ul')
card.append(this.commentList)
comments.forEach(comment => this.addCommentLi(comment))
// Create Delete Drink Button
const addDeleteButton = document.createElement('button')
addDeleteButton.setAttribute("class", "btn btn-danger btn-sm")
addDeleteButton.innerText = 'Delete Drink'
card.append(addDeleteButton)
addDeleteButton.addEventListener("click", () => this.handleDeleteDrink(drink, card))
// Connects to Drink //
this.drink = drink
this.cardContent = card;
}
// Helpers //
addCommentLi = comment => {
// Create Li //
const li = document.createElement('li')
this.commentList.append(li)
li.innerText = `${comment.summary}`
// Create Delete Button
const button = document.createElement('button')
button.setAttribute("class", "btn btn-link btn-sm")
button.innerText = 'Delete'
li.append(button)
button.addEventListener("click", () => this.handleDeleteComment(comment, li))
}
// Event Handlers //
// Handle Adding Comment to the DOM //
handleAddComment = () => {
const commentInput = document.getElementById(`commentInput-${this.drink.id}`)
api.addComment(this.drink.id, commentInput.value)
.then(comment => {
commentInput.value = ""
const newComment = new Comment(comment)
Drink.findById(newComment.drinkId).comments.push(newComment)
this.addCommentLi(newComment)
})
}
// Loads last comment created for drink object //
handleLoadComment = () => {
const newComment = this.drink.comments[this.drink.comments.length - 1]
this.addCommentLi(newComment)
}
// Deletes Comment from API and Removes li //
handleDeleteComment = (comment, li) => {
comment.delete()
li.remove()
}
// Deletes Drink from API and Removes drink card //
handleDeleteDrink = (drink, card) => {
drink.delete()
card.remove()
}
}
There are a few ways you can do this:
Pass a reference to a DOM node where you want to append your Drink cards
Get the raw HTML from the card and add it to an element as needed
For (1), try the following changes:
DrinkCard.js
class DrinkCard {
constructor(drink, comments) {
// your existing code
this.cardContent = card; // or card.innerHTML
}
}
Drink.js
class Drink {
// your existing code
render(el) {
// this method will render each card; el is a reference to a DOM node
el.appendChild(this.card.cardContent);
}
}
Finally, passing the DOM reference to the sorted entries:
const node = document.getElementById('rendered-content'); // make sure this exists
sortedArray.forEach(card => card.render(node));
Hopefully that will give you some pointers on how to render the cards for your purpose.
Updates
The error you are getting is because of the following reasons:
First, as you pointed out, an element with id rendered-content does not exist in your DOM
Using .card to append the rendered element results in a cyclical error because you are trying to append an element (.card) to the same element.
You can try the following:
Add <div id="rendered-content"></div> in your HTML somewhere the sorted cards needs to be rendered
If you don't want to have it in the HTML page all the time, you can create it before you pass it's reference. So,
const rc = document.createElement('div');
rc.setAttribute('id', 'rendered-content');
document.body.appendChild(rc);
const node = document.getElementById('rendered-content');
sortedArray.forEach(card => card.render(node));
This should help get rid of the errors hopefully.
Further Explanation
I am going to give you a very brief description of browser rendering and how it's working in this case. I will also leave a link for a detailed article that goes into more depth.
In the rendering flow, the following happens:
Your HTML document is retrieved by the browser and parsed
A DOM tree is then created from the parsed document
Layout is applied to the DOM (CSS)
Paint the content of the DOM to the display
Your code took care of almost everything in the original post. You created the card, added it's content and sorted the cards based on Drink type. The only step missing was adding it all to the DOM.
If you create elements dynamically like you did in the DrinkCard class, you need to attach it to your existing DOM. Otherwise, there is no way for the browser to know your card is in the DOM. Once you modify the DOM, layout and repainting is triggered which then shows your content on the display.
The purpose of div with id='rendered-content' is to provide a container that exists in your DOM or is added before you use it. When you are adding nodes to your DOM, you need a reference element where you want to add your new node. This reference could easily be document.body. In that case, the render method will add the card at the bottom of your body in your DOM. Providing a separate container in this case gives you more control on how you can display this container.
Here's an in depth discussion of rendering and how it works in the browser here. Hope the explanation answers your question.

how should i dynamically update a firebase document

i'm working on a simple note-taking app for my portfolio using JS and Firebase. Before i tell you what's happening i feel like i need to show you how my code works, if you have any tips and concerns please tell me as it would be GREATLY appreciated. That being said, let's have a look "together". I'm using this class to create the notes:
const htmlElements = [document.querySelector('.notes'), document.querySelector('.note')];
const [notesDiv, noteDiv] = htmlElements;
class CreateNote {
constructor(title, body) {
this.title = title;
this.body = body;
this.render = () => {
const div1 = document.createElement('div');
div1.className = 'notes-prev-container';
div1.addEventListener('click', () => { this.clickHandler(this) });
const div2 = document.createElement('div');
div2.className = 'notes-prev';
const hr = document.createElement('hr');
hr.className = 'notes__line';
// Nest 'div2' inside 'div1'
div1.appendChild(div2);
div1.appendChild(hr);
/*
Create Paragraph 1 & 2 and give them the same
class name and some text
*/
const p1 = document.createElement('p');
p1.className = 'notes-prev__title';
p1.innerText = this.title;
const p2 = document.createElement('p');
p2.className = 'notes-prev__body';
p2.innerText = this.body;
// Nest p 1 & 2 inside 'div2'
div2.appendChild(p1);
div2.appendChild(p2);
// Finally, render the div to its root tag
notesDiv.appendChild(div1);
}
}
/*
Every time this method is called, it creates 2 textareas,
one for the note title and the other for its body then it
appends it to the DOM.
*/
renderNoteContent () {
const title = document.createElement('textarea');
title.placeholder = 'Title';
title.value = this.title;
title.className = 'note__title';
const body = document.createElement('textarea');
body.placeholder = 'Body';
body.value = this.body;
body.className = 'note__body';
noteDiv.appendChild(title);
noteDiv.appendChild(body);
}
/*
When this method is called, it checks to see if there's a
note rendered already (childElementCount === 1 because there's a
button, so if there's only this button it means there's no
textareas rendered).
If yes, then merely call the renderNoteContent method. Else
get the tags with the classes 'note__title' and 'note__body'
and remove them from the DOM, then call renderNoteContent to
create the textareas with the clicked notes values.
This function gets mentioned at line 19.
*/
clickHandler(thisClass) {
if (noteDiv.childElementCount === 1) {
thisClass.renderNoteContent();
} else {
document.querySelector('.note__title').remove();
document.querySelector('.note__body').remove();
thisClass.renderNoteContent();
}
}
}
Now i need 2 buttons, createNotesButton and saveNotesButton respectively. These 2 buttons must be inside a function that will be called inside .onAuthStateChanged (why? because they will be needing access to the currentUser on firebase auth).
I want the createNotesButton to create a note prototype, render it to the DOM and create a new document on firestore, where this note contents will be stored. Here's how i did it:
PS: I feel like i'm not using this class correctly, so again if you have any tips i appreciate it.
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
/*
1. Creates a class.
2. Creates a new document on firebase with the class's empty value.
3. Renders the empty class to the DOM.
*/
createNotesButton.addEventListener('click', () => {
const note = new CreateNote('', '');
note.render();
// Each user has it's own note collection, said collection has their `uid` as name.
db.collection(`${user.uid}`).doc().set({
title: `${note.title}`,
body: `${note.body}`
})
})
}
Now i need a saveNotesButton, he's the one i'm having issues with. He needs to save the displayed note's content on firestore. Here's what i tried doing:
import {db} from '../../firebase_variables/firebase-variables.js';
import {CreateNote} from '../create_notes_class/create_notes_class.js';
const htmlElements = [
document.querySelector('.createNotes-button'),
document.querySelector('.saveNotes-button')
];
const [createNotesButton, saveNotesButton] = htmlElements;
function clickHandler(user) {
createNotesButton.addEventListener('click', () => {...})
/*
1. Creates 2 variables, `title` and `body, if there's not a note being displayed
their values will be null, which is why the rest of the code is inside an if
statement
2. If statement to check if there's a note being displayed, if yes then:
1. Call the user's note collection. Any document who has the title field equal to the
displayed note's value gets returned as a promise.
2. Then call an specific user document and update the fields `title` and `body` with
the displayed note's values.
3. If no then do nothing.
*/
saveNotesButton.addEventListener('click', () => {
const title = document.querySelector('.note__title');
const body = document.querySelector('.note__body');
db.collection(`${user.uid}`).where('title', '==', `${title.value}`)
.get()
.then(userCollection => {
db.collection(`${user.uid}`).doc(`${userCollection.docs[0].id}`).update({
title: `${title.value}`,
body: `${body.value}`
})
})
.catch(error => {
console.log('Error getting documents: ', error);
});
});
}
This didn't work because i'm using title.value as a query, so if i change it's value it will also change the queries direction to a path that doesn't exist.
So here's the question: how can i make it so the saveNotesButton does its job? I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note. Again, if there's something in my code that you think can or should be formatted please let me know, i'm using this project as a way to solidify my native JS knowledge so please be patient. I feel like if i had used React i would've finished this sometime ago but definitely wouldn't have learned as much, anyway thanks for your help in advance.
I was thinking of adding another field to each note, something that won't change so i can easily identify and edit each note.
Yes, you absolutely need an immutable identifier for each note document in the firestore so you can unambiguously reference it. You almost always want this whenever you're storing a data object, in any application with any database.
But, the firestore already does this for you: after calling db.collection(user.uid).doc() you should get a doc with an ID. That's the ID you want to use when updating the note.
The part of your code that interacts with the DOM will need to keep track of this. I suggest moving the code the creates the firestore document into the constructor of CreateNote and storing it on this. You'll need the user id there as well.
constructor(title, body, userId) {
this.title = title;
this.body = body;
const docRef = db.collection(userId).doc();
this.docId = docRef.id;
/* etc. */
Then any time you have an instance of CreateNote, you'll know the right user and document to reference.
Other suggestions (since you asked)
Use JsPrettier. It's worth the setup, you'll never go back.
Use HTML semantics correctly. Divs shouldn't be appended as children of hrs, because they're for "a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section." MDN
For your next project, use a framework. Essentially no one hand-codes event listeners and appends children to get things done. I see the value for basic understanding, but there's a rich and beautiful world of frameworks out there; don't limit yourself by avoiding them :-)

Delete same value from multiple locations Firebase Functions

I have a firebase function that deletes old messages after 24 hours as in my old question here. I now have just the messageIds stored in an array under the user such that the path is: /User/objectId/myMessages and then an array of all the messageIds under myMessages. All of the messages get deleted after 24 hours, but the iDs under the user's profile stay there. Is there a way to continue the function so that it also deletes the messageIds from the array under the user's account?
I'm new to Firebase functions and javascript so I'm not sure how to do this. All help is appreciated!
Building upon #frank-van-puffelen's accepted answer on the old question, this will now delete the message IDs from their sender's user data as part of the same atomic delete operation without firing off a Cloud Function for every message deleted.
Method 1: Restructure for concurrency
Before being able to use this method, you must restructure how you store entries in /User/someUserId/myMessages to follow best practices for concurrent arrays to the following:
{
"/User/someUserId/myMessages": {
"-Lfq460_5tm6x7dchhOn": true,
"-Lfq483gGzmpB_Jt6Wg5": true,
...
}
}
This allows you to modify the previous function to:
// Cut off time. Child nodes older than this will be deleted.
const CUT_OFF_TIME = 24 * 60 * 60 * 1000; // 2 Hours in milliseconds.
exports.deleteOldMessages = functions.database.ref('/Message/{chatRoomId}').onWrite(async (change) => {
const rootRef = admin.database().ref(); // needed top level reference for multi-path update
const now = Date.now();
const cutoff = (now - CUT_OFF_TIME) / 1000; // convert to seconds
const oldItemsQuery = ref.orderByChild('seconds').endAt(cutoff);
const snapshot = await oldItemsQuery.once('value');
// create a map with all children that need to be removed
const updates = {};
snapshot.forEach(messageSnapshot => {
let senderId = messageSnapshot.child('senderId').val();
updates['Message/' + messageSnapshot.key] = null; // to delete message
updates['User/' + senderId + '/myMessages/' + messageSnapshot.key] = null; // to delete entry in user data
});
// execute all updates in one go and return the result to end the function
return rootRef.update(updates);
});
Method 2: Use an array
Warning: This method falls prey to concurrency issues. If a user was to post a new message during the delete operation, it's ID could be removed while evaluating the deletion. Use method 1 where possible to avoid this.
This method assumes your /User/someUserId/myMessages object looks like this (a plain array):
{
"/User/someUserId/myMessages": {
"0": "-Lfq460_5tm6x7dchhOn",
"1": "-Lfq483gGzmpB_Jt6Wg5",
...
}
}
The leanest, most cost-effective, anti-collision function I can come up for this data structure is the following:
// Cut off time. Child nodes older than this will be deleted.
const CUT_OFF_TIME = 24 * 60 * 60 * 1000; // 2 Hours in milliseconds.
exports.deleteOldMessages = functions.database.ref('/Message/{chatRoomId}').onWrite(async (change) => {
const rootRef = admin.database().ref(); // needed top level reference for multi-path update
const now = Date.now();
const cutoff = (now - CUT_OFF_TIME) / 1000; // convert to seconds
const oldItemsQuery = ref.orderByChild('seconds').endAt(cutoff);
const snapshot = await oldItemsQuery.once('value');
// create a map with all children that need to be removed
const updates = {};
const messagesByUser = {};
snapshot.forEach(messageSnapshot => {
updates['Message/' + messageSnapshot.key] = null; // to delete message
// cache message IDs by user for next step
let senderId = messageSnapshot.child('senderId').val();
if (!messagesByUser[senderId]) { messagesByUser[senderId] = []; }
messagesByUser[senderId].push(messageSnapshot.key);
});
// Get each user's list of message IDs and remove those that were deleted.
let pendingOperations = [];
for (let [senderId, messageIdsToRemove] of Object.entries(messagesByUser)) {
pendingOperations.push(admin.database.ref('User/' + senderId + '/myMessages').once('value')
.then((messageArraySnapshot) => {
let messageIds = messageArraySnapshot.val();
messageIds.filter((id) => !messageIdsToRemove.includes(id));
updates['User/' + senderId + '/myMessages'] = messageIds; // to update array with non-deleted values
}));
}
// wait for each user's new /myMessages value to be added to the pending updates
await Promise.all(pendingOperations);
// execute all updates in one go and return the result to end the function
return ref.update(updates);
});
Update: DO NOT USE THIS ANSWER (I will leave it as it may still be handy for detecting a delete operation for some other need, but do not use for the purpose of cleaning up an array in another document)
Thanks to #samthecodingman for providing an atomic and concurrency safe answer.
If using Firebase Realtime Database you can add an onChange event listener:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.onDeletedMessage = functions.database.ref('Message/{messageId}').onChange(async event => {
// Exit if this item exists... if so it was not deleted!
if (event.data.exists()) {
return;
}
const userId = event.data.userId; //hopefully you have this in the message document
const messageId = event.data.messageId;
//once('value') useful for data that only needs to be loaded once and isn't expected to change frequently or require active listening
const myMessages = await functions.database.ref('/users/' + userId).once('value').snapshot.val().myMessages;
if(!myMessages || !myMessages.length) {
//nothing to do, myMessages array is undefined or empty
return;
}
var index = myMessages.indexOf(messageId);
if (index === -1) {
//nothing to delete, messageId is not in myMessages
return;
}
//removeAt returns the element removed which we do not need
myMessages.removeAt(index);
const vals = {
'myMessages': myMessages;
}
await admin.database.ref('/users/' + userId).update(vals);
});
If using Cloud Firestore can add an event listener on the document being deleted to handle cleanup in your user document:
exports.onDeletedMessage = functions.firestore.document('Message/{messageId}').onDelete(async event => {
const data = event.data();
if (!data) {
return;
}
const userId = data.userId; //hopefully you have this in the message document
const messageId = data.messageId;
//now you can do clean up for the /user/{userId} document like removing the messageId from myMessages property
const userSnapShot = await admin.firestore().collection('users').doc(userId).get().data();
if(!userSnapShot.myMessages || !userSnapShot.myMessages.length) {
//nothing to do, myMessages array is undefined or empty
return;
}
var index = userSnapShot.myMessages.indexOf(messageId);
if (index === -1) {
//nothing to delete, messageId is not in myMessages
return;
}
//removeAt returns the element removed which we do not need
userSnapShot.myMessages.removeAt(index);
const vals = {
'myMessages': userSnapShot.myMessages;
}
//To update some fields of a document without overwriting the entire document, use the update() method
await admin.firestore().collection('users').doc(userId).update(vals);
});

Categories

Resources