In this chapter we're going to go ahead and implement the widget inbox so that we can look at all the previous conversations we had for an active session. Let's go ahead and start by adding conversations get many function. So just make sure that you're on your main branch as always. Make sure you're not developing on any other branch. And let's go inside of packages, backend, convex, public, conversations.
And in here, let's go ahead and see what we have. So we have the create mutation and we have get one. I believe those are the only two we have. Now let's go ahead and let's add export const getMany. This will be used to load our inbox component.
So let's go ahead and prepare the handler, and then we're going to go and add all the arguments needed. So the arguments needed to load conversations are the following. We need the current contact session ID, and we need the pagination options. And thankfully, Convex provides that for us using Convex Server Import. So just make sure that you have contact sessions here.
We already used it in the one below, get one. So you can just copy it from there. Now in here we can get the context and the arguments. And let's go ahead and do what we usually do. And that's just confirm if the contact session is valid or not.
So I'm just going to copy this and I'm going to paste it here. And I would like to call this just this this. Let me just so this this and this contact session Just because I want to be very specific. I don't like when it's called session because this is literally the contact session table. So if the contact session table is missing or if it expired, we are going to throw back the error.
And now what we have to do is we have to fetch all conversations by this contact session. And if you take a look at our schema, our conversations table actually has an index to help us with that exact thing. So let's go ahead and fetch all conversations by using await context.database and let's query the conversations using with index and simply pass by contact session ID. And then go ahead and grab the query here and let's do query equals contact session ID, arguments, contact session ID. As simple as that.
Let's order them by descending and let's paginate them using arguments pagination options. And this will automatically add the cursor and everything else we need. And now, again, be careful. This is a public, you can see we are writing this inside of the public folder, which means nothing by default. It's just the place where we chose to add functions which are available to anonymous users using our widget chat box.
So remember, conversations has context session id, right? And even though we absolutely do know it at this point, still I would prefer if we don't return the entire object inside and while we are here it would also be a good idea to load the last message from each conversation because look at how this is supposed to look like right we are supposed to know what is the last message is it this is it resolved right so we are supposed to know something and there are many ways we can capture the last message but the one we can do now is not exactly the greatest of ways but we are going to iterate over each of our conversation and find the last available message. So let's go ahead and do const conversations with last message await promise all. Inside of this promise all let's do conversations dot page dot map let's go ahead and open async conversation here like this and let's do let last message by default will be a type of message document from convex dev agent so just make sure you have imported message doc here like that or it will be null which is the default state if you're worried about using async inside of dot map don't be so yes you can't just normally use async inside of .map but if you add it inside of promise all it will work as intended.
So now let's go ahead and do let's just const messages to be await support agent dot list await support agent dot list messages context thread ID conversation thread ID pagination options number of items will be one and cursor will be null because we only want the last message nothing more than that and if messages.page.length is larger than 0 let's assign the last message to be messages page first in the array or fall back to null. Perfect. And now let's go ahead and let's return individual items here. So return underscore ID will be conversation ID. Creation time is another conversation field that exists.
So this creation time exists even if you don't add it in the schema. So convex internally keeps the creation time. Status, conversation.status, organization ID, conversation.organizationID, thread ID, conversation.threadID, and last message, well, just last message. And there we go. And now what we have to do is just find where this ends, this promise all right here, okay.
And let's return conversations entirely and then page conversations with last message like this perfect So that is our function that loads all conversations for this contact session ID and assigns the last message to each of those. So just you know, some of you might have already noticed, you know, this isn't the most optimized way to fetch the last message. Yes, absolutely. So for those of you who didn't notice, the problem with this is for each conversation, we are adding additional requests, right? What are the alternative ways?
Well, in alternative, we could have a separate request for all messages by passing the necessary conversation IDs. The problem is our messages appy at the moment works like this, and this is the only elegant solution that I could find. For the current state of our app, and I believe for even when it grows a little bit, this solution will be okay. But if you get to a point where you have hundreds of thousands of these, you would probably have to look into optimizing this a little bit. Honestly, this should never really be a problem, especially in the public conversations, because which session, which expires within 24 hours, will have hundreds of thousands of conversations for a single organization?
That's absurd, right? So I think in reality, this should work just fine majority of time. So just wanted to give you a quick note about that. Some of you are probably aware of its time complexity here. Great.
But now let's go ahead and actually use this. So what did we say we need to do next? After this, we said we have to do the widget inbox screen component. So let's go ahead and create that. So I'm going to go inside of my apps, widget, inside of my modules, widget.
And let me go inside of UI, inside of screens here. And I'm going to copy the error screen simply because it's the simplest one to adapt. So widget, inbox, screen. Widget, inbox, screen. Let's remove the error message like that.
And let's remove this inside and simply say inbox. Let's remove this and let's remove this and the item is centered thingy. There we go. Now let's go ahead and let's go inside of our widget view here. Let's find the inbox here and let's just do widget inbox screen.
Just make sure you have added the import for it. And now let's go inside of the selection screen again, because right now we actually have no way of looking at this screen. So let me just do turbo dev. There is no way for us to manually go in the inbox screen. So as always, localhost 3001 with a proper organization ID is required.
You can see, we don't really have the inbox button now. So that's because we are missing the widget footer. So let's go inside of the widget selection screen where I am right now and let's go, let's see, so widget header ends here and then this div opens. So right here let's add widget footer, which is a self-closing component. You can import it from forward slash components widget footer.
And there we go. Now, as you can see, you can click here and that should take you to the inbox. Oh, actually it doesn't do anything yet. So let's also enable the widget footer now. So let's go inside of this widget footer And let's go ahead and do const screen to be useAtomValue from Yotai, screenAtom.
And const setScreen useSetAtom, screenAtom, like this. So onClick here, setScreen, selection. And this will check if screen is selection, that's correct. This will check if screen is inbox and add text primary to it. And on click it will call a set screen inbox.
As simple as that. Now let's try it out. So when I click here, there we go. We are in our new inbox. The problem is we have to go back instead of the widget inbox screen, which we are currently developing here.
And let's immediately add the widget footer here like this. And now you can switch between the selection screen and the inbox component. Perfect. And now let's focus on the inbox component. So first let's modify the inbox screen header here.
So instead of this header, we're just going to slightly modify it. So this div will be flex and then let's just do items center and gap x of 2. Then let's go ahead and remove the content inside and let's add a button from workspace UI components button. Let's go ahead and give it arrow left icon from Lucid React as I've added right here. Let's go ahead and give this a variant of transparent size of icon and on select whoops on click.
Let's go ahead and let's call set screen. So const set screen use set atom screen atom. So make sure you've added those two imports. And let's call set screen. This will go back to selection like this.
So now there we go. We should have a button to go back from there as well. And outside of here let's just add a paragraph inbox. There we go. I think it looks a bit nicer and gives us more space here.
No need for the welcome message on the inbox, right? Welcome messages here for loading for out. But for this, you know, we need space. All right. So now let's go ahead and let's load our other atoms here.
So I'm going to grab my organization ID using use atom value. Organization ID atom. Then I'm going to get my contact session ID using useAtom value again. Contact session ID atom family and passing the organization ID, which I have to capitalize properly. Or empty string.
Still don't like this solution, but it's okay-ish for now. And then let's go ahead and let's fetch our conversations. Use paginated query from convex react. Let's go ahead and pass in API public convers... Whoops I didn't import API.
So API from workspace backend generated API dot public conversations and this should be get many. And then we have to check do we have the contact session id if we have it let's go ahead and let's add contact session id to be well the very same otherwise we skip this query And then in the third argument here, let's add the initial number of items to be 10. There we go. Now we have our conversations. And then Inside of here, we can already do the following.
We should be able to JSON stringify conversations. And now, there we go. You can see all of my previous conversations and you can also see that some messages are visible in here. Basically the last message for each of them. So now let's create the components to properly display them.
So let's first see do we have to modify something in the container. Flex, this is Flex1, FlexColumn, let's change this to be GapY2 like that and OverflowYAuto like so. Then inside of here, we're going to iterate over them. So conversations, question mark results, but length is larger than zero. And then let's do conversations.results.map.
And let's get the individual conversation in here. Let me just find a way to close this. In here, we're going to render a button like this. And it looks like there are no errors here. I'm not sure do I need okay so this can still be undefined at this point so let's add a question mark here and let's add a class name to this button to be height 20 full width and justify between.
Key will be conversation underscore ID. On click here, we're going to set screen to chat. And we also need to call const set conversation ID, use set atom, conversation ID atom. So make sure you have imported conversationIdAtom and the only one we don't need is the error message atom here. So now we can set the conversationIdAtom here.
Let's do it first. ConversationId. Like this. Perfect. Then now we can select previous conversations.
Let's give this button a variant of outline. And let's create a div here with a class name flex-full-width-flex-column-gap-for-overflow-hidden-text-start Then inside another div with a class name flex-full-width, items-center-justify-between, and gap-axe-2. Inside, let's add a paragraph which says chat and the class name text muted foreground and text extra small. Let's copy and paste that again and in here we're going to do format distance to now which we don't have the package for so let's quickly do pnpm f widget add date fns so in our widget component we need that all right turbo dev once again so format distance to now which you can import from date FNS. So let's just do that.
From date FNS, and let me remove the unused icon here. And inside of the format distance to now we're going to call new date conversation creation time. Let's refresh this briefly and let's go inside of the inbox again. There we go. Chat about one hour ago, about two hours ago, two hours ago.
Perfect. So now let's go outside of this div. Let's open a new one with a class name flex full with item center justify. Okay I can't write justify. There we go.
So justify between and gap x2. And inside let's add a paragraph conversation dot last message question mark dot text and give this a class name of truncate and text small. Let's see how this looks so far. There we go. Pretty good.
And you can see how the trunk it looks like it will basically not allow this to overflow and now we can select from previous chats as you can see right. So if I go ahead and click this one there we go you can see and now you actually saw the infinite load in action. I'm not sure if you noticed. Great. So let's go ahead and add one more thing here which is this.
This little icon which indicates what is the status of this current conversation. So in order to do that we need to develop a new component called conversation status icon. So let's go inside of packages, UI, source. Let's go inside of components and let's create conversation status icon dot TSX. And let's import all the icons we are going to need right arrow right arrow up and check icon then let's import our CNUtil and then let's create the interface conversation status props which accepts one prop status and it can be unresolved escalated or resolved and basically you get those values from your schema so make sure that they match this if you want to you can actually import doc from let's see workspace backend oh yes but we don't have backend added to the packages UI.
So I think it's just simpler to do this rather than risk some weird linking here by PNPM and Monorepo. So just make sure it matches right like this. Then let's do const status config like a little factory map. So If it's resolved, we're going to have the icon, check icon, and we're going to have the BG color to be background and let's do 3FB62F. Just some green color that I like.
And then let's do the same thing for the other two statuses. So for unresolved we're going to be using arrow right icon and background destructive and for escalated we're going to be using arrow up icon with VG color VG yellow 500. And let's go ahead and let's add as constant here. Const. I'm being super inconsistent here.
Here I'm using the hex code, here I'm using a variable and here I'm using Tailwind. So feel free to standardize this if you want. I just really like this green for some reason. Okay. Now let's export const conversation status icon.
And in here let's add conversation status icon props. Let's destructure that status. And inside of here let's simply return a div with a class name CN flex items center justify center rounded full padding 1.5 config background color. And I didn't define the config. So const config is going to be status config, status.
And const icon will be config.icon. Like this. All right. And inside let's just render the icon. So make sure it's icon with a capital I.
And let's do class name, size 3, stroke 3, text white. There we go. Now we can go back inside of our widget inbox screen. And after this paragraph, Let's add the conversation status icon component and give it a status of conversation.status. Let's go ahead and import conversation status icon.
So import conversation status icon from Workspace UI Source Components ConversationStatusIcon. And you can see I have no errors in this file, which means that the ConversationStatus matches exactly the types that I defined in this component, which means that now all of them look like this. Great, except I don't really like it. Something is weird. Let's see, what did I forget to do here?
So this should be a little bit bigger as you can see on my screen here. They are a little bit bigger. So maybe I have a typo. Maybe I did something correctly. Let's go inside of the conversation status icon and let's see did I forget to do something.
So size three stroke three text white Flex item center. Justify center rounded full. Padding 1.5. In the widget inbox screen, truncate text small. What if I added class name here which is not accepted so we could add it here I have an idea class name optional string let's grab the class name here no idea why this worked for me but not for you.
Let's add that class name here then. And how about I add shrink zero here? Okay, that does not seem to be fixing this problem. Okay, conversation status icon padding 1.5. Okay, let me just debug a little bit.
All right, so not 100% sure why it's behaving this way. You can try adding like padding two and then it seems to work. But you can see padding 2 is equal to 8 pixels, which means that if I add 7 pixels, it should be just slightly smaller, but you can see it's visibly smaller. So let's just try size 5 instead of padding. That seems to be okay.
Combined with our shrink zero, I think this is okay. Let me just see. Yeah, seems to be have okay. Let's leave it like that for now. Great, and now we can actually test out the infinite scroll from the previous chapter because we didn't have the chance to do it.
So find the conversation where you have a lot of messages. You can see how by default, All of them are loaded, right? So let's go ahead and go inside of our widget chat screen here and find the use thread messages, change the initial number of items to five, Use infinite scroll, change this to 5 too and let's just add observer enabled false. And then let's go ahead and refresh this and head into that conversation again. And you will see that by default it will only load five messages and you have to click to load more and then it will load five more and then five more and then five more until it reaches no more items So that's how it works and that's how you can prove it works.
You can now increase the number to 10 and we remove the observer enabled false. And now that entire thing will happen but automatically. You can see it automatically reaches the top if you are at the top. Great. Now that we know how our use infinite scroll works and we've just tested it within individual conversations.
Let's do the same for the inbox, right? Because this inbox already has pagination added thanks to convex's pagination ops validator. So let's go ahead and quickly do that. So I'm gonna go ahead inside of my, we can actually copy this. This is handy from the widget chat screen, go inside of widget inbox screen.
And after we load the conversations, simply add that hook like this. So use infinite scroll and let's import it. Make sure you have added the import And let's also import infinite scroll trigger. Instead of messages, this will be conversations. Just like that.
And now This, instead of rendering the infinite scroll trigger at the top, like we did in the messages, in here we have to render it at the bottom. So after this ends, infinite scroll trigger like this, And we can just pass all the props that we have. Can load more, is loading more, on load more, which is handle load more, and ref, which is top element ref, all of these extracted from the use infinite scroll. And to test this, let's change this to load only two both in the paginated query here and the use infinite scroll and add observer enabled to be false then let's go ahead and refresh Let's go inside of here and by default only two conversations are loaded until you click load more and then two more and then two more until it reaches the end. So it works here as well.
And you can see that if you change this to true then and refresh then and click here it will automatically do that as long as this is visible. So only when it reaches all the way down there, it's gonna be considered no longer visible and it won't load automatically. So now we can remove this, change the two to 10. It was that easy for us to add infinite scroll. That's why I wanted us to develop those components.
You can see how useful they are. Perfect. So we can now revisit our previous chats. We can start new chats. Let's go ahead and do Checkpoint.
Let's go back. Let's go here. And there we go. You can see at the top, this is the checkpoint one we created. Perfect.
Amazing, amazing job. Let's see if that was the goal. So we created the widget inbox screen. We created the conversation status icon component. That's it.
Perfect. So 16 widget inbox. Let's go ahead and let's merge this. I just remembered I think there is one thing instead of our Dice Bear avatar component from the last time. Avatar source doesn't have the image URL dependency, right?
So let's just add it here. That was the CodeRabbit's review suggestion from the previous chapter. So just do that as well. Perfect. So I'm gonna go ahead and stage all of my changes.
16 widget inbox, that's it. And then I'm going to create a new branch, 16 widget inbox, and I'm going to click publish branch. And as always, let's go ahead and let's review this pull request fairly simple one but still you can see how useful it is CodeRabbit always manages to find something that we missed. And here we have the summary. We introduced an inbox screen for the widget, allowing users to view and scroll through their conversations with support agents.
We added conversation status icons for clearer visual feedback on the conversation states. We implemented the infinite scrolling for the inbox conversation lists and conversations now display the most recent message and relative creation time. Exactly what we did. And here's the bug fix avatar images now update correctly when the image URL changes. So that was the bug from the previous chapter.
And I can't even believe it, but we did a perfect PR. No comments from CodeRabbit. We did a very good job and well, fair enough. It was a very simple PR. So let's go ahead and merge these changes.
Once you've merged them, make sure that you go back to your main branch here and synchronize the changes. So everything is up to date. And once you've synchronized your changes, as always, just double check your graph. Looks like mine like this. You can see how many things we did so far.
And every single time we check out, we merge, we check out, we merge, just like that. And just a periodic reminder, just try TurboBuild, you know, no matter if it fails or succeeds, just so you are aware. Is everything going well or are things starting to fail? Just so you have some sense of how many things you're gonna have to fix before you go into deploying this app. But you can see mine is building just fine so even if you have some issues they are most likely super simple type errors or lint errors which you can fix just by reading the description.
Great I believe that marks the end of this chapter So let's go ahead and see you in the next one. Amazing, amazing job.