0

Update: I altered the votes node of the database. It didn't seem to make sense the way I had it structured.

This is my database structure:

-threadsMeta
    -posts
        -postId


-votes
    -threadId
        -postId
            -uid: "down" //or "up"

The comments in the below code, I think, describe the intended behavior versus the actual behavior.

getMyUpVotes(threadId: string, uid: string): Observable<any> {
    //Get all the postId's for this thread
    let myUpVotes = this.af.database.list(`threadsMeta/${threadId}/posts`)
    .map(posts => {
        //Put each postId into a Firebase query path along with the uid from the method's params
        posts.forEach(post => {
            this.af.database.object(`votes/${threadId}/${post.$key}/upVotes/${uid}`)
            //Emit only upvotes from this user on this post
            .filter(data => data.$value === true)
        })
    })
    myUpVotes.subscribe(data => console.log(data)) //Undefined
    return myUpVotes
}

3 Answers 3

1

The following method would return the array of posts with an upvote + for the given thread + for the given user:

getMyUpVotes(threadId: string, uid: string): Observable<any> {
  return this.af.database.list(`threadsMeta/${threadId}/posts`).take(1)
    // Flatten the posts array to emit each post individually.
    .mergeMap(val => val)
    // Fetch the upvotes for the current user on the current post.
    .mergeMap(post =>
      this.af.database.object(`votes/${threadId}/${post.$key}/upVotes/${uid}`).take(1)
        // Only keep actual upvotes
        .filter(upvote => upvote.$value === true)
        // Convert the upvote to a post, since this is the final value to emit.
        .map(upvote => post)
    )
    // Gather all posts in a single array.
    .toArray();
}

I've added the .take(1) to force Firebase observables to complete so that the final results can be collected with toArray(). That also means that once you have fetched the upvotes, you stop watching for future value changes. Let me know if this is a problem.

IMPORTANT. You should subscribe to the observable from OUTSIDE of your method.

I have created a runnable version to illustrate (note that I'm using plain observables as neither Angular nor Firebase are available in this environment):

const getPostsForThread = (threadId) => {
  return Rx.Observable.of([
    { key: 'jcormtp', title: 'Some post', threadId: threadId },
    { key: 'qapod', title: 'Another post', threadId: threadId },
    { key: 'bvxspo', title: 'Yet another post', threadId: threadId }
  ]);
}

const getUpvotesPerPostPerUser = (postId, uid) => {
  return Rx.Observable.from([
    { postId: 'jcormtp', uid: 'bar', value: true },
    { postId: 'qapod', uid: 'bar', value: false },
    { postId: 'bvxspo', uid: 'bar', value: true }
  ]).filter(uv => uv.postId == postId && uv.uid == uid);
}

const getMyUpVotes = (threadId: string, uid: string): Rx.Observable<any> => {
  return getPostsForThread(threadId)
    // Flatten the posts array to emit each post individually.
    .mergeMap(val => val)
    // Fetch the upvotes for the current user on the current post.
    .mergeMap(post =>
      getUpvotesPerPostPerUser(post.key, uid)
        // Only keep actual upvotes
        .filter(upvote => upvote.value === true)
        // Convert the upvote to a post, since this is the final value to emit
        .map(upvote => post)
    )
    // Gather all posts in a single array
    .toArray();
}

// NB. subscribe() outside of the method
getMyUpVotes('foo', 'bar')
  .subscribe(val => console.log(val));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.1/Rx.min.js"></script>

Sign up to request clarification or add additional context in comments.

2 Comments

Hey, thanks for your answer. It is important that the resultant object/array is observable. The reason for this is that, like reddit or s.o., the upvote/downvote button will be highlighted according to whether the user has upvoted/downvoted. Have a look at my updated answer and tell me what you think of the solution I came up with. I ended up just 'joining' the query with the posts query. I'd love to know whether you foresee any issues with this implementation.
Good idea you had restructuring the model. Your code looks good. (As always with observables you could write the same thing using slight variations but as far as I can tell it wouldn't make a difference.)
0

I think you want:

let myUpVotes = this.af.database.list(`threadsMeta/${threadId}/posts`)
    .flatMap(posts => 
          Observable.from(posts)
              .flatMap(post=>this.af.database.object(
                   `votes/${threadId}/${post.$key}/upVotes/${uid}`));
myUpVotes = myUpVotes.flatMap(t=>t.filter(x=>x.uid === true));

9 Comments

Hey, thanks for your answer. I'm not interested in the emitted items from the first observable, except to plug the key of post into my Firebase path. I do not want these keys in the resultant observable.
Right, and I think it gets you what you're after.
Hmm. I get the error "this.af.database.list(...).flatMap is not a function". It appears that flatMap can't operate on the Firebase reference this way.
maybe... have you tried. import 'rxjs/add/operator/flatMap';
I have. This isn't the only issue. Sublime complains about Rx.Observable as well--specifically the Rx keyword. If I remove it Sublime doesn't recognize the $key property on post.
|
0

So I did come up with a solution that suited my particular situation well. I just don't know enough about observables to know whether it might be the best way to do this. Anyway, I ended up 'joining' the query with my posts query. This works well for my particular situation because I'm rendering the posts with ngFor. I'm able to check whether the current user has upvoted/downvoted the specific post with this:

<i [class.voted]="post.vote === 'down'" class="fa fa-caret-down" aria-hidden="true"></i>

Here's how I'm getting the data:

getPosts(threadId: string, uid: string): Observable<any> {
    let result = this.af.database.list(`/threads/${threadId}/posts`)
    .switchMap(posts => {
        let joinedObservables: any[] = [];
        posts.forEach(post => {
            joinedObservables.push(this.af.database
                .object(`votes/${threadId}/${post.$key}/${uid}`)
                .do(value => {
                    post.vote = value.$value
                })
            )
        })
        return Observable.combineLatest(joinedObservables, () => posts);

    });
    return result
}

In this implementation I keep getting the emissions from the votes, which is important, as user votes have to be reflected on the page not just on load but if they change their votes.

I'd like to know whether anyone foresees any issues with this. I'm not at all good with this technology, and I wonder if I'm missing anything. I'll give the others who have answered the opportunity to voice any objections and to edit their own answers if they want before choosing this as the answer.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.