In this chapter, we're going to develop the contact panel. Contact panel is part of the conversation ID layout, which we already almost finished at this point, but there is just one thing missing, and it's this exact contact panel that we're going to be developing in this chapter. So I have TurboDev running which means I have my web available at localhost 3000. So in here when I select a certain conversation you can see that this is pretty much finished, but there is one thing missing and that's this side panel here that shows me more information about this user. This will be quite easy to do as we already have all the necessary information in the user metadata.
So let's start by creating a new layout that will be able to hold that panel here. So I'm going to go inside of apps, web, app, dashboard, conversations, conversation id. And in here I'm going to create layout.tsx. Let's go ahead and let's simply do layout. Let's go ahead and render the children inside so we can give this a type of react react node.
And inside of here we can just render children like this. Make sure that you do a default export here and that the file is called layout. And if you've done this correctly nothing should change. You can see I can refresh this and everything is exactly the same as it was. So just make sure you've done this inside of the conversation ID folder.
And now what we're going to do is we're just going to go back inside of our modules and let's go inside of dashboard here UI layouts and let's create conversation ID layout dot the S X and in here let's go ahead and export const conversation ID layout like so and instead of our newly created layout let's just copy the props so we can have them here too and let's simply return a div and render the children. So now we no longer have to develop the layout in the app router, we can develop it here in the UI folder. I feel more comfortable doing that. So now I can remove the fragment and add the Conversation ID layout. Import.
Like that. Now I don't have to worry about designing here in the export default file, which is a reserved file name and it's used exclusively for routing inside of this app folder. Instead I can focus on my module UI conversation ID layout. I like this better. Let me just see what the error is here.
I think this is just TypeScript server which needs to be restarted. Let's see if I'm correct. I am. Great. Now it's time to actually develop this because still you can see some slight changes, a bug, but let's fix it now.
So we're going to need to add the resizable components here. So let's add the resizable handle panel and panel group from workspace UI components resizable. This is from Shazam UI and let's go ahead and replace this with resizable. I apologize. This is resizable panel group.
Let's give it a class name height full flex one and let's give it direction. Horizontal. Then let's go ahead and add a resizable panel here. Like this and render the children inside give this resizable panel a class name of heightfull and give it a default size of 60 And then inside of here render the children inside of a div and give this div a class name FlexHeightFullFlex1 and FlexColumn. And right now it should look exactly as it looked before.
And now let's add the resizable handle which is a self-closing tag and let's give it a class name of hidden. But on large let's display it. And now let's create another resizable panel here. And in here we're going to have our contact panel. So there we go.
This is the place where we're going to develop the user information. Now let's just give it some more info here. So default size is going to be 40, maximum size is going to be 40 as well. And minimum size will be 20. And in order to make this a little bit better if you want to you can add a class name hidden large block like this and then if you are on not exactly mobile but I'm kind of in tablet mode it will not appear.
Now I'm not sure if this is a good decision or not, but I feel like this panel right here isn't as crucial as this panel and this panel right here. Because the sidebar can be hidden away so that's fine. But this always needs to be here and this always needs to be here. But the contact panel doesn't. And if we're talking about actual mobile responsivity here, I've taken a look at Intercom and Crisp and Zendesk.
All of them simply tell you to download their app. Right. So it looks like no one is really bothering with making the desktop app too responsive. So I kind of feel this is an OK middle ground but just at least allowing tablet mode to be usable because if you don't do this and zoom in you can see it just becomes weird at this point. So yeah maybe add this maybe don't however you prefer just make sure that you are zoomed out enough so that you can see the contact panel for now when we are developing it.
All right, now that we have this, it's time for us to actually create the contact panel. So we're going to do that in here instead of dashboard, UI components and let's do contactpanel.tsx. Components and let's do contact panel dot TSX. Let's mark this as use client and let's export const contact contact panel. Inside of here let's simply return a div contact panel.
And now let's go back in here and let's import that. There we go. Just like that. It's a self-closing tag. Again, nothing much should really change here.
So now what I want us to do is actually, well, build the contact panel. So let's give this div a class name, flex-height-full, with full flex-col, bg-background, and text foreground. Full with full flex call bg background and text foreground inside let's add a new div with a class name flex flex column gap y4 and padding of 4 inside of here another div with a class name flex items center gap x of 2. And in here we need our dice bear avatar component. So make sure you add this import.
And now for the size, you're going to give it 42. But for the information such as badge image URL, and image URL, and seed, we actually have no information to fetch. So let's go ahead and see if we have the necessary APIs to fetch the contact. So the first thing we have to do here is we have to somehow pass the conversation ID from our layout here all the way to the contact panel. Now since this is a use client component we can actually do this in a different way rather than passing props.
We can leverage the params, use params hook from Next Navigation. And from here we can get the conversation ID using params.conversationID like that. And let's go ahead and try and simply logging this somewhere. So I'm just going to try and do conversation ID here. Now let's see if it works.
And there we go. You can see it right here. And it's the exact conversation ID that's selected. So you can see how when I change the conversation ID changes, right? Perfect.
Now that we have that, let's go ahead and let's see, do we have the necessary functions to fetch this contact. So I'm going to check inside of my packages here back end convex. This is private. And let's see it looks like we have nothing in regards to the contact sessions. So let's go ahead inside of the private here and let's create contact sessions.
Dot D.S. And inside of here I'm going to export const get one by conversation ID. Why by conversation ID? Because that's the only thing I have in this component. So It makes no sense to fetch this by the contact session ID because I don't have that, right?
So this will be a query accepting the arguments for conversation ID which is a type of v.id conversations, right? That's the one we have. Now let's go ahead and prepare this. Let's fix the typo asynchronous. Let's import the query from the generated server.
Let's get the context and the arguments. There we go. Now in here, first things first, let's check that we have the identity from context out get user identity. And let's replace this with convex error from convex values. And Let's go ahead and do what we usually do.
I like throw an object I think it's better so code is unauthorized and message can be unauthorized like this. And let me just copy this. Paste it here. So this will be unauthorized and message will be organization not found. So once we have confirmed that we are authorized to attempt to fetch this.
Let's check if this actually belongs to our organization ID. So let me check my schema here and let's see the contact sessions here. We have the, oh, looks like each contact session has the organization ID here. That's very interesting. So I just want to check something.
Contact sessions, I should have them inside of my convex public folder. So I'm just going to check how we create it so we pass the organization ID. Okay that's very interesting. That means that we should be able to fetch it with an index. Is that true?
I think it is. We have by organization ID here for the contact sessions. Perfect. So I'm going to go ahead inside of the private which we are developing here. Let's do const contact session And let's go ahead and do a weight context database query, contact sessions.
And just let me remind myself how to be right. Is it just with index? It is with index. It's going to be by organization ID. So let me just collapse these.
So query with index. Let's get the query. Query dot equals organization ID. And this will simply be organization ID like this. And dot unique because is it dot unique.
Actually yeah I'm not sure this can work like that Because you can have different contact sessions within an organization. Yes this this is not the way we should be doing this. Let me just think again. How about we go back inside of the contact sessions schema here. Let's go in here.
So we have by organization ID. Why do we not have. We should also have the by just check. So we can fetch the conversation and then we have the contact session ID. Okay, let's do that first since we are technically working with the conversation ID here.
Not the most elegant solution, but yeah, let's do it like that for now. Conversation will be await context.database.get and passing the arguments.conversationId. If there is no conversation, in that case I'm just going to throw an error here, not found, conversation not found. And then another important check, if conversation.organizationID is different from my current organization ID, in that case, I'm unauthorized to fetch this. So unauthorized invalid organization ID.
And now what we can do is we can fetch the contact session by doing await contacts database dot get conversation dot contact session ID and we can return the contact session. I think that should be just fine. Great. So this is now a private function which allows us to fetch the contact session by their conversation ID. And it's protected so Only certain members of the organization can fetch it.
Now let's go back instead of the contact panel. Let's get the contact session from use query from convex react API from workspace back and generated API dot private dot contact sessions get one by conversation ID and conversation. Let's pass in the conversation ID to be well the conversation ID from above and we can add as ID here from workspace back and generate a data model and pass in the conversations here like this. And Just in case, yeah, because this can technically be ID or in some... I don't see how it's possible for this to be null because it's used within a layout that needs to have it.
But just in case, we can check if we have conversation ID. In that case, let's do... Let me just fix this. Like this. So we're doing our usual check.
If we have conversation ID then use it. Otherwise skip. There we go. So even if it's now it will work. Now let's go ahead and let's do a simple loading method here.
So if contact session is undefined which basically means this is loading. What I think we should render... Let's simply render null. Let's see. Yeah, that looks fine.
No need to have a skeleton for every single part. I think this is okay because it will be loaded quite quickly. Now that we have the contact session, Let's go ahead and let's try and add some information here. So the seed here will be conversation. My apologies, contact session.
And let's pass in the underscore ID. Okay. Or if contact null. There we go. This way we can safely use it at this point.
So now we can pass the seed and the seed, the image will now match exactly the user. You can see. That's what we wanted to achieve. Great. Now for the image URL, actually we don't have anything to pass, but for the badge image URL we should be able to pass in the user's country info but in order to do that we first need to generate the user's country info.
So just above here let's do const country info And let's add that to use memo from react so make sure you import that. And inside of here, let's go ahead and let's return get country from time zone. This is the util that we developed in not sure which chapter but definitely when we wanted to display this flags here. So get country from time zone and in here simply pass the contact session question mark metadata question mark and time zone I think is the one we need. Yes.
And then in here go ahead and add the contact session metadata time zone. And now we have the country info here. Now that we have the country info we should be able to pass this in here. So let's go ahead and do country info question mark dot code. If it's available let's do get country get country flag URL so make sure to import that as well from the same lib and pass in the country info dot code in here.
Otherwise undefined. So get country flag URL is from the same util and now there we go you can see the flag for that user here So you know where they're from. Perfect. Now that we have that, let's go ahead and render some more user information here. So just below the Dice Bear avatar, I'm going to open a new div with a class name, Flex1 overflow hidden, div with the class name, flex items center, gap x of 2, an h4, contact session.name and the class name line clamp1.
There we go. Now we have the user's name here. Outside of this div let's add a paragraph rendering the user's email so contact session dot email. There we go. Let's go ahead and style this paragraph here by giving it line clamp one as well.
Text muted foreground and text small. Perfect. Now outside of this div right here where we used to render this let's add a button. So make sure to import the button from workspace UI components button and in here let's add link from next forward slash link and we're going to render a mail icon from Lucid React and with the text send email And we're going to give this an href of mail to and then simply contact session dot email. Let's give this a child prop Class name of full width and the size of large.
There we go. We now have a nice button which will send, which will actually open your email app and automatically append the to to be this email. So that's what this does. Great. Now that we have that, it's time for us to render the user's metadata.
So in order to do that, we first need to import all the accordion items. So accordion, accordion content item and trigger from Workspace UI components accordion. Now, we would need to add Bowser into our app here. So let's go ahead and the root of our app to PNPMF. This is web add and let's do Bowser here.
I'm going to show you the exact version and I'm using in case you want to be on the same page as me. But I'm not sure how often this version even changes. So let's see exactly what I added here. Package.jsonAppsWeb. Bowser 2.12.0.
That is the version I am using. Now that I have Bowser I can import that too. And that will allow me to parse the metadata in a kind of a more reliable way here. So let's prepare a few functions for that. Just above the country info I'm going to prepare const parse user agent which will be use memo.
And in here I'm going to return user agent question mark string. If there is no user agent I'm going to return browser unknown OS unknown device unknown. Otherwise I will attempt to get the user's browser by using browser.getParser user agent I will attempt to get the result from browser.getResult. Once I have that I can return back an object with a bunch of information. So you can of course limit how much information you actually want to show but I'm going to show you what you can extract from the user agent.
You can extract the browser using the result.browser.name or fall back to unknown. You can do the same thing for the device vendor and the device model. So the more information you have as a customer support agent, the easier it will be to debug what is actually happening to your user here. And now in here, let's go ahead and do one more function. So const userAgentInfo is another useMemo and this one will very simply call the parseUserAgent function from above and it's going to pass in the contact session dot metadata dot user agent.
And let me just add this. There we go. And this will also be in the dependency array here. So let me just collapse this here. So this and parse user agent function from above.
Like this. Just make sure to immediately return and call this function here. Now that we have the user agent info, we are kind of ready to start showing some information here. So let me just try and JSON stringify this for now. So outside of this div, this is the place in this new div where we're going to render all of that.
So let's try and stringify the user agent info. I think that's the one. That's the final one, is it? It is, I think. So let me try and refresh.
Yes, you have to refresh because we shut down our app to install Bowser. So it has to rebuild now. Let's give it a second. There we go. So browser, Chrome, browser version, OS, Mac OS, devices, desktop, device vendor is Apple.
Right. So some useful information about the user. Of course, when you actually deploy this to production, you will have to abide by the laws, right? You will have to check, are you allowed to fetch that or not, of course. But for educational purposes, I'm just teaching you how you can do this.
Great. Now that we have that, let's build a modular way for us to build these accordions so that they can just read from our metadata object and automatically populate the information inside when the user clicks. In order to do that, we're going to have to prepare some types first. So let's go ahead and let's add a type info item, which has a label, which is a required string, a value, which can be a string or a JSX or React node and an optional class name. And let's also add another type called info selection with an ID of string and an icon of react component type which helds a class name a title and items which is an array of info items from above.
And now that we have that let's go ahead and let's build accordion sections array. So just before you do any returns let's go ahead and do const accordion sections use memo info section like so there we go And now in here first let's do if there is no contact session. Dot metadata. We have nothing to return except an empty array. So let's return it like that.
Otherwise let's return an actual array. The first thing we'll do is give it an ID of device info and in here we're going to have an icon of monitor icon so all icons will be imported from Lucid React so make sure you add them. The title here will be device information and then the items inside will be another array. The first item will be the label browser and the value here will be useragentinfo.browser and now you just have to kind of append some strings to make it look better, right. So, plus and then open parenthesis user agent info dot browser version.
If the version exists, go ahead and append it like this. User agent info dot browser version. If it doesn't just use an empty string. After that let's add a label os value and then the same thing we're kind of appending the OS version if it exists so user agent info.os plus if the OS version exists append the OS version otherwise add an empty string you can also just do this right I'm just showing you a way that you can add a few more strings to here and you can display the OS version next to the OS if it exists. But we need to do it in this way.
I'm leaving space here in purpose so it shows like the OS then a space and then the OS version. But we also have to manually fall back to this so it doesn't actually display null or undefined in the string because that looks bad. That's why we are using ternary here, right? Same thing that we're doing here. I just collapsed it for readability here.
Okay, Now that I have at least something here I want to already start and attempt to render this just so we see what we're building. So in here I'm going to check if conversation my apologies if contact session dot metadata exists. In that case let's add accordion here. Let's give it class name full with rounded none border y. Let's give it a prop collapsible and the type of it can be multiple it can be single whatever you prefer.
And let me just see so collapsible does not exist. Maybe it needs to be single then. Yes. OK. And then let's do accordion sections dot map and then let's render a section individually.
So accordion item will be used to render the section and let's go ahead and give it a key of section.id. Let's give it the value of section.id And inside of here let's add a accordion trigger. And let's go ahead and render a div. Let's give this a class name. FlexItemsCenter and gap of 4.
Let's render a section.icon which is a self-closing tag. Give it a class name size for and shrink zero and then a span a rendering section dot title and give this a class name. Actually no need for any class name. Like this. So let's try it out.
There we go. Device information and when you click, looks like it's not really opening anything because we didn't develop the accordion content. So just below the accordion trigger add accordion content here. This will have a class name of px5 and py4, a div inside with a class name space y2 and text small and in here simply do section.items.map get the item render a div here and then render a span item.label and then add like a little colon here give this span a class name of TextMutedForeground give this div a class name of FlexJustifyBetween give it a key of section. OK let's do section dot ID dash section dot label.
So it's unique item dot label. Oops. And then in here another span rendering the item dot value. And the class name of item dot class name. There we go.
And now you can see the browser version and the OS as well as the version under the device information accordion. And now let's go ahead and just improve the styling of the accordion a bit. So we're only going to do this once and then we're just going to copy our accordions as we need them. So for the accordion item open a class name and add rounded none, outline none, has focus visible, Z10, has focus visible, border ring, has focus visible ring dash open square brackets three pixels has focus visible ring dash ring forward slash 50 now for the accordion trigger give it a class name of flex full width flex 1 items start justify between gap 4 rounded none bg accent px 5 py 4 text left font medium text small medium text small outline none transition or hover no dash underline disabled pointer events none disabled opacity 50. And I believe that's all we have to do.
There we go. This is the exact styling that we need. And now we can just copy and paste this. Actually no, we don't have to copy and paste anything. That's it.
We just have now populate our accordion sections to have more info. If you're satisfied with how this looks already you can just skip to the next chapter. But I will show you how you can add some more information here because I'm sure some of you do want to see that. So after OS let's go ahead and let's add label device. Let's go ahead and give this a value and then the same thing right So user agent info dot device plus user agent info dot device model.
And but let's collapse this like so. So if device model exists. Open backticks space dash space and then user agent info dot device model. Otherwise empty string. This is model.
There we go. And we can also use the class name prop here to for example capitalize this. And now you can see that the desktop shows capitalized like this whereas Mac OS doesn't write because it looks kind of weird if desktop is lowercase. Perfect Now let's go ahead and do some easier ones. So for example label screen value would be contact session dot metadata dot screen resolution.
Let's go ahead and copy this one. The next one would be viewport viewport size. Then we would have cookies Cookie enabled and it's actually better to just use a ternary here so if it's enabled show enabled otherwise show disabled. So now you have cookies viewport much more information here right. Now let's go ahead and add a whole new section here.
So let me just check. OK. So you have to go where the this device info object ends right here and add a new one. Give it an ID location info icon globe icon from Lucid React title location and language and language and give it items spread country info. Whoops.
Like that. If country info exists open an array. Otherwise it's going to be an empty array. So in here let's add label country and for the value let's go ahead and Let's render a span country info dot name and let's give this a class name flex. Actually we don't mean anything.
I think just this is enough. There we go. If you want you can also add some flag here right. And now let's also add a few more elements in here. So after this.
Label language. Value. Let's go ahead and use contact session.metadata.language. Let's copy this. Let's add time zone.
Let's copy this. And last one let's do UTC offset. Time zone offset. Let's see. Yeah, You can do it like that or maybe you can humanize it a bit.
Maybe open this. So this is minus divide by 60 and turn it into hours. Did I do this correctly? Let me just check. Contact section.
Okay, maybe we don't need this. No need to complicate this chapter any further. I think you get the point right. Basically all of the things that did they all just disappear now. How did I do that exactly.
Let me just debug What's going on here. Oh yeah. One important thing and why this is actually happening is because this is missing some dependency array here. So let's add contact session. Let's add user agent info.
And let's add country info. And there we go. Now you will always have some things here. So yes, you can now add in here pretty much everything you want from your user metadata in the exact same way that I just added here. So you have the platform, the vendors, screen resolution, time zone, refer current URL, and you can separate that into whatever section you want.
I don't think it makes much sense for you to watch me populate all of those fields for another 15 minutes because they're exactly the same as I just did this. I think this is good enough. It shows the most useful information where the user is from as well as their device information. And If you want to add even more fields you can of course add even more fields inside of these accordion sections. Perfect.
So now let's go ahead and merge this. So let's see. We created a new layout. We created a contact panel. Now let's commit the changes and let's merge this pull request.
So yes, you can see in here we have session details. So how would you do that? Well, the exact same way, right? You would just add another property here with an ID section details, the label would be section details, the icon would be clock icon and the items, let's make it an empty array like this. Do I need some items inside?
Let's see. Let's just add at least one so we have at least those three sections. So label session started, value new date, contact session dot underscore creation time to locale string. Let me just see if I did this correctly or not. It seems like I made a mistake somewhere.
Just a second. So this ends here. Location info ends here. And then a new one is started. Let's see what did I do wrong.
It's title not label. There we go. And now you have section details. Right. So you can add as many of these elements as you want here.
Perfect. Now that we have this let's go ahead and stage all of those changes. So I'm going to stage all changes. I'm going to add a message, 30 contact panel. I'm going to commit and I'm going to create a new branch 30 contact panel.
I'm going to publish branch and now let's open a pull request and let's review. So I'm creating a new pull request here and let's see all the changes we did. And here we have the summary. We introduced a resizable split view layout on conversation pages. We added a contact panel showing device, browser, OS, screen, viewport, location, language, timezone, UTC offset, as well as some session information.
All of this was done using our metadata and user agent string. In here we also have a sequence diagram explaining how our new function get one by conversation ID works as well as no actual comments other than some nitpick ones meaning we did a pretty good job with this one. So let's go ahead and merge this pull request and then let's go back inside of our IDE switch to our main branch and click synchronize changes. This way our main local branch is up to date with our main remote branch. As always, check the graph to confirm that we just merged in chapter 30 contact panel.
I believe that marks the end of this chapter. Amazing job and see you in the next one.