In this chapter, I want to continue our comments development, specifically focusing on adding infinite loading. Just a quick reminder that, at least in my case, it's been over 24 hours since I have uploaded my video on mux, which probably means that my assets have been deleted. Remember, if you're using free tier, if you haven't upgraded, if you don't have a credit card, your videos will be deleted after 24 hours. You can check that by going inside of your assets and selecting in YouTube development. You can see that everything in here is empty.
After that, go inside of your Drizzle Kit Studio, go inside of your videos here, and if there are any videos, go ahead and delete them. You can see that I have no videos. If you do, you can just mark this to select all of them and then delete them. And automatically, thanks to our foreign keys and cascading, all video reactions, video views and comments will also be deleted. So in order to continue developing, we now have to go and upload a new video simply so we can continue working on the comments.
So at this point, it doesn't matter if you are looking at your own video or someone else's video. So I will just go ahead and click create in this case, and I will upload a demo video here. It doesn't matter if you own the video or someone else. In our previous chapters, that was important because we wanted to demonstrate the subscription functionality. And here's the processing banner.
Yes, that's something we never saw until now. So there we go. We can see that now. And there we go. Here is the video.
Perfect. And zero comments. So what we want to do now is we want to implement proper infinite loading for these comments because right now that's not happening. Let's go inside of source, inside of modules, comments, server, procedures. Right now this is all we do inside of GetMany.
We pass in the video and we just load the comments, right? We don't specify how many, we don't specify anything other than this. All we do is we join the users. So let's start by modifying our input here besides accepting video ID to also accept a cursor which is going to be an object. It will accept an ID of the video, so UUID and also updated at.
We need updated at in case, we need updated at because we will be sorting by updated at, right? So that's how you need to build your cursor, which is something that we have to add here. So let's go ahead and immediately do that. So we don't forget. So we're going to add here in the end, order by descending from Drizzle ORM, comments updated at, like this.
Make sure you have imported descending from Drizzle ORM. Great, so in your cursor, you should now have the ID and the updated ad and the cursor itself should be nullish. So it's not required. And besides the cursor we should also have a limit with a minimum of one and the maximum of 100. There we go.
And basically now we will be doing similar thing as we do in videos procedures. Let me find getMany here. My apologies, I think it's Studio Procedures. Here we have getOne and we have getMany. So we have the cursor right here and let's go ahead and let's destructure the cursor and the limit.
So I'm just going to keep this open so we can easily refer to how we are building my apologies, not here, inside of the comments, get many. So we only added video ID here and now we have the cursor and the limit because we've added this too in the comments procedures. And now what we have to do is we have to modify our where query by copying this part right here. So this is something that we need. Let's go ahead and add that inside of where.
So we're going to wrap our where in an and like this. Let's collapse this. Let's import end from Drizzle ORM. So make sure you have this. So this will be the first thing we're going to query by.
So comments from this video. And then if we have the cursor, we're gonna go ahead and query by the cursor. So let's import or from drizzle ORM, larger than from drizzle ORM. And instead of using videos, we will be using comments like this. Now we also need to limit.
So let's go ahead and add limit, limit plus one, like this. We're using limit plus one so that we check if there's more data. I think we've added that comment here, right? Add one to the limit to check if there's more data. And I think it also makes sense to add descending by videos ID here as well.
My bulge is descending by comments ID as well. Because we are looking at less than, so that's descending, right? And now that we have this, let's check what we have to do. We have to, we can basically copy this entire thing. As more items, last item, next cursor and null.
So let's add that here. There we go. Let me just move that here. And we will now use the next cursor inside of here. So let's see how do we define that here.
So items is defined in this constant, let's confirm that it is items and next cursor like this. Items, next cursor. I think this should be enough for our infinite loading. Let me just double check here that we are not missing something. So I'm just comparing with my original source code because this can be a little bit tricky but I think everything's alright.
We have the cursor, we have the limit and we're using that cursor right here. Great, so let's confirm. So either larger than the comments updated at needs to be larger than cursor updated at and equals comments updated at cursor updated at larger than comments id cursor id otherwise undefined. I think this should work just fine. Now that we have this, let's go ahead and go inside of our app home videos video ID page.
Instead of calling prefetch, we will call prefetch infinite. We can now remove this to do here and besides passing video ID, we can pass the limit to be default limit from our constants. Let me just separate this. There we go. So we are now prefetching infinite.
Now let's go inside of a video view and let's go inside of the comments section here. And instead of using use suspense query, we should use use suspense infinite query. And besides passing video ID, we should pass the exact same limit default limit but that's not all let's go ahead and search for use suspense query we should be using it in the videos section in the studio we also need the second param So let's copy it from here and let's add it here. And it should work exactly the same. The issue is that we now have pages.
So what we have to do is we have to flatten them out. And we can visit the video section simply to remember how we do that. So we just have to copy this part. Pages, flat map, page, page.items, and then we map over individual video. So I'm gonna add that here.
Dot map, let's see. Comments, pages, flat map, page, page.items, and then we have the individual comment. Great! So what's missing now is the infinite scroll component. So let's go ahead and add infinite scroll like this.
Make sure you have imported the independent scroll from components. I'm going to move them here. And from here, you can also add the query. And then you can pass all the necessary props needed here. So those are going to be hasNextPage, isFetchingNextPage and fetchNextPage.
There we go. And to try it out, let's add is manual first, simply because I think it's easier to test whether this works or not. There we go. We have the, you've reached the end of the list, but let's try adding some comments. So this is test.
Another one. I'm trying to fill more than five comments basically because I think our default limit is five. If you have set the default limit to be higher, then you're going to have to fill that. So I have set it to five, which means that now when I refresh, I should see one, two, three, four, five, and then load more should load the previous ones. Excellent.
Of course, clicking on the user info will redirect you to A404. Perfect. So now, while we are here, since this chapter is quite short, let's use the time to implement this, the total comments information. So I think that we are missing some styles here. If I go inside of the comment section, we haven't added any styles for the heading element, so let's add text extra large and font bold like this.
There we go, zero comments. And now we need to find a way to count our comments And that shouldn't be too difficult. Let's go ahead and sort of get many and let's see how can we exactly do that. So in Drizzle, there is a count method. So technically I could add total and then just count from drizzle ORM, right?
I could import this and just add comments ID which will basically count all the comments. How did I get all of these errors just from this? Let me go ahead and go inside of Rizzle. Did I forget how to use count? Oh, my apologies it should be just count?
Oh, okay, It's because my cursor was left here. So, yes, I think that this will give you a total count of all the records, but the issue is this doesn't give us the entire amount of records, right? Because we are using a cursor now and we are using a limit. So what we have to do is we have to find a way to capture the total number of comments. And we can do that, for example, calling this total count and we could use database count comments and then equals comments video ID, video ID.
We could do that. But one thing that I don't like with this is that every single comment will have that. Right? So I'm not sure if we like or dislike that. So I'm gonna go ahead and remove this information which we don't need now.
Technically what we could do now is we could go inside of comments, pages, first page and then first item I think. Let's see. Pages, items, first one, and then total count. So technically, we could do this, but anyway, it's correct. It says seven comments, right?
Even though it only loaded five of them, but I really don't like the way we are accessing them. And I especially, I'm not sure what happens if the comments are deleted. So how about we try that? If I go inside of comments here and delete all records, what happens then? I think, there we go, we get an error.
So obviously, this doesn't work as expected. So what I did in my source code is a separate query, right? I know that I made a whole big deal about using a single query as much as possible and learning how to make optimized queries. But I think that total data, I mean, total count isn't really a big deal. So let's go ahead and do it that way.
I'm gonna go ahead and do const total data, and I'm gonna do await database select from comments where equals comments video ID, matches the video ID. And all I'm going to do here is I'm gonna add count and I'm gonna use the count here from Drizzle ORM. So if this works correctly, this should give me, make sure you've imported count, this should give me the number seven. So I will remove the total count from here and I will use total data here. And now it's all a matter of where I want to add this and I will just add it here total count will be total data dot count yeah I have to do this so let's just how I did this Or maybe this isn't the best way to do it.
Okay, let's try it like that. And now we should be able to just go to the first page maybe. There we go, total count. Let's see if we're going to get an error. So now we have no errors here.
And I think that if I try test it will update. There we go, second comment. I'm now purposely going to try and add them this way simply so we can also test if Infinite Loading is properly adding them. Let's add fourth, fifth comment. Let's try sixth comment and let's try seventh comment.
There we go. So now it starts with the latest one, the seventh comment, and if we load more is the second and the first comment. And it still says seven comments here. Perfect. So this basically achieved what we wanted.
The issue is... What is the issue? Oh, well, the issue is, yeah, that we can improve this a little bit in the get many here by at least making this to be together. So Let's go ahead and do it like this. Let's go ahead and define a promise all by having data and total data inside of an await promise all like this.
And then I can just query this as the first argument. So technically this will be total data and then this one will be data and then just query this entire thing after like this and remove it there we go So now these are available within the promise all. Let's see what my errors are. Total data.count doesn't exist. Yes, I think I will have to do it like this.
Total data, first element in the array, dot count and let's see, this will always be a number it seems so I think we can safely set it like this, I don't think we need to do anything else this should always give us some form of number, This should be indented and all of this should be indented as well. Yes, and I think that these don't have to be awaited anymore. Because we just need promises, right? We don't need the outweighted result. And I think that if you refresh, it should behave exactly the same.
Seventh comments, seventh, again, I clicked. So seventh comment, sixth comment, fifth, fourth, third comment. There we go. So what I wanna do now is two more things that we can do in this chapter because I wanna keep the replies and the comment reactions for their separate chapters. We can add the deletion of individual comment and we can also add the loading skeleton simply because the loading skeleton for the comment section is very very simple there is actually no skeleton at all what I decided to do here for the comment skeleton comments section skeleton is just a simple div with a class name of margin top of six flex justify center items center and loader to icon from Lucid React with a class name of text muted foreground size 7 and animate spin just make sure you've imported the loader to icon from Lucid React and make sure that you render this here and now when you refresh it should look nice like this I think this is the same thing that YouTube does.
They don't use the skeleton for the comments simply because it's too dynamic of a data, especially if you're logged out, if you're logged in, if there are likes, dislikes, right? So for that kind of dynamic data, I think the loader actually suits well. And sometimes too much skeleton is a thing. I think that's especially visible in our studio here. Like that's, I think this is too much of a skeleton actually, but it's okay.
We got a good exercise in building skeletons. Great, so let's go ahead and build our delete procedure now. This one should be quite simple. So similarly to our create one, let's copy the create one because they're similar. They're both mutations.
I'm not gonna call it delete, I'm gonna call it remove. And we will simply pass in the video ID. We don't need the value. And then in here, we're just going to do deleted comment, await database delete from comments, no values needed, just a where equals, actually we're gonna need and, and two equals. The first one will be if the comment's user ID matches the user ID and the second one will be if the comment's ID matches, actually we need just the ID of the comment, not the video.
This is not related to video in any way. So the ID of the comment here matches the ID. Which one you put First doesn't matter, but I think like the ID of the comment is more important than the user ID here, but both of them are important. And in case there is no deleted comment, I know this doesn't make sense, but yes, when you delete something, you will still get info about what you deleted. So if there's nothing here, that would mean that there was an error here because you should be able to see what you deleted.
Code not found. There we go. So that is our delete procedure. And now let's go inside of comment section here and let's go inside of the comment item. And I think we have, oh yes.
So we need to modify our comments, get many output here in order for this types to work because we just changed the entire thing to work on pagination. So how about we mark items here and then number and then hover over a comment and everything looks like it works well. Great! So now let's go ahead and let's add the remove method from tRPC, from the client. Make sure you've imported from the client like this.
TRPC.comments.remove.useMutation And let's go ahead and render this and we will render this inside of a drop-down menu. So let's go ahead and import everything we need from the drop-down menu. I'm gonna do that here. So that's the drop-down menu itself, the content, an item and the trigger. And then we have to find a place to render this.
So below this comment value, I'm going to add a comment to do reactions. And then outside of this div right here, let's add the drop-down menu like this. And inside a drop-down menu trigger, which will encapsulate our button component from components UI button. So just make sure you have added an import on the button as well. Let me just move it here.
The button itself will have a variant of ghost, a size of icon and a class name of size 8 and it will render a more vertical button from more vertical icon and the class name doesn't matter make sure this is as child so it becomes the button make sure you have imported the more vertical icon from Lucid React let me move that here And let's just check it out so it's in correct place. This is where I want it to be. And now we're going to add some content here. So drop down menu content, drop down menu item. And the first one will actually be message square icon from Lucid React and reply with the size of 4 and an on click of nothing but yes we will be able to trigger the reply from here or from the button reply here.
And let's also give the content an align of end simply because I think when I click, there we go, this looks a bit better than it being in the middle. And now we can copy this drop-down menu item here. And this one will be delete with trash to icon. Both from Lucid React. And we should now have the delete method.
Great! And how about we do this. Let's import a user ID from use out from clerk-next.js. So let me just move that here. There we go.
So now we have the user ID and then what we can do is we can hide this if we are not the author. So if comment user clerk ID is identical to user ID, only then show this drop-down menu item. Otherwise, no point in showing this to any other user since they will get an error anyways. So let's go ahead and there we go. Since I wrote these comments, I can see the delete button.
Great. Now let's go ahead and let's simply call remove mutate and pass in the ID to be comment.id. Now let's go ahead and modify the remove here so that it has proper on success and proper on error here. In order to do that let's prepare our toast from Sonar and I'm pretty sure we are going to need to add Clerk from UseClerk simply so we can trigger the... Let's go ahead and use Clerk so we can trigger the...
What's the correct... So we can trigger the pop-up model, the sign-in model in case the error code is unauthorized for whatever reason. First of all, in Success, let's do toast success, comment deleted and utils which we need to add here const utils is trpc use utils, comments get many, invalidate and pass in for which video ID which is comment video ID On error here let's check for the error First of all toast error something went wrong And then if error data code is unauthorized, we will call Clerk Open Sign In. There we go. So I think that now this should be working.
So my seventh comment is here. And if I click delete, there we go, six comments and this simply says sixth comment right here. Excellent! And I'm just going to try logging out and joining with another user so I can see if this works or not. So I logged in in my other account and another user made this comment.
And first of all, I can't even see the delete method, but just in case, let's try triggering the delete method just to confirm. Maybe we made some bug, maybe this won't fail. So if I click delete, there we go. Something went wrong. Basically, it was unable to delete that.
We got back 404, which is exactly what we throw back in the procedure here. Meaning that we will delete a comment where both of these arguments match. And that's not the case in this case, meaning with throwback an error you can't do that. Perfect! Means our code is working just fine and we cannot modify other users comments.
Perfect! And I think that is it for this chapter. So I'm going to go ahead and mark everything as complete. We've modified the comments, get many, we have prefetched, we used useSuspense infinite query and we added infinite loading. On top of that, we added proper comment count and we even added some drop-down options right here.
Here's another tip for you. Every time we open the drop-down, you can see that my scroll bar disappears and it kind of pushes the content ever so slightly. If that bothers you, here's a trick. You can go ahead and add model false to dropdown menu. And now it won't do that.
But I don't know what are the implications of setting the model to false. So yeah, use it at your own peril. But I've used it in my source code and I didn't encounter any issues. And you can do that for any drop-down you encounter. For example, here you can see how It visibly removes my scroll bar, but here it doesn't because of this prop right here.
So if you want to do that, you can add it. Great. Amazing, amazing job and see you in the next chapter.