I'm trying to create a list of events that a user is going to. First I get event keys and then what I would like to do is subscribe to each event and listen for changes. Currently only the last event works because this.eventRef is being changed in the for loop.
eventRef: AngularFireObject<any>
getEvents() {
const eventsGuestsLookup = this.db.object(`eventsGuestsLookup/${this.uid}`).valueChanges()
this.eventsGuestsLookupSub = eventsGuestsLookup
.subscribe(eventKeys => {
if (eventKeys) {
console.log(eventKeys)
for (const k in eventKeys) {
if (eventKey.hasOwnProperty(k)) {
this.eventRef = this.db.object(`events/${k}`)
console.log(this.eventRef)
this.eventRef.snapshotChanges().subscribe(action => {
const key = action.payload.key
const event = { key, ...action.payload.val() }
this.makeEvents(event)
})
}
}
}
})
}
What I do next is get the user's response and for each status I want to display certain information. I don't know any other way of doing this, so I check both lists attending and notAttending and if there is a response from the user I change the event properties.
makeEvents(event) {
console.log(event)
event.goingText = "RSVP"
event.setGoing = 'rsvp'
event.setColor = "rsvp-color"
const attending = this.db.object(`attendingLookup/${this.uid}/${event.key}`).valueChanges()
this.attendingLookupSub = attending
.subscribe(data => {
console.log('attending', data)
if (data) {
event.goingText = "ATTENDING"
event.setGoing = 'thumbs-up'
event.setColor = 'attending-color'
}
})
const notAttending = this.db.object(`not_attendingLookup/${this.uid}/${event.key}`).valueChanges()
this.notAttendingLookupSub = notAttending
.subscribe(data => {
console.log('not attending', data)
if (data) {
event.goingText = "NOT ATTENDING"
event.setGoing = 'thumbs-down'
event.setColor = 'not-attending-color'
}
})
this.events.push(event)
}
*** Edit
const eventsGuestsLookup = this.db.object(`eventsGuestsLookup/${this.uid}`).valueChanges()
eventsGuestsLookup.subscribe(keys => {
of(keys).pipe(
mergeMap(keys => {
Object.keys(keys).map(k => {
console.log(k)
})
return merge(Object.keys(keys).map(k => this.db.object(`events/${k}`)))
})
).subscribe(data => console.log('data', data))
})
what you want to acheive is flat your observables collection. to acheive it you can do something like this :
//Dummy eventKeys observable.
const obs1$ = new BehaviorSubject({key:1, action: 'lorem'});
const obs2$ = new BehaviorSubject({key:2, action: 'lorem'});
const obs3$ = new BehaviorSubject({key:3, action: 'lorem'});
const eventKeys = {
obs1$,
obs2$,
obs3$
};
// Dummy eventsGuestsLookup observable.
of(eventKeys)
.pipe(
//eventsGuestsLookup dispatch collection of obserbable, we want to flat it.
mergeMap(ev => {
// We merge all observables in new one.
return merge(...Object.keys(ev).map(k => ev[k]));
}),
).subscribe(console.log);
inportant note : ev[k] is an Observable object. On your case you should do something like :
.map(k => this.db.object(`events/${k}`)) // will return observable.
live demo
Related
I need to concatenate two objects in Vue/Javascript and order the resulting one by date field but I'm getting empty results using Object.assign().
I've tried other methods searching on StackOverflow but nothing has worked as expected. Mostly I get empty results
methods: {
getAllMessages: function () {
// console.log(this.myID);
let distinct = '';
firebase.firestore().collection('private-messages').where('memberID', '==', `${this.myID}`)
.orderBy('date').onSnapshot(res => {
let members = [];
res.forEach(doc => {
members.push(doc.data().uid);
});
distinct = Array.from(new Set(members));
// console.log(distinct);
this.getUserToTalk(distinct);
});
},
getMyMessages: function (memberID) {
let myMessages = [];
firebase.firestore().collection('private-messages').where('uid', '==', `${this.myID}`).where('memberID', '==', `${memberID}`)
.orderBy('date').onSnapshot(res => {
res.forEach(doc => {
myMessages.push(doc.data());
});
});
// this.myMessages = myMessages;
return myMessages;
},
getMessages: function (memberID) {
let messages = [];
firebase.firestore().collection('private-messages').where('uid', '==', `${memberID}`).where('memberID', '==', `${this.myID}`)
.orderBy('date').onSnapshot(res => {
res.forEach(doc => {
messages.push(doc.data());
});
});
// this.memberMessages = messages;
return messages;
},
getUserToTalk: function (memberID) {
axios.post('http://localhost/backend/getMemberToTalk.php', {
"token": token,
"whoToTalkTo": memberID,
}).then(response => {
if (response.data != "Error getting user data and tour") {
let joinedData = []
// console.log(response.data);
response.data.forEach(res => {
let id = res.memberID;
let messages = this.getMessages(id);
// console.log(messages);
let myMessages = this.getMyMessages(id);
// console.log(myMessages);
// if I use below assignment I can get the messages, but I need to concat them.
// so, if I use let conversation = Object.assign({}, messages, myMessages);
// or let conversation = { ...messages, ...myMessages }
// the result is always empty.
let talk1 = Object.assign(messages);
let talk2 = Object.assign(myMessages);
console.log(talk, talkmy);
let data = {
memberID: res.memberID,
memberProfileImg: res.memberProfileImg,
memberName: res.memberName,
memberLastname: res.memberLastname,
memberCity: res.memberCity,
memberState: res.memberState,
memberMessages: messages,
myMessages: myMessages,
// messages: conversation
}
joinedData.push(data);
});
// console.log(joinedData);
this.memberData = joinedData;
// console.log(this.memberData);
} else {
console.log(response.data);
}
}).catch(error => {
console.log(error);
});
},
}
These are the full objects and in the example below I need to add the object from messages.js:140 as a "third index" in messages.js:137 resulting in
0:{...}
1:{...}
2:{...}
and then
3:{...}
I have a list a discussion object containing an array of comments and each comment can hold an array of replies. I display the discussion this way:
<div v-for="comment in comments" v-bind:key="comment._id">
<Comment :itemId="itemId" :comment="comment" />
<Replies v-if="comment.replies.length > 0" :itemId="itemId" :comment="comment" />
</div>
<Button value="Load more" #clicked="loadMoreComments(itemId)" />
and Replies:
<div v-for="reply in replies" v-bind:key="reply._id">
<Comment :itemId="itemId" :comment="reply" />
</div>
<Button :value="Load more" #clicked="loadChild()"/>
As you can see both use the same pattern. They differ in a computed property:
computed: {
comments() {
return this.$store.getters.DISCUSSION.comments.map(id => this.$store.getters.GET_COMMENT(id));
},
replies() {
return this.$store.getters.GET_REPLIES(this.comment).map(id => this.$store.getters.GET_COMMENT(id));
},
},
When I hit the Load more button for comments, new comments appear. But when I hit the Load more button in replies, then no new reply is displayed though I can see in debugger that the array was enlarged.
Vuex store submodule:
state: () => ({
discussion: {
incomplete: true,
comments: [],
},
comments: {},
}),
getters: {
DISCUSSION: state => state.discussion,
GET_COMMENT: state => id => state.comments[id],
GET_REPLIES: state => (comment) => {
if (comment.allShown) {
return comment.replies;
}
return comment.replies.slice(0, REPLY_LIMIT);
},
},
mutations: {
APPEND_COMMENTS: (state, payload) => {
const { comments, incomplete, userId } = payload;
state.discussion.incomplete = incomplete;
const commentIds = [];
comments.forEach(comment => processComment(state, comment, commentIds, userId));
state.discussion.comments = state.discussion.comments.concat(commentIds);
},
PREPEND_COMMENTS: (state, payload) => {
const { comments, userId } = payload;
const commentIds = [];
comments.forEach(comment => processComment(state, comment, commentIds, userId));
state.discussion.comments = commentIds.concat(state.discussion.comments);
},
SET_REPLIES: (state, payload) => {
console.log('SET_REPLIES');
const { commentId, replies, userId, replace } = payload;
const comment = state.comments[commentId];
if (!comment) {
return;
}
state.comments[commentId].showAll = true;
const commentIds = [];
replies.forEach(reply => processComment(state, reply, commentIds, userId));
if (!comment.replies || comment.replies.length === 0 || replace) {
state.comments[commentId].replies = commentIds;
} else {
state.comments[commentId].replies = comment.replies.concat(commentIds);
}
},
}
function processComment(state, comment, commentIds, userId) {
if (comment.replies) {
const repliesIds = [];
comment.replies.forEach((reply) => {
reply.voted = hasVoted(reply.votes, userId);
state.comments[reply._id] = reply;
repliesIds.push(reply._id);
});
comment.replies = repliesIds;
comment.allShown = comment.replies.length < REPLY_LIMIT;
} else if (!comment.parentId) {
comment.replies = [];
comment.allShown = false;
}
state.comments[comment._id] = comment;
commentIds.push(comment._id);
}
The complete source code is there: https://github.com/literakl/mezinamiridici/tree/comment_refactoring/spa
Here is minimum reproducible codesandbox: https://codesandbox.io/s/frosty-taussig-v8u4b?file=/src/module.js
I have verified that this happens because of the getter with a parameter. When I put the reply in static array so I could use parameter-less getter, it started to work.
I follow this recommendation: https://forum.vuejs.org/t/vuex-best-practices-for-complex-objects/10143
Where is the issue?
Update:
One thing that smells is the mutation GET_REPLIES because it works on the passed object. So Vue has no chance to detect that the object is from the state. So I have rewritten it to pass only commentId and load the comment from the state, but it did not help.
I guest you should replace showAll with allShown prop and also use Vue.set where you add new keys to comments object because due to Vue caveats Vuex doesn't see new props, see caveats for objects
SET_REPLIES: (state, payload) => {
console.log("SET_REPLIES");
const { commentId, replies, userId, replace } = payload;
const comment = state.comments[commentId];
if (!comment) {
console.log(`Comment ${commentId} not found`);
return;
}
state.comments[commentId].allShown = true;
// state.comments[commentId].showAll = true;
...
function processComment(state, comment, commentIds, userId) {
if (comment.replies) {
const repliesIds = [];
comment.replies.forEach(reply => {
Vue.set(state.comments, reply._id, reply);
// state.comments[reply._id] = reply;
repliesIds.push(reply._id);
});
comment.replies = repliesIds;
comment.allShown = comment.replies.length < 3;
} else if (!comment.parentId) {
comment.replies = [];
comment.allShown = false;
}
Vue.set(state.comments, comment._id, comment);
// state.comments[comment._id] = comment;
commentIds.push(comment._id);
}
Also correct GET_REPLIES call like this:
computed: {
replies() {
return this.$store.getters
.GET_REPLIES(this.comment) // passing comment itself instead of its id
.map(id => this.$store.getters.GET_COMMENT(id));
}
},
corrected example
My FlatList does not update when the props I pass from redux change. Every time I send a message I increase everyones unread message count in both firebase and in my redux store. I made sure to include key extractor and extra data, but neither helps. The only thing that changes the unread message count is a reload of the device. How do I make sure the flatList updates with MapStateToProps. I made sure to create a new object by using Object.Assign:
action:
export const sendMessage = (
message,
currentChannel,
channelType,
messageType
) => {
return dispatch => {
dispatch(chatMessageLoading());
const currentUserID = firebaseService.auth().currentUser.uid;
let createdAt = firebase.database.ServerValue.TIMESTAMP;
let chatMessage = {
text: message,
createdAt: createdAt,
userId: currentUserID,
messageType: messageType
};
FIREBASE_REF_MESSAGES.child(channelType)
.child(currentChannel)
.push(chatMessage, error => {
if (error) {
dispatch(chatMessageError(error.message));
} else {
dispatch(chatMessageSuccess());
}
});
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child(channelType)
.child(currentChannel).child('users')
UNREAD_MESSAGES.once("value")
.then(snapshot => {
snapshot.forEach(user => {
let userKey = user.key;
// update unread messages count
if (userKey !== currentUserID) {
UNREAD_MESSAGES.child(userKey).transaction(function (unreadMessages) {
if (unreadMessages === null) {
dispatch(unreadMessageCount(currentChannel, 1))
return 1;
} else {
alert(unreadMessages)
dispatch(unreadMessageCount(currentChannel, unreadMessages + 1))
return unreadMessages + 1;
}
});
} else {
UNREAD_MESSAGES.child(userKey).transaction(function () {
dispatch(unreadMessageCount(currentChannel, 0))
return 0;
});
}
}
)
})
};
};
export const getUserPublicChannels = () => {
return (dispatch, state) => {
dispatch(loadPublicChannels());
let currentUserID = firebaseService.auth().currentUser.uid;
// get all mountains within distance specified
let mountainsInRange = state().session.mountainsInRange;
// get the user selected mountain
let selectedMountain = state().session.selectedMountain;
// see if the selected mountain is in range to add on additional channels
let currentMountain;
mountainsInRange
? (currentMountain =
mountainsInRange.filter(mountain => mountain.id === selectedMountain)
.length === 1
? true
: false)
: (currentMountain = false);
// mountain public channels (don't need to be within distance)
let currentMountainPublicChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Public");
// mountain private channels- only can see if within range
let currentMountainPrivateChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Private");
// get public channels
return currentMountainPublicChannelsRef
.orderByChild("key")
.once("value")
.then(snapshot => {
let publicChannelsToDownload = [];
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
// add the channel ID to the download list
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
// flag whether you can check in or not
if (currentMountain) {
dispatch(checkInAvailable());
} else {
dispatch(checkInNotAvailable());
}
// if mountain exists then get private channels/ if in range
if (currentMountain) {
currentMountainPrivateChannelsRef
.orderByChild("key")
.on("value", snapshot => {
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",
snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
});
}
return publicChannelsToDownload;
})
.then(data => {
setTimeout(function () {
dispatch(loadPublicChannelsSuccess(data));
}, 150);
});
};
};
Reducer:
case types.UNREAD_MESSAGE_SUCCESS:
const um = Object.assign(state.unreadMessages, {[action.info]: action.unreadMessages});
return {
...state,
unreadMessages: um
};
Container- inside I hook up map state to props with the unread messages and pass to my component as props:
const mapStateToProps = state => {
return {
publicChannels: state.chat.publicChannels,
unreadMessages: state.chat.unreadMessages,
};
}
Component:
render() {
// rendering all public channels
const renderPublicChannels = ({ item, unreadMessages }) => {
return (
<ListItem
title={item.info.Name}
titleStyle={styles.title}
rightTitle={(this.props.unreadMessages || {} )[item.id] > 0 && `${(this.props.unreadMessages || {} )[item.id]}`}
rightTitleStyle={styles.rightTitle}
rightSubtitleStyle={styles.rightSubtitle}
rightSubtitle={(this.props.unreadMessages || {} )[item.id] > 0 && "unread"}
chevron={true}
bottomDivider={true}
id={item.Name}
containerStyle={styles.listItemStyle}
/>
);
};
return (
<View style={styles.channelList}>
<FlatList
data={this.props.publicChannels}
renderItem={renderPublicChannels}
keyExtractor={(item, index) => index.toString()}
extraData={[this.props.publicChannels, this.props.unreadMessages]}
removeClippedSubviews={false}
/>
</View>
);
}
}
Object.assign will merge everything into the first object provided as an argument, and return the same object. In redux, you need to create a new object reference, otherwise change is not guaranteed to be be picked up. Use this
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});
// or
const um = {...state.unreadMessages, [action.info]: action.unreadMessages }
Object.assign() does not return a new object. Due to which in the reducer unreadMessages is pointing to the same object and the component is not getting rerendered.
Use this in your reducer
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});
The below observable creates an array of event objects.
eventsRef: AngularFireList<any>;
events: Observable<any>;
this.eventsRef = db.list('events');
this.events = this.eventsRef.snapshotChanges().map(changes => {
return changes.map(c => ({ key: c.payload.key, ...c.payload.val() }));
});
I need to add additional data to this.events from other database lists. So I need each event object to contain a guest count and data eventsFilters. I'm not sure how to do that. This is what I have so far:
this.events = this.eventsRef.snapshotChanges().map(changes => {
changes.map(data => {
console.log(data.payload.key)
this.db.object(`/eventsFilters/${data.payload.key}`)
.valueChanges()
.subscribe(data => {
console.log(data) //event filters
})
})
changes.map(data => {
console.log(data.payload.key)
this.db.object(`/eventsGuests/${data.payload.key}`)
.valueChanges()
.subscribe(data => {
let guestCount = Object.keys(data).length;
console.log(guestCount)
this.guestCount = guestCount; //guest count
})
})
return changes.map(c => ({ key: c.payload.key, ...c.payload.val() }));
});
Edit --------
I got this far using combineLatest but I'm still not sure how to group each event data.
this.eventsRef.snapshotChanges()
.switchMap(
(changes) => {
let userQueries: Observable<any>[] = [];
let lists: Array<string> = ['eventsFilters', 'eventsGuests'];
changes.map(data => {
for (let list of lists) {
userQueries.push(this.db.object(`/${list}/${data.payload.key}`).valueChanges());
}
})
userQueries.push(this.eventsRef.snapshotChanges());
return Observable.combineLatest(userQueries);
})
.subscribe((d) => {
console.log(d)
});
console.log(d) outputs something like this:
[
{}, //object with data from eventsFilters for first event
{}, //object with data from eventsGuests for first event
{}, //object with data from eventsFilters for second event
{}, //object with data from eventsGuests for second event
...
[{},{} ...] //array with all events
]
Here is an example combining 3 observable:
combinedData$ = combineLatest( entityList$, settings$, currentUser$).pipe(
map(([entityList, pageSetting, currentUser]) => {
//entityList, pageSettingand currentUser holds the last value emitted on each observables.
if (!pageSetting.ShowAllOrganisation) {
//If not showing all organisation, then we have to filter it
retVal = entityList.filter(entity=> entity.organisationId === currentUser.organisationId);
}
return retVal;
})
);
CombineLatest will return a new observable. If one of the 3 observable emmits a new value, the combineLatest will be triggered and emmits a new value. For more info on how combineLatest is working visit the official documentation
Using the IndexedDB API we have these 2 methods: getAll() and getAllKeys() with an usage example below:
let transaction = this.db.transaction(["table"]);
let object_store = transaction.objectStore("table");
request = object_store.getAll(); /* or getAllKeys() */
request.onerror = (event) => {
console.err("error fetching data");
};
request.onsuccess = (event) => {
console.log(request.result);
};
The problem is getAll() seems to retrieve only the data in an array format, and getAllKeys() gets all the keys without the data. I could not find a method to get both keys and values.
Isn't there a better way of getting the data and the keys with one call, like it is stored?
If not, is there a nicer way I could do this without making the code too confusing with multiple asynchronous calls happening?
I was able to retrieve all values with their keys with one callback function using an IDBCursor like this:
transaction = this.db.transaction(["table"]);
object_store = transaction.objectStore("table");
request = object_store.openCursor();
request.onerror = function(event) {
console.err("error fetching data");
};
request.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
let key = cursor.primaryKey;
let value = cursor.value;
console.log(key, value);
cursor.continue();
}
else {
// no more results
}
};
Alternatively you can use getAllKeys, followed by a transaction to fetch the values for each key.
const getAll = (db, store) => new Promise((res, rej) => {
// Fetch keys
const keysTr = db.transaction(store).objectStore(store).getAllKeys()
keysTr.onsuccess = (event) => {
const keys = event.target.result
if (keys?.length) {
// Start a new transaction for final result
const valuesTr = db.transaction(store)
const objStore = valuesTr.objectStore(store)
const result = [] // { key, value }[]
// Iterate over keys
keys.forEach(key => {
const tr = objStore.get(key)
tr.onsuccess = e => {
result.push({
key,
value: e.target.result
})
}
})
// Resolve `getAll` with final { key, value }[] result
valuesTr.oncomplete = (event) => {
res(result)
}
valuesTr.onerror = (event) => {
rej(event)
}
}
else
res([])
}
keysTr.onerror = (event) => {
rej(event)
}
})