In this chapter, we're going to create the dashboard inbox. This will be quite similar as to our previous chapter, widget inbox, but this will be used to display conversations for that specific organization on the operator side. So in the previous chapter, we made it so that the user who is attempting to contact support can see all of their previous conversations. And now we're going to create this. On the dashboard side, the operator will be able to see all the conversations made for asking for support.
So let's start by building the conversations layout first. And let's go ahead and do TurboDev. And let's go ahead and change this time to go to localhost 3000, basically our web dev task, because this is our dashboard. And the reason I'm telling you to open this is simply because we got so used to working with the widget. So in case you didn't forget, this is actually our main dashboard, right?
So we are now going to be developing the conversations part right here so we already have the conversations page if I'm correct if I click here we have empty conversations perfect and Now what we're going to do is we're going to go inside of apps inside of web modules and let's go inside of dashboard here and let's go inside of layouts and in here I'm going to create conversations. Whoops, conversations layout. The conversations layout will have all the imports from Workspace UI components resizable. So handle, panel, and panel group. And let's go ahead and create export const ConversationsLayout.
And in here, let's define the props. So they are only going to be children. So we can destructure them. There we go. And let's go ahead and Let's return resizable panel group.
Let's give this a class name of full height. Whoops, height 8 full, flex 1 and direction will be horizontal. Inside let's add a resizable panel. Let's give it the default size of 30, maximum size of 30 and the minimum size of 20. And inside let's go ahead and simply render conversations and just conversations, that's it.
Outside, let's add the resizable handle, which is a self-closing tag. And then let's open another resizable panel here. And in here, we're actually going to render the children and let's give this a class name of HeightFull and a default size of 70 just like this now that you have this set up Let's go ahead and let's actually render this layout. So we have to go inside of our web app, dashboard route group inside of conversations, go ahead and add layout. It's a reserved file name, just like page.
So make sure you don't misspell it. Let's import conversations layout from modules dashboard UI layouts and in here let's go ahead and render it so what we can do here is we can just copy this thing so we don't have to repeat it again. And let's go ahead and name this just normal layout and export default layout. And inside of here we are just going to return the conversations layout and children inside. That's it.
So now when you refresh here, you will see that you have this and you will be able as you can see to resize this. So this is where we're going to render all of our items here. And it seems like every single time I close this, it closes itself. Not sure why, because I think inside of dashboard layout, we do have a cookie here. So weird that that happens, but okay, it's not too much trouble.
But I will research as to why this is happening. But let's focus on the conversations now. So the problem is, well, there isn't a problem. Basically, this is where we're going to develop this. So how about we start with this little filter status at the top.
So instead of the conversations layout, we're actually going to render, instead of this paragraph, a component called Conversations Panel. So Conversations Panel. It's going to be a self-closing tag. And we're going to put it inside of Components here. So Conversations Panel.
And the conversations panel, let's just export it like this, is going to be the following. Let's go ahead and add a div. Let's go ahead and add a class name with the following class names. Flex, full height, full width, flex column, bg, background, text sidebar, foreground inside of this div let's add another div with a class name flex, flex column, gap 3.5, border bottom and padding of two. And now we have to import all components from workspace select.
So select content, item, trigger and value from workspace UI components select. And once you have those, you can go ahead and build the filter. So let's go ahead and add the select component here and let's give it some attributes. The default value is going to be all. On value change for now will just be an empty arrow function and value for now can be all.
Inside of select Let's go ahead and let's add select trigger and let's give it a class name. Height 8, border none, PX 1.5, shadow none, ring 0, hover BG accent, hover text accent foreground focus visible, ring 0 inside of the select trigger add the select value which is a self-closing tag and give it the placeholder of filter outside of the trigger add the select content inside add a select item and now let's go ahead and give this a value of all and inside let's add a div and a class name flex items center and gap 2 and inside of here let's add a list icon from lucid react Let's go ahead and give it a class name size 4. And let's add a span with text all. So just make sure you have imported list icon. And while we are here How about we add all the other icons we are going to need.
So arrow right icon, arrow up icon, check icon, corner up left icon. These are all we are going to need for now. And now basically what we ought to do is just copy and paste this a few times, but I don't want to do that before we actually render the conversations panel. So let's go back inside of the conversations layout, which is in the layouts folder. And let's just import our newly created panel from the parenting from the sibling components.
All right, so the problem is this has to be a client component. So let's go inside of the conversations panel and let's mark it as use client. And that should fix the issue. There we go. And now you have a little select button here.
So that's how it's going to look like. Now let's go ahead and let's add it some more values because right now we only have all. So I'm going to copy this select item and the next one will be unresolved. Let's use the arrow right icon and the text unresolved. Then let's go ahead and duplicate this again and let's call this escalated escalated and this will be arrow up icon let's duplicate this again and this will be the last one which is resolved and let's give this check icon so now you should have all unresolved, escalated, and resolved.
Great. So now that we have that, Let's go ahead and let's build the scroll area, which is going to display our conversations. So that's going to happen right outside of this div here. So let's add scroll area from workspace UI components, scroll area. Make sure you add that import.
And let's give this a class name maximum height calculate 100 VH minus 53 pixels make sure to not add any space here. So when you hover over it, it should display this. If it's not displaying anything when you hover, make sure you have the Tailwind CSS extension. It will help you a lot when writing Tailwind. So how do I know minus 53 pixels?
Well, basically I want the scroll to start if we fill up this container. But the problem is, you can see that this is 53 pixels high, right? So basically I just used inspect element. I found out it's 53 pixels height and it's fixed. It doesn't change its height.
So I'm just reducing that part because it doesn't know it's there. So now inside of here, let's add div class name, flex full width, flex one, flex column and text small. And now inside of here, we have to render our conversations But in order to render the conversations, we first have to develop the API to fetch them. And we already have something quite similar, right? Inside of our packages, back in convex, in the public folder, we have conversations.
So let's copy them entirely, and let's go inside of convex and create a new folder called private this time. So instead of private is what I'm going to well be building private API routes, something that should only be accessible for operators, people who are logged in, people who I know that they have in their clerk session, the organization and things like that. So a much more secure API routes and also more powerful API routes because these will be able to delete things, mark them as resolved, escalate them, those kinds of things, right? So I wanna make sure that I store them in a different folder as compared to the public folder. And this way I know, okay, so all of these are kind of public, I should be careful with them.
So in here, for example, I have to be careful with what I return. Remember, I can't just return the entire thing. I have to pick and choose what I return. Same with get one here. Well, in private, I won't have to worry about that because the user here is actually logged in.
They have a password, they have an email, they have a Gmail account, things like that. So let's go inside of private conversations, which we just copied here. And let's focus on the get many. So we are no longer going to have the contact session ID. That's no longer needed because the user is actually logged in now.
But what we will have now is the status, which will be optional and the union of three literals unresolved escalated and resolved basically the exact same thing as our schema make sure they match and it's the exact same thing we will allow the user to filter by from here so that's what it's referring to. So how do we validate inside of this now that we don't have contact session id? Well just by using normal authentication with convex and clerk using the context. So you can get the identity by awaiting context out getUserIdentity. And then if there is no or if identity is specifically null, throw new convex error here, code, unauthorized message, whatever you want, identity, not found.
Just like that. And now while we are here, we can also extract the organization ID. So how do you get the organization ID? Let's just double check that we actually have this. So head to your Clerk dashboard and in here, go inside of configure and go inside of JVT templates here.
Select the convex template, which we created in the beginning. So Clark works with convex. And in here, yeah, you can see that we have organization ID. If you don't have organization ID, I think we did this together. I'm 99% sure we did this together, but just in case you somehow missed it, it doesn't come by default here, right?
So basically you have to add like a new field and you have to call it something, and then you go ahead and choose organization, organization.id and you click save and then that will be stored in something. And then in your code, you will be able to access identity.something, right? So just do that for organization ID if you don't have it. All right, I'm just gonna reset these changes, I don't need them. Make sure you have organization ID here.
That's the important thing. Great. So now we can do this, and then we can go ahead and copy this again. If there is no organization ID basically let's go ahead and throw unauthorized again organization not found great and now we can go ahead and load the conversations but Let's do it a little bit differently. So let's define the conversations constant here and let's give it a type of pagination result from convex server, then document from generated data model and conversations.
So let me show you those imports. Document is imported from generated data model and pagination result type from convex server. Great, so now we have our conversations here already. And I'm gonna remove this simply because, Well, it's honestly easier to just build it from scratch. Let me return everything here.
Let's go ahead now. And let's check if we have arguments dot status. So if we pass the status, in that case, conversations will be the following await context dot database query conversations with index by status and organization ID. So that's why we have to do it this way to make it actually more optimized because we added an index called by status and organization ID. So this will be unbelievably faster than if you were to actually filter by status.
So basically just a small lesson in using convex the correct way. You could absolutely not use the index. You can just do a filter here and then request the query, query.equals and then I'm not even sure what's the ABI but if you can use the index, use the index. It is the correct and the faster way to do it. So let's go ahead here and let's pass the query and let's check if query.equals two things.
So the first thing we're going to check if it equals is if status equals arguments.status and it needs to be a type of unresolved let's just not misspell this unresolved or escalated or resolved And that's how you won't get the error. Basically, make sure you are not misspelling it and that it's the same thing as this. Maybe even the same order as this. And actually, you can also do as doc conversations Status. Yeah, that works too and it's even more precise.
And let's add another equals here, Organization ID matches the Organization ID from the user's session. So we know that the user actually has permission to load this conversations. Let's order by descending and let's also paginate this using arguments, pagination options. There we go. And let's also develop else, which will basically be a simpler query.
So conversations again, await context database, query conversations, But this time we're using a simpler index just using by organization ID. So if you want to you can explore if you can somehow conditionally choose between indexes, but honestly, this isn't too complicated, just an if else, it's fine this way if you ask me. Let's see your query equals organization ID, and let's pass in the org ID, and then we can just copy the order and the paginate. Let's just chain them. And there we go.
And now what I want to do is I want to develop const conversations with additional data. So what do we need to add here? Well, basically, similarly as to our conversations.ts inside of public, where we had to, let me just find get many, similar to this where we had to add last message. We're gonna do the same thing here, but also I want to populate the contact session information because remember in my schema, each conversation has its equivalent contact session. So we're going to now load the equivalent contact session populated, like give it the username or the email, right?
Whatever information we have about that contact session so that we can display it in a nicer way because think about it. In order to display their country of origin and their name, we need to load that conversation ID from the conversation table that we have it. So let's go ahead and do that now. Back inside of conversations, make sure you are developing this in the private folder here. And let's go ahead and do conversations.page.map.
Let's go ahead and mark this as an asynchronous method. And let's get the individual conversation here. Now inside of here, we're gonna do the following thing. First, let's do last message thing, which will be a type of message, whoops, message doc, or null. So I'm pretty sure this is the exact same code that we have here.
And now let's also prepare the contact session here to be await context.database.get conversation.contactSessionId. Now, if there is no contact session found, we can just return null and we don't even have to fetch the last message or anything because this is a completely invalid conversation. If we can't find the equivalent contact session ID, we can't even display this. So this is kind of like us building an inner join. If you're familiar with SQL, this would be the inner join.
We're only loading conversations which have their contact session ID relation and their last message relation. We are not loading any other ones. So right now we are technically kind of loading them but we are breaking that by returning null and then we'll filter it out later. So if there's no contact session return null similar to the inner join And then let's check the following. So const messages will be await support agent.listMessages, pass in the context, threadId, conversation.threadId, paginationOptions, same thing as in the public one, so number of items one, cursor, null.
That will load the last message. So if messages page length is larger than zero, it means there is a message that we found, let's assign the last message to be messages.page, first page or fallback to null. And that is, I believe, last message solved. So now from here we can go ahead and return conversation and let's just populate the last message and the contact session. Like this.
Not contact session ID. We should have the contact session entirely. So this variable. That's what we are doing. Perfect.
Now that we have this, let's go ahead and filter out any null values so after this promise all let's do const valid conversations conversations with additional data dot filter Let's go ahead and get the individual conversation and let's give it a check. So con is non nullable type of conv. Conv isn't null. Like this. And then we can safely return the conversations here and page valid conversations.
Just like that. Perfect. So I actually want to delete get one from here simply because it doesn't belong. And create won't even exist from the private one. At least for my needs, my dashboard doesn't need to be able to create conversations, right?
So only get many is the one I need from here so we can remove this, we can remove components, and for now we can remove the mutation. Maybe we will need it later. Excellent. Now that we have this, let's go ahead and let's add this to our conversations panel. So I'm gonna go inside of conversations panel.
And now let's go ahead and let's go up here and let's do const conversations, use paginated query from convex react. And in here pass API private, oops, I have to import API from workspace back end generated API and now let's go ahead and you can see how now we have private dot conversations dot get many. In case you don't have it, always double check that your workspace backend has successful functions. If there's an error here, it will probably be very descriptive and you will just have to go back in here and fix whatever is the error and save the file until it works. If it's very stubborn, maybe just try restarting the TurboDev thingy.
All right. So now that we have paginated query here, let's go ahead and check if we want to pass in any arguments. So for example, for status, let's go ahead and just pass, well, if we pass undefined, that will basically be served as all of them because inside of our private, you can see that arguments.status is completely optional. You don't have to pass it. And if there is no argument.status, we're just gonna use this, an index that loads all of them regardless of the status.
And let's go ahead and pass the initial number of items to be 10. Perfect. So now we have our conversations. And now if you want to, instead of here, let's go ahead and JSON stringify the conversations. And there we go.
All of our conversations here. Great. So now let's go ahead and let's actually display them in a nice way. So inside of here we're going to do conversations.map, myapologies.results.map, get the individual conversation, And let's go ahead and define some variables first. So let's check if the last message we received is from us or from the user.
So is last message from operator, that is us, that is the person looking at the dashboard. We're simply going to check the following. If conversation last message question mark message question mark role is not equal to user. And then let's go ahead and let's attempt to extract the country this user is from. So the way we can do this is by using get country.
Oops, get country from time zone. There are of course a billion ways you can do this, but this is just one of them that I found basically. If you remember in our schema, in the contact sessions metadata, we take in the time zone. So I found a little NPM package that can transform that into the country. So let's go ahead and install that package.
So PNPM, let's filter into our dashboard because that's the only place we're going to need this dashboard. And let's go ahead and simply add countries and time zones. No project matched in the filters. That's interesting. Okay.
My apologies. Web, not dashboard. Yes. My apologies. We, we, Hawaii web.
Well, because apps web, that's why. Okay. So now that we have this, let's go ahead and actually create this util instead of the web app. Let's go inside of lib here. Let's go ahead and create country utils.ts.
Let's import all as city from countries and time zones which we just installed and let's export function get country from time zone. Let's go ahead and accept the string here. If there is no time zone, well, we can't return anything, so let's just early return. Otherwise, let's get the time zone info by using ct get time zone, time zone. If there is no time zone info, question mark countries dot length, meaning if this array is empty, also nothing we can do, Break early.
Then let's do country code here. TimeZoneInfo.countries first in the array. Const country. Cd getCountry. And let's pass in the country code as string.
And let's go ahead and return the code, country code, and name, country, whoops, name or country code. There we go. So that's our simple get country from time zone util. Let's go ahead and import this from lib utils. So make sure you've added it.
So yes, we developed it instead of apps web and then in here in the lib folder. And now inside of here, we're gonna go ahead and pass conversation.contactSession.metadata which is completely optional, so time zone. Like this. And then let's do country flag URL. For now let's just use logo.svg, even though we don't have it here.
So let's quickly prepare that because we are going to need it. Instead of apps web, go ahead and open the public folder. Quickly go inside of widget public, copy logo.svg from here and then add it in web right here in the public. There probably is a way to share this across all apps, maybe by using the packages, UI public, maybe in the source, let's see. I don't know, Maybe there is a way to do it, but this is not complicated.
It works. It's fine. All right. So now, oops. Now I'm just using logo SVG.
We will develop the way to get the country later, But let's finally go ahead and let's return something here. Let's go ahead and import link from next link. So just make sure you've added link from next link. Make sure you didn't accidentally import from Lucid icons because that can happen. The href is actually going to be forward slash conversations and then conversation underscore ID.
So whenever a user clicks on one of these, that's where it's going to redirect. Now let's go ahead and let's add the key to be conversation underscore ID as well. Let's make the class name CN. So we're going to have to import CN. If you haven't, make sure you do.
So CN from workspace UI libutils. Now inside of here, let's go ahead and do relative, flex, cursor, pointer, items, start, gap three, border, bottom, padding four, py five, text small, leading, tight, hover, bg accent, hover, text accent, foreground. And then let's go ahead and let's check if path name, which we don't yet have, so let me just quickly add it. I always forget to add a path name. So in here, let's just do const path name, use path name from next navigation.
That's it. That's all we need. Let's go back to our check here. So if path name is identical to this, so If we are currently selected, in that case, let's just quickly add BG accent and let's add text accent foreground. Basically, we are doing it as if we were hovering over it.
So it kind of indicates, okay, this is selected. I think you can actually see that here. Let me just show you. You can see how this one is hovered. It's selected.
That's what that is. All right. Now, instead of this link, let's go ahead and let's render something. So let's add a div. Let's go ahead and give it a class name which is going to be dynamic.
So I'm going to add minus translate y one and a half absolute top one and a half left zero height left zero height 64% with one rounded R full BG neutral 300 opacity zero transition opacity and then simply copy the path name check again. In that case go ahead and add opacity 100. So what is this? This is super weird and also it's a self-closing tag. So what is this weird div?
It's not visible right now, but basically I'm not sure if you can see, but this little indicator that something is selected. That's what this is. You're gonna see it in a second. Now, outside of this, after this self-closing div, let's add the Dice Bear avatar component, which we developed in the previous chapter, previous chapters. So workspace UI components, Dice Bear avatar here.
Let's go ahead and let's pass the seed here to be conversation contact, contact session.__id, or you could have just used the contact session ID like that. And let's give it the size of 40. Let's give it class name, shrink zero. Let's refresh now. Oops, turbo dev.
Let's refresh and let's see if we can actually load something. And there we go. So for each contact session that you have, you should have a different glass icon. And when you hover, you can see how nice it looks. Great.
So now that we have this, let's go ahead and open a new div. Class name, flex1. Let's open a div. Class name. Flex full width items center and gap2 and then in here let's go ahead and render a span conversation.contactSession.name And let's give this a class name of Truncate and FontBold.
There we go. Now you should see the name of your contact session, whatever that was at the time of you creating it. Great. So let's go ahead and open this again. Class name, ML auto flex.
Actually it's just shrink zero and then text muted foreground and then text extra small. Format distance to now. We already used this somewhere, but I fear that it was in the widget, right? Which means that we have to install another package here. So let's do pnpmf web add datefns.
So basically filter to the web app and add datefns. And let's actually check the datefns version. So it's this one. So I just want to add the same one to avoid any conflicts between my packages here. There we go.
Looks like all is well. And now I should be able to import format distance to now from date FNS. Let me just do that here. From date FNS. Great and inside I'm going to pass conversation creation time.
So let's check that out. There we go. So about six, eight hours ago, one day, one day ago. Great. Now let's go outside of this span, outside of this div, and let's open a new div with a class name, margin top one.
So we separate the bit from the creation time above and let's do flex items center justify between and gap two inside of here let's add another div with a class name flex with zero grow items center and gap one and then let's check is last message from operator if it is we're going to add corner up left icon We're going to give it class name size three, shrink zero and text muted foreground. Basically, what is this? This will be a nice little indicator for us so that we know that we sent the message back. So when we are just looking at it like this, we know that we have responded to all of these conversations. Well, in our case, the AI responded, but still same thing.
Basically, the user wasn't ignored. The user received a response. And then let's add a span here. Conversation, last message, question mark text. There we go.
And now let's render it in a nice way, shall we? So let's give this a class name, CN, line clamp one, text muted foreground and text extra small. Already looking much better. And now let's go ahead and check if not is last message from operator. So if last message came to us and we didn't respond to it, let's go ahead and mark it as font bold and text black.
And just for fun, you can change this to true just to see how it's going to look like. So it's going to be looking like this when we didn't respond to the user. Basically as if we didn't even see that message. So it's gonna be much more visible like, hey, you didn't respond to this user. If this little indicator wasn't enough, it's also going to be very bold so you're going to see, okay, this user is waiting for your answer.
Excellent. And now outside of this div, let's go ahead and reuse our conversation status icon. We can import this. Let's go ahead and just do that here. So import conversation status icon from workspace UI components conversation status icon.
We developed this in the previous chapters. Let's add status here. Conversation dot status. And let's see. There we go.
Already looking much, much better. And now let's go ahead and let's try the following. Let's try and create the flag for each of our customer here because right now we're only using the country but we aren't actually generating the badge thingy so that we can display where the user is from. So Let's go inside of country utils where we develop the get country from time zone and just below let's do export function get country flag URL, country code string And let's go ahead and generate the country flag by using an amazing free service, flagcdn.com. Let's go ahead and define the width 40.
And then inside of here, let's pass in the country code to lowercase .png. Literally that simple to get access to all the flags in the world. So truly an amazing service. And now let's go ahead and go back inside of the conversations panel and let's actually use it. So we're going to check if we were able to obtain country code let's use the get country flag URL pass in the country code otherwise undefined.
So make sure you have imported the get country flag URL. And now let's go ahead and use the country flag URL constant just above here on the Dice Bear avatar. And let's pass in badge image URL, country flag URL. And if it's undefined, it simply won't be rendered. And there we go.
You can see that I'm from Croatia. So all of my requests here are made with the Croatian flag. Amazing. And when you click on it, I'm pretty sure it's gonna be 404, but still it basically works. It redirects you somewhere.
Amazing, amazing. So in here, it seems like we don't have the message, probably because we were just testing the conversations in the first place, but I am super happy with how this turned out. And now let's enable the filter because that's currently not working. So there's a very easy way we can do that by using local use state. But I imagine users wanting to use this, for example, they want to set this to escalated and they will pretty much use escalated all the time, right?
So I don't want them to have to go to their app every single time and change to escalated. So how about we use a package, a state management that we already have in our widget app, Yotai, and just use it here and use the atom with storage, which will automatically save this value in the local storage so the user doesn't have to track it. So let's go ahead and quickly check what version of Yotai are we using in the widget? So it's this version. Let's go ahead and do pnpm fweb add yotai.
Whoops, that's not how you add that. There we go, like that. And that should add the exact version I believe. There we go. Now let's go ahead and let's go inside of apps, web.
Instead of web, let's go ahead inside of dashboard module. Let's add constants.ts and I will export const. Let me just call this status filter key. CVA or actually call it echo status filter like this. And then let's go ahead again back inside of the dashboard here and let's create atoms dot DS.
Let me close everything else so we can actually see what I'm doing inside of apps, web modules, dashboard atoms. Let's import atom with storage from yotai utils. Let's import doc from workspace back in the generated data model and let's export const status filter atom, atom with storage and let's go ahead and give it the key of status filter key and the default value. So you can set this to be all. And now let's just make it properly typed.
So open these and you can go ahead and add document conversations status or all like that. Perfect. And now that you have this, let's go back inside of the conversations panel. Let's go ahead and add that atom here. So we are already familiar with this.
So const status filter here can be use atom value from Yotai React. And let's use status filter atom. And const set status filter use set atom status filter atom like this And then let's go ahead and give the value of this to be status filter. And on change, let's get the value here. And let's call set status filter and passing the value.
I'm just, yeah, it just won't be compatible here. Okay, so let's just do value as, and then pass all the possible options. Unresolved, escalated, resolved, and also all because we do have an item that is all. And inside of here, I mean inside of the status filter atom, we also allow all because remember, all isn't the status. So in our case, all means just load all of them.
And now that we have this, we can actually go ahead and modify the use paginated query here to check if we have the status. So let's go ahead and check. If status filter is equal to all, in that case, it will be undefined. Otherwise, status filter. Yeah, just pass status filter.
That's it. And I'm not sure if this will throw errors or not. So by default, when I hover over status filter, it could be unresolved, escalated, resolved, or all. And in here, if I check if it's all, it's undefined. Otherwise, Oh, this is super cool.
TypeScript is very smart. You can see, yeah, by default it includes all, but if it's in this conditional case, it automatically removed the all type. I am actually impressed by TypeScript. Very cool. So right now all of mine are unresolved.
So if I change this to escalated, it's empty. Same thing with resolve. Only unresolved should actually load something. So you can actually go inside of your conversations here, pick one and just change it to be resolved and change some other one to be escalated. And let me repeat this resolved.
And there we go. You can see how now you have more variety here. Amazing. Great. Perfect.
So let's go ahead and now add infinite loading here simply because it's super easy to do and we already have all the components that we need. So let's go back instead of the conversations panel and just below the use paginated query here Let's go ahead and let's add use infinite scroll Like this now we have to import use infinite scroll. So let's just quickly do that here at the top somewhere. So from workspace UI hooks use infinite scroll. And let's also add infinite scroll trigger from workspace UI components, infinite scroll trigger.
Great, so now that we have this, let's go ahead and do the following. So inside of here, we're going to add status to be conversations.status and conversations load more for load more and load size will be 10 same as my initial number of items. And then in here, let's go ahead and let's destructure all of these things. Top element ref handle load more, can load more, is loading more, and is loading first page. Great, And now let's go ahead and actually add the infinite scroll trigger.
So we're going to go all the way down. After we finish this iterating over the conversations, still inside of this div, Let's add the infinite scroll trigger. It's a self-closing tag. And let's simply go ahead and pass it. Can load more, is loading more, handle load more and top element ref.
And by default, you for a second just saw no more items, which will basically appear at the bottom here when you reach the end of the list. So as always, in order to test if this is working correctly, I recommend that you change the initial number of items and the load size to maybe like five and go ahead instead of the use infinite scroll and disable the observer. Refresh and there we go. So now you have to click load more to load the previous five which is basically what's done automatically by the default settings there we go amazing so I believe that's all that we wanted to do here what we can also do here is we can use the is loading first page to create a nice loading element here so let's go ahead and quickly do that so right here before we enter the scroll area let's check if it's loading first page loading otherwise render the entire scroll area like this and let's indent this thing So now while it's loading first page, you will see a brief loading element here. And now let's go ahead and let's actually build this loading element.
So I'm gonna go down here and I will do export skeleton conversations. Let's go ahead and let's return a div. And basically, this div here will be pretty much I think the exact same thing actually not exactly okay so let's give it a class name as the following flex minimum height 0 flex 1 flex column gap 2 and overflow auto Then inside let's create another div and let's go ahead and give it the following classes relative, flex, full width, minimum width of zero, flex column and padding 2. And then let's go ahead and let's open one more div which will actually be the container and now we're going to render the items inside. So width full, space y2 and text small.
We actually don't need the text because there won't be any text. Now in here let's go ahead and do array from length 8.map, skip the first element, get the index and let's return a div here and let's close it. And now in here let's go ahead and do class name, flex, items, start, gap3, rounded, large, padding 4 and last, border, bottom, 0. Actually just this is enough. Let's go ahead and give it key of index.
And inside let's add skeleton from workspace UI components skeletons. Just make sure you have added this import. And Let's go ahead and give this flexFullWidth itemsCenter and gapTo skeleton className, height4, width24 open this again, mlAuto height of 3 width 12 shrink 0. Like that and just one more. ClassName marginTop of 2, skeleton className height3 widthFull.
ClassName, margin top of two, skeleton, className, height three, width full. And let's use the skeleton conversations now here. So instead of this, let's just render skeleton conversations. And if you want to look at it more, you can set this to be true. And there we go.
I think this looks pretty cool. And you can change this now to be is loading first page. And there we go. Now you're going to have a very nice loading mode. Excellent.
Very, very cool. All right, let's go ahead and merge this. In the meantime, in between the chapters, I'm going to debug why this is happening. When we open it, it should stay opened. So kind of weird that that's happening but sure, we are going to fix it.
Great. So let's go ahead now and merge this. So let's see. We added this. We added this and this.
Great. And now let's go ahead and merge that. So 17 dashboard index. I'm going to stage all of my files. 17 dashboard index.
Let's commit. Let me open the new branch. 17 dashboard. What was it? Dashboard inbox.
Yeah. And I will publish the branch. And now we're going to go ahead and see if we made any security issues in our code or any bugs in general. And here we have the summary. We introduced the Conversations panel with infinite scrolling, filtering by status, and enhanced display including avatars, country flags, and message indicators.
We added a resizable dashboard layout with a dedicated conversations panel and content area. We implemented a status filter for conversations with persistent storage. We enabled paginated and filtered fetching of conversations including last message and contact details. Awesome! So as for the comments here we were actually quite good.
The first comment we get here and you can see how much research CodeRabbit actually does. So in here it actually compared my convex generated types and it found out that the message role can be user, assistant, tool, and system, which is true. But in here what I do is I just check if we are not user. And CodeRabbit says, you should be careful that, you know, you should check if it's assistant. Well, what CodeRabbit doesn't know is that technically, whether we responded as the operator or if our AI robot responded, both of that is considered the operator.
So even if the system or even if the tool responds, all of that is the operator because all of that is coming from our side, the dashboard side. So technically this is actually true. We should leave the code as it is because the only person who could be on the other side is the user. And in here it added potential time zone parsing errors. Yeah, we could do that for the country.
Yeah, I see. So I should check this outside of calling this function. Okay, yeah, I'll see. I'll make sure we do that in the next chapter here. But for now I think we're good.
Let's go ahead and merge this pull request right here. After we have merged it, let's go ahead and go inside of main And let's click synchronize changes either here or here in the lower corner you can do that. And once you have synchronized your changes, make sure that your... Oh, I just realized I said index not inbox. My bad.
Perfect. So I believe that marks the end of this chapter. Amazing job and see you in the next one.