Find, update an element and change its index in array of objects - arrays

What I am trying to achieve here is for a Chat App where the last conversation (between the current logged in user and the target user) should update its lastMessage and be placed at the first position in the conversations array.
Here is a simpler version of the conversations array:
const conversations = [
{ ... },
{
_id: 5,
lastMessage: {
sender: 1
receiver: 2
message: 'Hello World',
conversation_id: 5
}
},
{ ... }
]
If the user with id = 1 sends a message to the user with id = 2 it should update the conversation with _id = 5 and move it to index 0 of the conversations array.
What I currently have done is moving the conversation to the top of the array but I cannot figure out how to change its lastMessage. (React reducer)
conversations: [state.conversations.find(el => el._id === action.payload.conversation_id), ...state.conversations.filter(el => el._id !== action.payload.conversation_id)]
Thanks to anyone who can help me!

Would something like this meet your requirements?
// find the first convo as you already do
const firstConversation = state.conversations
.find(el => el._id === action.payload.conversation_id);
// this is the current `lastMessage`
const lastMessage = {...first.lastMessage};
// make changes to `lastMessage` here, or assign a completely new object as required
const firstConversationUpdated = {...firstConversation, lastMessage: lastMessage};
// update state below
conversations: [firstConversationUpdated,
...state.conversations.filter(el => el._id !== action.payload.conversation_id)]

Related

ReactJS: Why does navigating appear to change `readyState` of previous `EventSource`s?

Question: Why does navigating appear to change the readyState of the previous EventSources?
===============================================
Explanation: I'm working on a frontend (React) in which the user can enter a sequence of search queries (i.e. strings) and for each search query, my backend (Flask) will return a sequence of URLs. For each search query, I've decided to receive the server's response via an EventSource. Specifically, I first create a React state array of backendEventSources:
const [backendEventSources, setBackendEventSources] = useState([]);
Then I update the backendEventSources when a new prompt comes in:
useEffect(() => {
console.log('Inside useEffect')
// Take 0 for the newest prompt.
const newBackendEventSource = new EventSource(
`https://localhost:8080/generate?counter=${promptsResultsArray[0].counter}&prompt=${promptsResultsArray[0].prompt}`,
{withCredentials: false})
newBackendEventSource.addEventListener('open', () => {
console.log('SSE opened!');
});
newBackendEventSource.addEventListener('error', (e) => {
console.log('SSE error!');
console.error('Error: ', e);
});
newBackendEventSource.addEventListener('close', (e) => {
console.log('SSE closed!');
const data = JSON.parse(e.data);
console.log("close data: ", data);
newBackendEventSource.close();
});
newBackendEventSource.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log("message data: ", data);
// https://stackoverflow.com/a/47580775/4570472
const newPromptsResultsArray = [...promptsResultsArray];
// Since we preprend new results, we need to compute the right index from
// the counter with the equation: length - counter - 1.
// e.g., For counter 2 of a length 3 array, we want index 0.
// e.g., For counter 2 of a length 4 array, we want index 1.
// Recall, the counter uses 0-based indexing.
const index = newPromptsResultsArray.length - data.counter - 1
newPromptsResultsArray[index].URIs = [data.uri];
newPromptsResultsArray[index].isLoading = false;
setPromptsResultsArray(newPromptsResultsArray);
// Instantiating the element and setting the src property starts preloading the image.
// for (const newImgURI of newImgURIs) {
// const imageElement = new Image();
// imageElement.src = newImgURI;
// }
// setTimeout(() => {setImgURIs(newImgURIs)}, 8000);
});
// Add new backend event source to state for persistence.
setBackendEventSources(backendEventSources => [
newBackendEventSource,
...backendEventSources])
return () => {
newBackendEventSource.close();
};
}, [prompt]);
I use URL params for React navigation:
const navigateToGenerateResults = (promptString) => {
console.log('Adding new prompt results to promptsResultsArray');
// State doesn't update immediately (or even synchronously). To ensure we can immediately
// access the new values, we create a newPromptsResults.
// https://stackoverflow.com/a/62900445/4570472
const newPromptsResults = {
isLoading: true,
prompt: promptString,
counter: promptsResultsArray.length,
URIs: ["https://simtooreal-public.s3.amazonaws.com/white_background.png"]
}
// Prepend the new prompt to our promptsResultsArray
// https://stackoverflow.com/a/60792858/4570472
setPromptsResultsArray(promptsResultsArray => [
newPromptsResults, ...promptsResultsArray])
console.log('Setting prompt to: ' + newPromptsResults.prompt)
setPrompt(newPromptsResults.prompt)
console.log('Navigating from /generate to /generate with prompt: ' + newPromptsResults.prompt)
navigate(`/generate?counter=${newPromptsResults.counter}&prompt=${newPromptsResults.prompt}`)
}
However, I've discovered that as soon as I navigate from one URL to another, the previous EventSource's ready state switches from 0/1 to 2. Additionally, my newBackendEventSource.addEventListener('close' function is never triggered.
Why does navigating appear to change the readyState of the previous EventSources?

Query Parameter Values Not Updating When Set from Google Map Component

I have a map component that contains some clickable overlays on the map. Users can click and unclick the overlays on the map to select them and when they do so the app loads some data based on the overlays that are currently selected.
The current structure is as follows:
User clicks the map which executes a function passed to the map as a prop, which takes the current value of the neighborhoods and either adds or removes them from the query string.
The function executes a history.push()
I use a useEffect checking the value of the query param neighborhood and send a request to the backend to fetch the listings if the values have changed.
My issue is that when the user clicks on the map, the function executes but the value pushed to the params is never updated, causing the logic to fail the next time the user clicks on the map.
Relevant Snippets of Code are as follows:
history/param variables:
const { region, state, neighborhood, transactiontype } = useParams();
const location = useLocation();
const { pathname, search } = location;
Function that is passed down to the child map component:
const updateChildComponentHandler = (dataFromChild, addorRemove) => {
SetLastSetter("map");
SetNeighborhoodTypeSelected("custom");
// if a new neighborhood is added, just taking the existing string and adding &`neighborhood`
if (addorRemove === "add") {
let newGroup = ""
if (neighborhood !== "any") {
newGroup = `${neighborhood}&${dataFromChild}`;
SetMapChangeType("add");
}
// if no neighborhood was initially selected, just replacing the "any" with the proper neighborhood
if (neighborhood === "any") {
SetMapChangeType("add");
newGroup = `${dataFromChild}`
}
// pushing the new parameter string
const newPath = pathname.replace(neighborhood, newGroup);
history.push(`${newPath}${search}`);
}
// same concept as above, just removing the neighborhood from the string if it is removed from the map
if (addorRemove === "remove") {
let newGroup;
if (neighborhood !== dataFromChild) {
newGroup = neighborhood.replace(`&${dataFromChild}`, "")
SetMapChangeType("delete");
}
if (neighborhood === dataFromChild) {
newGroup = "any";
SetMapChangeType("add");
}
if (neighborhood.split("&")[0] === dataFromChild && neighborhood !== dataFromChild) {
newGroup = neighborhood.replace(`${dataFromChild}&`, "")
SetMapChangeType("delete");
}
const newPath = pathname.replace(neighborhood, newGroup);
const newerPath = `${newPath}${search}`;
history.push(newerPath);
deleteListings(dataFromChild);
}
SetUpdateMap(true);
}
UseEffect Logic:
useEffect(() => {
const func = async () => {
let neighborhoodParams;
if (neighborhood !== "any") {
neighborhoodParams = neighborhood.replace("%20", " ").split("&")
}
if (neighborhood === "any") {
neighborhoodParams = [];
neighborhoodParams[0] = "any";
}
const neighborhoods = neighborhood.split("%20").join("_");
SetNeighborhoodParams(neighborhoodParams);
SetNeighborhoodParamString(neighborhoods);
if (mapChangeType === "add") {
if (neighborhoodParams.length > 0) {
if (!requestMulti) {
await fetchProperties(transactiontype, neighborhoodParams[neighborhoodParams.length - 1].split(" ").join("_"), filteredRegion, state, filters, "single")
}
if (requestMulti) {
await fetchProperties(transactiontype, neighborhoods, filteredRegion, state, filters, "multi")
SetRequestMulti(false);
}
}
}
}
func();
}, [neighborhood]);
The issue I am experiencing is that when the map initially loads, the neighborhood is set to "any". The first time a user clicks an overlay for a neighborhood, the correct data is sent from the map and the map/data requests update and the URL parameter up top shows the new neighborhood. However, the second time a user clicks, the value of { neighborhood } is not updated and is still set to "any", so the function just replaces the value of { neighborhood } rather than adding it on as per above. I previously coded this with class components and am trying to convert it to hooks, but it seems like there is some dissonance that is causing the map component not to have access to the updated history variable. I am new to react/hooks, and appreciate if anyone could lend some advice.
Thanks!

Nested onSnapshot problem in react-native-Firebase Firestore

I'm struggling to overcome problems that I have on nested onSnapshot, so, in short, I have 3 nested onSnapshot and when parent/root onSnapshot updates it also creates new onSnapshot listeners and leaves old ones too, so old and new ones are listening to the changes. I know that I should unsubscribe it but I can't, I'm losing track of which listeners are added or already exist.
One solution is to create array of unsubscribing functions in parent onSnapshot, but there another problem comes, I'm using docChanges with forEach and it is hard to manage which would I unsubscribe from array.
I saw this on Stackoverflow but it doesn't fit mine or even doesn't explain correctly exactly this case: nested listeners
What can you suggest to me? I don't know what else should I do.
Thanks in advance.
here is an example of my code that I'm trying to implement unsubscribe stuff( I use Mobx):
// TODO: optimise this query
const id = auth().currentUser?.uid;
// get all channelId-s that user has
channelParticipantsRef
.where('user.id', '==', id)
.onSnapshot((userChannels) => {
userChannels?.docChanges().forEach(function (channelParticipant) {
const channelParticipantData = channelParticipant;
if (!channelParticipantData.doc.exists) return;
// get all channels data that user has
channelsRef
.where(
'channelId',
'==',
channelParticipantData.doc.data().channelId,
)
.onSnapshot((channels) => {
if (!channels || channels.empty) return;
channels.docChanges().forEach(function (channel) {
const channelDataObject = channel.doc.data();
console.logBeauty(channel.type, 'channelDataObject ');
if (channel.type === 'added' || channel.type === 'modified') {
const channelData: ChannelTransformedDataType = {
...channelDataObject,
language: channelParticipantData.doc.data().language,
channelParticipantId: channelParticipantData.doc.id,
lastMessageDate: {
...channelDataObject.lastMessageDate,
},
otherUsers: [],
};
// get all channels users
channelParticipantsRef
.where('user.id', '!=', id)
.where('channelId', '==', channelDataObject.channelId)
.onSnapshot((channelParticipants) => {
const participants: UserModelType[] = [];
if (channelParticipants.empty)
return self.removeChannel(
channelDataObject.channelId,
);
channelParticipants.docs.forEach(
(channelParticipant) => {
participants.push(channelParticipant.data().user);
},
);
channelData.otherUsers = participants;
console.log(channelData, 'channelDatachannelData');
if (channel.type === 'added')
self.pushChannel(channelData);
else self.editChannel(channelData);
});
} else if (channel.type === 'removed') {
self.removeChannel(channelDataObject.channelId);
}
});
});
});
});

Sort function not working on firebase .onSnapshot for chat app

The actual list where I'm sorting by yDS1a41FxzZlTjVvzxjteKSHSn22:
yDS1a41FxzZlTjVvzxjteKSHSn22: 1571361291153
id: "t4ImLnwtFU5e7VKcCmm4"
photoUrl: "https://firebasestorage.googleapis.*"
startDate: "2019-10-17"
unread: []
userId: "FxttC8c66QXMElHIkBFApVVGevy1"
I have a chat app that uses Cloud Firestore as a backend. I'm using a dynamic property on the chat app so I can't order by in my Firestore query, I have to do it once I pull the data from Firestore and store it in an array. The problem is that it's not sorting by time desc once I store the data in the array.
qry
.onSnapshot((val) => {
val.forEach(snp => {
if (snap.data().likes.includes(snp.id)) {
this.users.push({
id: snp.id,
userId: snp.data().userId,
unread: snap.data().unread,
[this.loggedInUser.uid]: snp.data()[this.loggedInUser.uid],
});
console.log('log', this.users);
}
// remove any unique items
this.users = Array.from(new Set(this.users.map(a => a.id)))
.map(id => {
return this.users.find(a => a.id === id);
});
});
this.getPhotoUrl();
this.checkIfUnread();
let test = this.users.sort((a, b) => (a[this.loggedInUser.uid] < b[this.loggedInUser.uid]) ? 1 : -1);
});
So if you send a message and you go back to the main message screen it should sort the messages by the last message sent...it's ignoring the sort all together.

Setting nested array inside object in ReactJS

I have a Post like application, where a user can add comments with emojis to the post, which I have a method for:
addEmoji = (newEmoji) =>{
// mark if new emoji is already in the array or not
let containsNewEmoji = false;
let authors = []
authors.push(this.props.comment.author.name)
console.log(this.props.comment.author.name)
console.log(authors)
// recreate emojis array
let newEmojis = this.state.emojis.map(emoji => {
// if emoji already there, simply increment count
if (emoji.id === newEmoji.id) {
containsNewEmoji = true;
return {
...newEmoji,
...emoji,
count: emoji.count + 1,
authors: [...authors, authors]
};
}
// otherwise return a copy of previous emoji
return {
...emoji
};
});
console.log(authors)
// if newEmoji was not in the array previously, add it freshly
if (!containsNewEmoji) {
newEmojis = [...newEmojis, {...newEmoji, count: 1, authors: [...authors, authors]}];
}
// set new state
this.setState({ emojis: newEmojis,
showEmoji: true});
}
As shown in the method comments to the code, each emoji-only displays once, otherwise, a count variable will increment, to be shown below each comment.
I would like to add the feature, to save an array of the given username of the person, who added the emoji.
the username is given in as a prop
this.props.comment.author.name
so I have tried making an array to add the names 7
let authors = []
authors.push(this.props.comment.author.name)
the issue is that it's being overwritten each time a new emoji instance is being passed, I tried saving it to the object
return {
...newEmoji,
...emoji,
count: emoji.count + 1,
authors: [...authors, authors] // i want to save the old copy of authors and pass the new name
};
newEmojis = [...newEmojis, {...newEmoji, count: 1, authors: [...authors, authors]}]; // and then set the object in the end
As of now, the array is being overwritten each time, but could I set the parameter inside the object?
This is coming from setting the author field to an empty array early on in the code,
let authors = []
Instead it has to be set to the authors earlier on, as in:
authors: [..emoji.authors, author];
You should also consider using function of setState when dealing with setState.
addEmoji = (newEmoji) => {
const author = this.props.comment.author.name;
this.setState(({ emojis: prevEmojis }) => {
let containsNewEmoji = true;
const newEmojis = prevEmojis.map((emoji)=>{
if(newEmoji.id === emoji.id) {
containsNewEmoji = false;
return {
...emoji,
count: emoji.count + 1,
authors: [..emoji.authors, author];
}
} else {
return {
...emoji,
}
}
});
if(containsNewEmojis) {
newEmojis.push({
...newEmoji,
count: 1,
authors: [author],
});
}
return {
emojis: newEmojis,
}
});
}
I have reversed the containsNewEmoji variable so that it fits the context.
Yes, in the addEmoji method you're currently recreating the authors array each time addEmoji is called. Instead of defining a new authors array, push the new author into the existing authors property of the emoji.
Without knowing how the emoji object is initially created I can't give a definitive answer, but hopefully the following is a start. The solution assumes the emoji object has an authors property of type array.
addEmoji = (newEmoji) => {
// mark if new emoji is already in the array or not
let containsNewEmoji = false;
// recreate emojis array
let newEmojis = this.state.emojis.map(emoji => {
// if emoji already there, simply increment count
if (emoji.id === newEmoji.id) {
containsNewEmoji = true;
return {
...emoji,
count: emoji.count + 1,
authors: [...emoji.authors, this.props.comment.author.name]
};
}
// otherwise return a copy of the previous emoji
return emoji;
});
};

Resources