Issue
ive got a user node with in my database that stores any info about the user. then in my app i have to image buttons
mlike = root.findViewById(R.id.btn_like);
mdislike = root.findViewById(R.id.btn_dislike);
mlike.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
likeStations();
}
});
mdislike.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dislikeStations();
}
});
private void dislikeStations(){
db = FirebaseDatabase.getInstance();
ref = db.getReference().child("Users").child(user);
DatabaseReference likesRef = FirebaseDatabase.getInstance().getReference().child("STATIONS").child(station);
likesRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
Boolean dislike_snapshot = dataSnapshot.child("downvote").getValue(Boolean.class);
likeref.child("downvote").setValue(+1)
}
}
then its pretty much the same thing for the likeStations() method. What id like to know is how to make sure they can only click the buttons once or if they click the like after clicking dislike it removes 1 from dislike before putting 1 on like
i know it will use the user node hence why ive added it to the question
}
Solution
It always starts with having the right data structure. If you want a user to be only allowed an action once, you need:
- To be able to identify the user, by signing them in with/to Firebase Authentication.
- A data structure that allows checking if they've taken the action before. That would typically be a map with the UID as the key, as that's the only way you can ensure uniqueness.
Since you want to track upvotes and downvotes separately, that'd be:
upvotes: {
"uid1": true,
"uid2": true
},
downvotes: {
"uid3": true
}
Now the rest of your requirements are a bunch of security rules on this structure.
For example, a user is allowed to cast a vote for themselves, which would look like this:
{
"rules": {
...
"upvotes": {
"$uid": {
".write": "auth.uid === $uid"
}
},
"downvotes": {
"$uid": {
".write": "auth.uid === $uid"
}
}
}
}
Next up, a user can only upvote or downvote, not both, which is something like this:
"upvotes": {
"$uid": {
".write": "auth.uid === $uid
&& !newData.parent().parent().child('downvotes').child(auth.uid).exists()"
}
},
And then of course the equivalent rule under downvotes
too.
With the above rules, a user can cast an upvote with this JSON patch (which you could build as a Map
in Android):
{
"upvotes/uid1": true
}
A downvote would be:
{
"downvotes/uid1": true
}
But that would get rejected if they'd already upvoted, so you can switch their upvote to a downvote by doing this (single) write operation:
{
"upvotes/uid1": null,
"downvotes/uid1": true
}
The null
here deletes their upvote, and the true
writes the downvote, both happening as a single operation.
Storing and securing the counters
Finally that gets us to the counting that you asked about, which is a separate topic from uniqueness. Storing the count is optional now, as the information is already present in the number of UIDs. But it is a good optimization as without a stored counter, users would have to download all UID values to count them.
So the data structure becomes:
upvotes: {
"uid1": true,
"uid2": true
},
upvoteCount: 2,
downvotes: {
"uid3": true
},
downvoteCount: 1
Now that you have the data structure and we've seen the transitions, it becomes a matter of ensuring that the change in count matches the vote being cast.
First example of that is the upvote snippet we saw above: you can increase the upvote count when you cast an upvote. In rules
"upvoteCount": {
".write": "(
newData.val() = data.val() + 1
newData.parent().parent().child('upvotes').child(auth.uid).exists()
&& !data.parent().parent().child('upvotes').child(auth.uid).exists()
)"
}
I hope this expression makes sense if you read the definition above it:
- the user can only increase the upvote count by 1,
- and only if they added their UID to the
upvotes
list, - and if it didn't exist there before
You'll similarly need to handle decreasing the upvote count if they remove their vote, and then also handle the downvotes counts. The rules for these are all similar, but separate.
Once you have that, the final step is to enhance the rules for upvotes
and downvotes
to also require that the user updates the count when they cast a vote. When a user casts a vote, they also have to update the count. So that'd be the inverse of the rules we just saw:
"upvotes": {
"$uid": {
".write": "auth.uid === $uid
&& newData.exists()
&& !data.exists()
&& newData.parent().parent().child('upvoteCount').val() ==
data.parent().parent().child('upvoteCount').val() + 1
}
},
Answered By - Frank van Puffelen
Answer Checked By - Timothy Miller (JavaFixing Admin)