{
  "video_id": "AbztO-X7PCQ",
  "title": "Build a Real-Time Slack Clone With Nextjs, React, Tailwind, Auth.js | Part 2/2 (2024)",
  "url": "https://www.youtube.com/watch?v=AbztO-X7PCQ",
  "transcript": "You are watching part 2 of Slack Clone here on YouTube. You can find the link to part 1 in the description. This is a quick reminder that you can also use my platform, CodeWithAntonia.com, and you can watch this entire course in chapters, and you can also mark your progress. You can also become a premium member, and you can unlock special courses like Canva Clone, as well as source code to over 20 premium projects and over 170 hours of premium content. Now let's go ahead and let's implement the emoji selection tool. But just before we do that, I want to quickly modify this text here below. I want it to show dynamically. So let's go back inside of our editor component here and let's find that text. It should be right here at the bottom. And the first thing I want to do is only display it if the variant is create. So I don't want to show this text later when we reuse this component to open the edit variant for a message. So only if we are creating like this. So now it should be shown because in the chat input, we did not specify the variant. But if you add the variant to be update, there we go. You can see how it's much different and there is no that text below. So let's go ahead and remove the variant. And now you should have the text appear here. And let's also add one more thing here. So I want to make this class name dynamic. So I'm going to wrap it inside of curly brackets here, and I'm going to introduce the CN wrap around that. So we should already have the CN library from cnlib utils. And this can stay as the default class. And we're going to go ahead and just add a opacity zero as the default. And we're going to add transition here as well. And then if not empty, we're going to go ahead and change the opacity to 100. So just make sure you have a little comma here at the end because this is like a new parameter here. So now you can see nothing is written here until I start typing, right? So only then those that appear the same time when our button is enabled. Great. So now that we have that resolved, let's go ahead and let's actually implement our emoji button here. So for that, we're going to need to install two packages. So let's write bun add at emoji dash mart slash data. That's the first one. So use npm install or bun add. And the second one is emoji dash mart slash react. So we need two packages like that. There we go. And now let's go ahead and let's implement a package called emoji popover. So I'm going to go ahead inside of the components here because it's going to be reusable. And let's call it emoji-popover.tsx like this. And inside of here, I'm going to go ahead and prepare everything we need from the popover component, which looks like we don't actually have. So let's first add it. So bunx, dash dash bun, chat CNUI, add latest, add popover. like this, or just a quick reminder, if you are using Node, npx chat CNUI at latest at popover, right? Great. So I now have the popover and there we go. I can now go inside of it and no more errors for my imports here. And besides this, we're also going to need the tooltip. And perhaps some of you already know this, we could technically use the hint component, right? Why not use the hint component? Well, unfortunately, we can combine the popover and the tooltip, but not exactly in a way that this hint component works. So this hint component allows us to pass children only inside of the tooltip trigger. But the solution we need is to have popover around the tooltip provider and then the content of the popover inside of the tooltip triggers. It's a pretty complicated solution. So it's best that we re-implement the entire thing here from scratch in the emoji popover component here. So make sure you've added these two imports. And let's add an interface, emoji popover props. And I'm going to add children to be react.react node. A hint will be an optional string and on emoji select we're going to get an emoji which will be a type of any and return back the void and let's go ahead and export const emoji popover here let's destructure the emoji popover props so children hint and on emoji select and inside of here what I want to do for now is just return a div and render the children inside so that's all I want for now now let's go back inside of the actual editor component here and now we have to use that emoji popover so let me just go ahead and import it so import emoji popover from dot slash emoji popover like this. And we are now going to wrap it around our button, which has the smile icon inside. So we are going to replace the hint wrapper with emoji popover wrapper like this. And let's go ahead and give the on emoji select here to for now just be an empty arrow function like this. So let's go ahead and go back inside of the emoji popover here. and let's go ahead and give the hint a default value of emoji. And then if we want to reuse this component somewhere else, we can modify the hint if we want it to say something more specific, like add a reaction, which is something we are probably going to do once we get a list of messages and we are going to want to reuse that component. And a better hint would be add a reaction. But for this place, the hint of text emoji is just fine. So great. We now have that. So let's go ahead and add two states, popover open, set popover open to be use state from React. And let's go ahead and put false as default. And then we're going to have tooltip open and set tooltip open, use state false like this. And now I'm going to go ahead and I'm going to wrap the entire thing inside of a tooltip provider. Inside of the tooltip provider, I'm going to add a popover. Let me just properly wrap the children. There we go. And the popper rule, we have an open value of popover open and on open change to be set popover open. Then we're going to add a tooltip component. Like this. And we're going to give the tooltip component a open of tooltip open. on open change set tooltip open and delay duration will be shortened to 50. Now we're going to add a popover trigger around the children. We're going to mark it as a child so it becomes the children we pass it but inside we also need to add a tooltip trigger. So this is what I was talking about, right? The composition of combining a popover with a tooltip is a little bit complicated. So that's why we could not reuse our hint component, because you can see that the structure is inherently different. So this one uses tooltip provider at the top. This one does as well. But then immediately inside of tooltip provider, we need to add the popover, right? So they work on in a different way. Great. We have that. And now in the tooltip content here, we're going to add a render our hint inside. So let's add a paragraph and hint. That's what the tooltip is for. And let's give the tooltip content a class name, background, black, text, white, border border dash white slash five. And let's give the hint text itself, font medium and text extra small. And then outside of the tooltip content and outside of the popover trigger, and more specifically outside of the tooltip entirely, we now add the popover content, right? So you can see that composing the combination of these two is quite confusing, right? And now we can finally add, let's go ahead and first just style it. So the popover content will have a class name of padding zero, pool width, border none, and shadow none. And then inside of here, we can add a picker component. So for that, let's go ahead and do the following. Import picker from at Emoji Mart slash React. And we also need the data from at Emoji Mart data. So we need these two right here. And let me just move React use state to the top like this. And now inside of here, we're going to go ahead and render our picker component with data as data and on emoji select. will for now just be an empty arrow function and we can actually log out what we select. And you can ignore this TypeScript error for now. And if we've set this up correctly and looks like I've messed somewhere up, let me just go ahead and see. Okay, so this is okay inside of the emoji popover, but probably inside of some trigger, I've messed something up. So let me see inside of the popover trigger here. So inside of the popover trigger, yes, this is the mistake I've made. The popover trigger should end right here. The popover trigger should only encapsulate the tooltip trigger like this. So I think that now if I refresh, there we go, no more errors, but I'm going to bring it back just so you can see what I've made. So in my previous example, my popover trigger was also encapsulating the tooltip content In order to fix that I have to move the popover trigger to only encapsulate the tooltip trigger right So the tooltip content and the popover trigger are on the same level They are both encapsulated by tooltip like this. So I will zoom out just in case you want to pause the screen and write it out like this. All right. So let's go ahead and try it out now. I'm going to go ahead and there we go. I can now select an emoji. I'm going to open my inspect element here. When I click on an emoji, there we go. We have a lot of information about this emoji and we're going to use this native right here to insert it into our text bar right here like that. So let's go ahead and let's implement the on select function here. Const on select will get the emoji, which is a type of any because unfortunately the emoji mark library at least what i've been able to find is not strictly typed so we don't have the exact type of that object but let's go ahead and do on emoji select emoji and then let's forcefully close the popover and let's also do set timeout set tooltip open. We have to time it out because unfortunately, otherwise it's kind of buggy, right? It will stay open, which is like completely okay. But you know, I kind of wanted to behave a little bit better than that. So let's replace the on select here like this. And now let's go inside of the editor and let's actually go ahead and implement the on emoji select method. So first of We no longer need this button to actually have an on click. So we can remove that from here. We just have to develop our own emoji select. So let's go ahead and do that. I'm going to go ahead and just below the toggle toolbar, I'm going to add const on emoji select, which will give you the emoji object, right? And inside of here, I'm going to get quill from quillref.current. and then I'm just going to do quill question mark insert text will question mark get selection question mark index or we fall back to zero and then I'm just going to add emoji.native inside make sure you don't misspell the native because we typed it as any so you know you won't get any errors so make sure you properly spell native which is basically what you see we remove the console log. Okay. Now let's go ahead and add the on emoji select to the actual emoji popover here. Like that. So now if you try and go ahead and add an emoji, there we go. An emoji has been added. And what's cool is that we've developed it so that it will always add it to the end of your line, right? So that's why we had to implement this. So we get the selection and we get the index, right? Like what is the last character's position? If we don't have an index, we need to fall back to a number. So we fall back to the first position, meaning, okay, maybe there is no selection at all. And I think we already do that somewhere else. Yes. In shift enter, when we insert the new line, we do the exact same method to get the new line always at the end of our current text. Great. So you've implemented the emoji tab right here. Excellent. The next thing we have to do is the image selection. Great, great job. Now let's implement the image functionality for our editor. So I want to go ahead inside of the editor component. And besides the text and set text, I'm going to add image and set image as well. Let's go ahead and add use state and give it the default value of null. Let's also define what it can accept. So it can be either a type of file or null. So this state here will be controlled to indicate to the user that an image has been uploaded, right? Well, technically not uploaded yet at this moment, but it has been selected, right? For that, we're going to be using state because state re-renders, right? But besides that, we also need an image element ref. So image element ref will be use ref and null. And it will otherwise also accept HTML input element. So we're going to use the image element ref to extract the image in use effect, for example, in here when we need to submit our form. So for that, we're going need to use an image element ref. So we don't have to add the image inside of the dependency array because then every time the user adds a new image, the entire editor will have to get reinitialized, which can cause some problems. So that's why we have to have two states, one in ref and one in image. Now let's go ahead and let's create a so-called phantom input, right? So this is what we're going to do. Inside of the flex call, we're going to add a self-closing input tag here. And we're going to go ahead and give it a type of file. We're going to give it an accept of image and an asterisk here, a ref of image element ref. And on change here will be an event, set image, event target dot files, and then the first one in the array. And you can put an exclamation point here to get rid of the TypeScript error. And then a class name hidden. Like that. There we go. So now let's go ahead and actually connect this to our image button. So we're going to go ahead and find the image button. It is right here. And this on click will do as following. Image element ref dot current click. So we are going to simulate clicking on this phantom input, which doesn't actually exist in the sense that it is not visible. So let's go ahead and click on image. And there we go. that should open up a new file explorer here. Great. So then after we select an image, we should also find a way to display that image. So we're going to do that just below our container ref here. So if we have an image for this, we use the image state, right? And let's go ahead and turn this into a boolean by adding two exclamation points at the top here. Let's go ahead and give this a padding of two. Let's go ahead and write another div inside with a class name relative, a size 62 pixels, flex items center, justify center, and group slash image. Why group slash image. Well, in case we have another group, and I believe we do have another group somewhere, or perhaps actually forgot to add a group here. But nevertheless, you can either use group, or you can specify the name of the group. So this is a Tailwind specific syntax, right? So this is group image, but usually you're also able to do just the group. But that becomes a problem if you have multiple group hover elements inside, right? But I think we actually don't have any. But still, better save than sorry. I'm going to call this group slash image like this. And you're going to see how we're going to use that in a moment here. So inside of here, we're going to go ahead and add a native HTML button element with a on click, which will do the following. it's going to call set image and set it to null and image element ref dot current dot value will be an empty string and add an exclamation point here like this and we are also going to give it a class name let's just prepare that for now so I want to add an x icon here basically we are doing the delete icon now so it removes the image we just added So make sure you have the X icon here and give the X icon a class name of size 3.5. So now if I add a random image here, let me just go ahead and add an image. There we go. You can see the little X button that I have here, but I still don't have the actual image. So let's go ahead and just style this X button first. So I'm going to give it hidden, but on the group hover slash image, it's going to become flex. It's going to be rounded full. It's going to have a background color of black with a 70% opacity. On hover, it will be black in its entirely. We're going to give it an absolute position. Minus top, minus 2.5. Minus right, minus 2.5. Text white. And then we're going to have size 6. Z index of 4. Border of 2. border white items center and justify center so a very long class name but this will make our x button look much better now there we go so you can see now when i hover in this well for now transparent image we have a nice delete button here only if you've added an image right And now let's go ahead and actually render that image. So I'm going to go ahead outside of the button here and I'm going to add an image from next slash image. So make sure you've imported this. Let me just go ahead and move it to the top like this. There we go. And we are now going to go ahead and give it a source of URL dot create object URL and passing our image from our state. an alt of uploaded and it's going to have a fill property and the class name will be rounded, extra large, overflow, hidden, border and object cover. And there we go. We have a nice gradient. So now you can go ahead and add an image, for example. And there we go. An image has been added here. And when you hover, you have this nice little button which will remove that image. And let just wrap it up by adding a hint around this button And let give the hint a label of whoops remove image So now when you hover, there we go. It says remove image. Perfect. So you just added the image tool. So what we're going to do next is finally call our submit methods. So we're going to implement this submit form here. But also we're going to implement this on click save and on click submit methods here. Great, great job. Now let's implement the submit functionality. So I'm going to go ahead and close everything besides the editor and our chat input inside of my channel, channel ID right here. So keep the chat input and the editor open. And let's go ahead and let's create a handle submit method, which will accept a body and image. And that will be a type body string and image file or null. And let's console log body and image here. Now let's go ahead and assign the handle submit right here in the on submit method. Let's go inside of the editor now. And the first thing I want to do is a very, very simple thing actually, which is find my own cancel, which I'm not using anywhere at all. So let's just go ahead and actually use this inside of our update variant. So let's just quickly find the variant update and we should have a cancel button. So we're just going to go ahead and call whatever function that may be that we are going to pass on later. And now let's go ahead and go inside of the create method here and let's actually call on submit properly. So go ahead and add on submit. and pass in body, which will be json.stringify, and then quillref.current?getContents. And then image, which we can get from our state. That's it. And now we have to copy this, and we have to add the same thing for our save method here like that. So basically in our database, we're going to be storing the contents, which are the type of delta and OP here. So that's how we are going to store that without having to sanitize HTML. And now we have to do the same thing in the options in the use effect here. So let's go ahead and prepare that. So it's going to be a little bit more complicated. it. First, we're going to go ahead and get the text version. So quill get text. Then we're going to add const added image here to be image element ref dot current question mark files. First in the array or no. Whoops. Like that. And then we're going to have a new is empty rule here because remember we can't use any other is empty. We have one is empty right here, but we can't use it, but we can reuse it. So let's just copy this here because we have to reuse this regex. That's why we are transforming it into text here so that we can use our is empty logic here. So our is empty is going to be very simple. If there is no added image, because if a user just adds an image, that will count as a non-empty submit, right? Because user will be able to just send an image and not type anything else. So if there is no image, and if we have this, a text without those new lines is empty, that means user truly has not typed anything, nor have they added an image. So we're gonna do if is empty, we're gonna do an early return. Otherwise, our body will be JSON stringify quill get contents. So inside of here, we don't have to call the quill ref because we have access to the actual quill, which is, I believe, just here below. So we can go ahead and pass it like that. And let's then call our submit function. But we are not calling on submit. We are calling submit ref dot current like that. So let's go ahead and call that conditionally here because the ref can technically be empty. And let's pass in the body and image will be added image. Make sure you don't do this because if you do this, then it will read the image from state. And then you have to add that to the dependency array. But we don't want that, right? So go ahead and add added image right here. There we go. So make sure you have the console log for your body and image here. Make sure you've handled the on submit when user presses enter. Make sure this has not been modified. And the only other thing we have is the on submit here. In here, we can use the quill ref. So if you're wondering why can we use the ref here, we can use it because this will be, what's the word, called, right? It's a method that will be called and returned back a result, right? So we can use that in this case, but we cannot use it and expect it to constantly update. So for example, I can't use it here in the disabled field, but I can use it on click because it's going to be clicked and then it's going to be executed. And then we're going to get the result. Whereas here, it will only be executed once and never again. That's why we have to use the state here. And let's also just do a quick more modification. So our is empty here should now be modified. This is empty, which is used, you know, to control the states here. So if there is no image and if the text replaced trim length is empty. And let me just go ahead and check some other quick things here. Do we have any leftover explicit falses? I don't think so. False. There we go. None. Perfect. So let's go ahead and try it out. I'm going to go ahead and open my inspect element here, and I'm going to write hello world, and I'm going to submit, and there we go. You can see my body here has this specific quill value, which we are going to store in the database, and then later we are going to render. And the image is null because the image has not been added. So if I go ahead, and this time I add an image, and if I submit, there we go. So now we have an image, which is a file. You can see my image from Unsplash right here. So what we have to do now is we have to implement a schema for messages. And we very first going to do a simple thing, which is just, you know, add a new body message with the body. And then we're going to do image upload with convex. So let's go ahead and let's revisit our schema so we can add the message. So I'm going to go ahead and close everything here and go inside of convex and inside of schema right here. And after channels, let's go ahead and add messages, which will be define table. The message is going to have body, which will be a type of string, image, which will be optional, and it will be a type of ID. And this is important, underscore storage. So this is internal ID. Make sure you don't misspell it, underscore storage. Then it's going to have a member ID, so the user who sent this message, but specifically member, right? We don't want to map our messages to users. We want to map it to members in channels. So this will be a VID of members. Besides that, it's going to have a workspace ID it was sent in. So this will be V.ID workspaces. And then it's going to have an optional channel ID. So this will be v.optional channels. So you might be wondering, oops, v.optional v.id channels. So you might be wondering how exactly can the channel ID be optional? We need to know where the message is. Yes, but remember, our message can also be sent in a direct one-on-one conversation, which is not a channel ID, right? so we're going to have an optional channel id but also a parent message id so optional and then v.id messages so it's going to refer to itself right because it can be a reply in a thread and we're also going to have updated at which will be again optional number like this or perhaps let's actually make it a default and required. And we're going to add a to-do here, add conversation ID as well, which right now does not exist, but we are going to have it later. And once you do that, just confirm inside of your Banex convex dev or npx convex dev, you get this message, convex functions ready, schema validation complete, make sure there are no errors. This means that you have successfully added your schema update. So what we have to do now is we have to develop the actual create method to send a message. So let's go ahead and do that. Let's go inside of convex and add messages.ts. Let's export const create to be a type of mutation. Let's prepare the arguments. And let's prepare an asynchronous handler here. So the arguments are going to be body. And we also need the values from convex values like this. So body image, which will be optional completely, which is an ID of underscore storage. Then we going to have a workspace ID which is required Then we going to have channel ID which is optional And it will be v.id channels. And then we're going to have another optional parent message ID, which will be messages. And I'm just going to add a to-do here, add conversation ID. because we don't have it at the moment. So in the handler, we can destructure the context and the arguments. Let's get the user ID using await alf from .slash alf, get user ID and pass in the context. So make sure you've added alf here. As always, if there is no user ID, we're going to throw new error because this is a mutation, unauthorized. Like that. There we go. And now it's time for us to find a member. So how about we create a little helper? Because inside of these messages, we're going to need to associate with the member a lot, especially when we implement the get method. So let's create a little helper here. Const get member will be an asynchronous method, which in the first argument will accept context, which is the query context from generated server. In the second argument, a workspace ID, which is a type of ID workspaces. And in the last argument, a user ID, which is a type of ID users. So let me go ahead and collapse all of these. Make sure you've added an import for the ID module. So what we're going to do here is very simple. return context dot database query by members with index our familiar index by workspace id and by user id so we're going to query workspace id by workspace id and we're going to chain user id and user id like this there we go and of course ensure that we return a unique result here So now we have this little helper. So let's go ahead and go back inside of our create mutation here. And let's go ahead and get the member by passing away to get member, context, arguments, workspace ID, and our user ID. If there is no member, throw new error, unauthorized. and now let's go ahead and let's insert the message so const data will be await the context database insert messages member id will be our member underscore id body will be arguments body image will be arguments image channel id will be arguments channel id workspace id will be arguments workspace ID. And parent message ID will be arguments parent message ID. And let me just see whether I did something wrong here. Is there something missing inside of here? So I'm inserting inside of messages. And did I add the member ID here? I do have a member ID. So this should work, but it is throwing me an error. So let me just debug for a second. Well, I am missing the updated at property, which I have defined here to be defined. Yes. So update ID, updated at will be new date. And this is not correct. So let me just quickly see. instead of new date let's use date dot now there we go and we have the data we don't have to actually do anything with it what i just want to do now is well not data but we have message id so let's go ahead and return that as usual return message id there we go so i'm just going to go ahead and do one thing. I'm going to add a comment here, handle conversation ID. So there is a possibility that conversation ID should be used for this type of API call. But even though we will later add it as arguments, we won't actually pass it. It's an edge case, which happens when we reply to a thread inside of a one-on-one conversation. So we're going to have to take care of that so we can extract the conversation ID from previous messages and handle that. You're going to see exactly what I'm talking about when we get to that. But I just want to add an important to do here because we do need to handle that edge case. But for now, you should be able to send messages. So let's go ahead and turn this into a hook. So I'm going to go inside of source features and I'm going to create a new messages folder. And inside, I'm going to create an API folder. And inside of here, I'm going to create a use create message dot TS. And let's go ahead and, for example, copy use create workspace. We can copy the entire thing and paste it inside of use create message here. Let's rename it to use create message. and let's go ahead and modify the mutation to be api messages.create. And now we have to modify the request type. So this will have a body, which is a string. It will have an image, which will be a string or null. I think workspace ID, which is also a string, or actually we can use the proper types. So the image will be an ID of storage. Workspace ID will be ID of workspaces. But I still think that, well, we did not exactly handle it entirely. Let me just see. So the response, it doesn't match. Messages is not assignable to type workspaces here. So API messages create. we request optional image, optional channel ID, parent message ID, so workspace ID. Did I pass that in? Workspace ID is ID of workspaces. Okay. Besides this, we also need the channel ID, which is ID of workspaces. Parent message ID, which is ID of messages. and to do add conversation ID as well. So in channel ID, I meant to write channels here. And what we have to modify is that parent ID and channel ID are optional. And also the response type here should be messages. Oh, looks like that resolved the entire thing, right? So we don't have to explicitly write this to be optional, I believe. Let's see. Yes, I think if we actually know this is required, right? Oh, but we probably should do this. So from our side, this hook doesn't cause any problems, right? So the image should be optional. The channel ID should be optional. And the parent message ID should be optional like that. so we can move the required ones first here and leave the to do here and just ensure that there are no errors you know inside of this hook here great and now that we have this we can go back inside of our chat input component so let's go ahead and go inside of source app folder workspace workspace id chat input here and i'm going to go ahead and i'm going to add our use create message hook from features API use create message inside of here I'm going to extract mutate which we can call create message like this and now inside of here what we can do for now is extract our workspace ID, use workspace ID, get our channel ID, use channel ID. So make sure you've added these two hooks, use workspace ID and use channel ID. And instead of, we can still leave console log if you want to just for this part, so it's easier to debug. And then we can go ahead and add create message. We can pass in the workspace ID, the channel ID, the body, and that should be it. We can't pass the image just yet because image first needs to be uploaded. Otherwise, we're going to get an error. Great. So now what I want to do is I want to refresh the entire thing. And I also want to prepare my database. So inside of here, I now have the messages, but my messages are completely empty. So I'm going to go ahead and attempt to send a hello world message. And let me go ahead and make sure that I can also see my console log here. There we go. So my console log is here. It should be my body should look like this, right? So if I go inside of data here, there we go. This is exactly how my body looks like here. And let's see which channel I did. This is associated to my new channel. Let's see if that's true. That's true. It's the new channel. Perfect. And let's see what workspace ID this is associated to. GitHub workspace, which is exactly the one we are in. GitHub workspace. And last thing should be to confirm that this is sent by this member. So let's go ahead and confirm quickly that. And we should, it's an admin. So I already know it's the correct user. And this should basically, well, I can't find the click. Is it this one? There we go. So this is my GitHub user. Perfect. So we now know that we can send messages. So what I want to do now is that every time we send a message, I want to clear this input so it's ready for a new message. So we can do that.\nwith kind of a dirty trick. So the reason I want to do it like that is because, let me just show you what I have in mind. So I'm going to add a state here, which will be called editor key and set editor key. And it's going to start at zero. And let's add use state from react here. And then what we're going to do is every time create message is finished, we're going to go ahead and call setMessage, get the previous key and do previous key plus one. Not setMessage, my apologies, setEditorKey. And then we're going to go ahead and pass in the key here as EditorKey. So every time the key changes, the editor component will be destroyed and rebuilt again. And this way, the entire state will be reset. So if you try and click test, there we go. The entire thing is now reset. So that's simply easier to do than to, you know, we can technically use the editor ref here. So we could use editor ref current set contents, right? And then inside of here, we could set an empty array. So if I try that, that is also a solution. So if I write test, there we go. You can see that's also a good solution. The problem arises when we add an image, for example. So when we do this, one clears and the other doesn't clear. Now, we could technically fix that as well by, you know, adding image element ref. We could do that and then somehow capture inside of our editor when we assign the image element ref right here. So I believe we could kind of turn this into a method. And then basically, you see what we have to do, right? In my opinion, we can just use this trick, right? So we're just going to force the entire editor to re-render by changing the key. like that. So for now, I'm not going to use any of the callbacks inside of here. Instead, we're going to go ahead and do something a little bit differently. So inside of here, I'm going to add an option, throw error to be true. So this will now throw an error, which means that I can go ahead and mark the entire handle submit method as an asynchronous. I can remove this console log and I can also wrap the entire thing inside of try and catch now like this. And I can put a wait here. And let's also add a catch here like this. Let me just indent this. This will be an error here. And let's also add finally here. So the reason I'm doing this is because we're going to have to, for the image, we're going to have to do some fetch API calls inside of here and as well as some other mutations. And it's just simpler to chain them inside of try and catch rather than those, you know, on success here. So then we would do one thing. And then we would have to do, you know, inside of another mutation here, we would have to do on success here, another thing and do that multiple times, right? So it's just not a nice syntax. So thankfully, our use create message hook has the throw error option. So we can safely do this inside of try and catch here. So now set editor key will only be called after this is successfully called, right? So while we are here, let's also do this. Let's add pending, set is pending, and let's call this is pending. So we are not going to have the is pending this time from our mutation because we're going to have lots of mutations that have to be called. So it's just easier to handle it like this. So inside of the try block, let's set is pending to true. And only in finally, set is pending will come to false, like so. And let's also go ahead and do a, so my apologies, inside of the catch, we're just going to do toast.error failed to send message. and import toast from Sonar. And now we can use this is pending here from chat input and disable the editor if we are pending like so. So let's see if our editor will be disabled or not. So hello world. And I think for a split second, it actually is disabled, especially these buttons here. but I think we can make this even better before we wrap up this chapter. So inside of the editor, what I want to do is I want to find my main div here, which encapsulates this container ref, right? And let's go ahead and wrap this inside of curly brackets because this very long class name will now become dynamic. So when we are disabled, we're going to go ahead and add some opacity. So let's wrap this inside of CN library, which I believe we already have inside of here. We do. Great. In case you don't, CN is from LibUtils here. So this will be our first class name as in our default class name. Make sure you add a comma at the end here. And then we're going to check if disabled opacity 50. And make sure you put this like that. So I'm going to refresh the entire thing. And I'm going to write hello. And there we go. You can see how now for a second, the entire thing is grayed out. Excellent. So what we're going to do in the next chapter is actually give us the ability to submit something with an image. Because right now we are not doing anything with the image, right? We just have it here, but we are not assigning it here. That's because we first have to upload it. So that's what we're going to do. Next, and then we're going to work on displaying our messages. Great, great job. Now let's implement image upload. So let's go ahead and let's create a new endpoint inside of convex. We're going to call this file upload. Inside of here, we're going to have a single mutation. So let's first import mutation from generated server. And then let's export const generate upload URL, which will be a mutation, which will very simply accept a handler inside. And it will return await context storage generate upload URL. That's it. This is our entire API endpoint. Now let's go ahead and create a hook for this. So I'm going to go ahead inside of my source folder features and I will create a new folder called upload. Inside of here, I'm going to create our API folder and I will copy a random mutation. For example, use create workspace. I'm going to copy it and paste it inside of upload. I'm going to rename this to use generate upload URL. So pretty descriptive. Make sure you are inside of the use generate upload URL and rename the hook. So use generate upload URL like this. Now the request type will not exist like that. So let's go ahead and remove that immediately. So the values in this case will just be an empty object and they will not be required at all like this. Now let's modify this mutation to be to API slash upload slash generate upload URL like this. And inside of here, we can completely remove the values. We don't need to pass them at all. And the response here seems to be, let's just go ahead inside of the generate upload URL. So this is returning. Do we actually care about what it returns? All right, this is what we're going to do. We're going to go ahead and remove the response type here completely and just use any. So inside of here, we're also going to add any. we can remove this import and I believe then this will be okay there we go so it doesn't really matter because we're not gonna use it besides or actually I think that the response type can be a string so let's try that what if the response type is just a string will that work So string or null. There we go. That seems to resolve the issue here. So the response is a type of string. And in here, the response is a string. Perfect. So I don't know why in here. Okay, it does have a string inside of a promise. My apologies. Okay, great. So our response type is string or null. We don't need this import. And we can keep the response type here, the response type here as well. Perfect. Now that we have this and you've ensured that there are no errors inside, let's go back inside of our app folder, workspace, workspace ID, channel, channel ID, chat input. And in here, I'm going to go ahead and I'm going to add use generate upload URL from features upload API use generate upload URL. I'm going to add a mutate here, generate, upload URL, like that. And now we're going to go ahead and do the following. Let's create a type here, which we're going to call create message values. So we're going to expect a channel ID, which is an ID of channels. Make sure you imported ID from its place here like this Besides this we going to have Workspace ID which will have workspaces Body, which is a string, Image, which is ID of undercore storage, and let's go ahead and just mark the image as optional. And now, we're going to go ahead and modify our handle submit method. So one thing I forgot we have is the editor ref. So how about alongside passing the disabled prop, we can also actually disable the quill editor. So editor ref, question mark current, enable, set to false. And mandatory in finally, make sure that you turn it back on to true. Like so. Great. So now let's go ahead and continue developing here. So we're going to modify these values just a bit. We're going to prepare them here. So values will be create message values. We're going to pass in our channel ID, our workspace ID, our body. And for the image, we're just going to set undefined here. And I think we need to modify this. So image will be undefined. I just have to fix this. Create message values. There we go. So this is now fine, right? So now we're going to check if the user submitted an image in this message. So we are reading from this prop right here. We're going to generate the URL using await generate upload URL. We're going to do do we have the options here? Oh, so these are the options, the values, and then inside of here, we're just going to do throw error true here as well. And then we're going to do const result to be await fetch URL, which we just got from above like this. And then we're going to give it a method of post. We're going to give it a headers of content-type image.type, and the body will be image. And then from storage ID, we will be able to await results, result.json like this. And we're going to get the storage ID here. Let me just see what's wrong with the URL here. String RDEFINE is not valid here. so it's possible to get undefined here. Alright, so if there is no URL, what we can do is we can throw new error, URL not that. So this error will be caught inside of here and it will be filled to send message. But is it actually possible for that to happen? I'm not exactly true. Let me just go ahead and see. Instead of generate, upload URL. inside of, it's right here, generate upload URL. So the response type can be null. So I suppose that yes, it can be undefined. Okay. That's all right. We're going to go ahead inside of chat input. And if there is no URL, we're going to throw an error because we are inside of try and catch. But I think if there's no URL, this will throw an error in itself, right? Okay, so what we're doing here basically is just skipping our values prop, right? We're just passing in an empty object here in order to get to the option so we can assign it throw error through so that it gets caught inside of our catch method here. Great, so we have the URL now and then we're going to do a fetch API call and we are going to attempt to extract storage ID from here. But before we do that, remember, this await fetch will not throw an error if it fails. Instead, we have to do check manually. So if result is not okay, in that case, let's get toast.error. Well, we can just throw new error. And we are going to throw failed to upload image. And then this will get caught inside of here, like that. And if we have storage ID, in that case, we can go ahead and do the following. We can add values.image storage ID like that. And then we can modify the create message here to simply get the values. There we go. So this should now handle the entire upload process for us. So let's go ahead and try it out now. I'm going to go ahead and prepare my database here. More specifically, I'm going to prepare my files. So inside of my file storage, I have nothing at the moment. So let's go ahead and try this out. I'm also going to open this in case we can notice any errors. I'm going to write text with image and I'm going to go ahead and select a random image I have and let's go ahead and click submit. And looks like there were no errors at all. That would mean that we successfully uploaded. Well, let's take a look inside of my files. And there we go. I have my image right here. I can download it. And this is my uploaded image. Perfect. And let's check one more thing inside of my messages here in the latest one. I also have the image storage ID associated here. And well, it looks like it doesn't have a link like this other one do. I don't know if this is, but I think this means that, yeah, it's basically connected to our storage. So later when we return the message list, we're going to have to turn it from storage ID into an actual image. Perfect. So we can now upload images. Amazing. In case you got any errors, we can always visit the source code and you can also, you know, double check that you, you can add a console log here for the URL, for example, to see if you get the URL. So let me go ahead and do that. So let me give you a console log of my URL here so that you know what I'm seeing here. And inside of here, a console log of my result. So inside of here, you should be able to see what's going on. I'm going to refresh the entire thing. I'm going to write test here and I'm just going to pass a random message. So let's write out. there we go. You can see that I got the upload URL above right here. And my result is a response right here with, I don't know exactly where it is. It's a readable stream. Okay. Perfect. And if you got any other errors, so I'm just showing you now in case this works, you know, you can speed this part up in case you still have errors. It could be that your await create message is failing. So that can happen if you misspell the ID of the image. So that should be underscore storage. Make sure it's underscore storage here. Make sure inside of your use create message, you do the same thing here. So image is ID underscore storage. And make sure that inside of your API messages create, you also handled the image as v.id underscore storage. You can also maybe see some helpful errors inside of here if it's not working. And final thing you should check is your schema. Make sure images have the ID of storage. Amazing, amazing job. Before we move to loading our messages, I actually want to wrap up our schema entirely so that I can properly add the conversation ID where I need it because we are going to need it anyway to populate our messages. So let's go inside of schema.ts, inside of our convex folder. And in order to add the conversation ID, we have to implement the conversations themselves. So just above the messages, after channels, let's define conversations. Let's write define table. and let's go ahead and define the properties. So each conversation will be held inside of an individual workspace ID. So each conversation needs the workspace ID. Every conversation will be one-to-one only. For everything else, we have channels. So we're going to have member1ID, which will be v.id members. and then we're going to have member2id, which is going to be the same thing. So it doesn't matter which one is the first one and which one is the second one, but more often than not, the member1 will be the one who initialized the conversation in the first place. And that's it. That's our entire conversations schema. So now let's go ahead inside of our messages table here. let's remove this to do and let's actually add it. So conversation ID will be v.optional v.id conversations like that. Of course, make sure that your Bunex convex dev or NPX convex dev is succeeding and it's not failing at any point. So just make sure that your schema is updated. You should also now see all the new values inside of your data here. So you should see the conversations right here. Great. Now there's one more table here that we need, and that is the reactions. But before we do that, let's actually add all the necessary indexes to messages here. So the conversations are also going to need an index. So let's go ahead and take care of that as well. So the index here will be by workspace ID. And let's add the workspace ID here. And similarly, we're going to have to add some indexes here for messages. So for messages, we're going to need a simple by workspace ID as we had above. But we're also going to need a couple of others. Then we're going to have by member ID. member ID then we're going to have whoops not a comma we are still chaining after that we gonna have by channel ID let copy this one and let go ahead and let write by conversation ID and that we use the conversation ID After that we going to have by channel ID underscore parent underscore message underscore ID underscore conversation underscore ID. So this will be a very large index that will be used to load the threads in channels and in one-on-one conversations. And we also need the parent message for what the thread is actually loading. So let's add everything we need in correct order. So we first need the channel ID. Then we need the parent message ID. And then we need the conversation ID. There we go. And the last table here we need is reactions. So define table again. We're going to have a workspace ID for reactions. So v.id workspaces. We're going to have a message ID for what this reaction will be attached to. We're going to have a member ID as the user who added the reaction. And we're going to have a value, which will be a string and the value of the emoji, right? Let's go ahead and add the index by workspace ID, which will have the workspace ID. and let's go ahead and add the index by message ID, which will have the message ID. And last one will be by member ID. Member ID. There we go. So we finished our schema. And now that we've added the conversation ID to our messages, let's go ahead and let's resolve all of those places where we need to add it. We're going to start inside of the messages.ds here, where I have in the create method added a to do add conversation ID. So I'm just going to copy and paste the channel ID and write conversation ID here, v.optional ID of conversations like that. But it's not going to be just that. So this is what we are going to do. After we do our member check, we need to handle conversation ID separately. So I'm going to do this. I'm going to write a let underscore conversation ID. And by default, it's going to be the one inside of the arguments, right? But there will be a special case if we are replying inside of a thread of one-on-one conversation. When we reply in that thread, in our URL, we are not going to have the conversation ID. We're going to have the individual other member ID that we are talking to, right? So the conversation ID is something internal that we know of, right? So that is a case where message being sent will not have the conversation ID come from the front end. But since it is a reply in a thread, it will have the parent message ID. So that's what we are checking for here. If we don't have arguments.conversationID and if we don't have arguments.channelID. So this ensures, all right, this is now definitely a reply inside of a one-on-one conversation. And to further enforce that, we're going to ensure that we do have the parent message ID. So this combination is only possible if we are replying in a thread in one-on-one conversation. That is the only case where this can happen. So inside of here, let me just modify this to four like this. So inside of here, I'm going to go ahead and add const parent message here. Okay, let me just fix this too like this. Parent message will be await context database get arguments parent message ID because we have that. But in case we don't have the parent message ID, if that happens, we can just throw an error because we have no idea where this message is supposed to go. It doesn't have a channel ID. It doesn't have a conversation ID. And even the parent message ID is incorrect. In that case, we truly have no other option than to throw an error parent message not found like that. Otherwise, we are going to modify our conversation ID to be parent message dot conversation ID. So that's how we are going to discover which conversation ID do we need if it has not been passed. And then inside of here, besides channel ID, we can pass in the conversation ID to be our conversation ID. Just like that. So that's how we took care of that case. Why are my indentations all of a sudden so weird? Oh, it's because this is the weird one. All right. There we go. And this one as well. I have no idea how they became so weird. Okay. There we go. Now we also have to modify the hooks. So let's go ahead. Inside of our features, inside of messages, use create message. Inside of here, we're going to add the conversation ID, optional ID of conversations. And once you add this, nothing should really change at this point. This should still be working because we modified our created to accept the optional conversation ID. We also handled this special edge case in case there is no channel ID, no conversation ID, but there is the parent message ID. We are going to come back to this. I know it might be a little bit confusing because you can't exactly see what's going on, but I have to write this because I'm afraid I will forget it later. So I'm going to go ahead and try and remember this later to remind you why this is happening. All right, and let me go ahead and quickly go through to do. And it looks like we also have to remove the associated messages here. So let's go ahead and do that. Let me see where I actually am here. Okay, we're going to do that later, actually. We're going to have some other chapters for that. What I want to ensure now is that I have the conversation ID in all the places I need. There we go. So we have finished our schema now. We have added the conversation ID to our messages create method, to our messages create hooks, and we are now ready to continue developing. Let's just confirm that chat input has no errors. There we go. chat input has no errors because this create message does not need to accept the conversation ID, right? So inside of this chat input, create message value, we don't have the conversation ID because we don't need it. This is just for channels at the moment. Great, great job. You are now ready to load some messages. Now let's go ahead and let's build an API endpoint where we are going to load all of our messages for a specific channel. But this is going to be a bit more complicated than you think. So let's go inside of convex, inside of messages.ts. We've already started building a helper method here called getMember, right? And we use it throughout our code. For example, inside of here in the create function, we leverage that getMember so we have to write less code. Well, before we can build the get query here, we have to add a couple of more of these helpers. So let's go ahead and let's start by populate user. So const populate user will be a very simple method, which will accept the query context as the one below it, and a user ID, which will be a type of ID users. Inside of the populate user method, we are very simply going to return context database get user ID. We're also going to add populate member, which will have the exact same queries, but instead of user ID, it will, of course, have the member ID here, which is a type of ID members. and inside of here, it's going to return context database get member ID. So what's the difference between the, my apologies, not export, just const. So what's the difference between populate member and get member? So populate member works very simply. I just want to populate the member using the member ID, right? I don't care about user information inside of that member. Whereas getMember serves as a general method where I want to get a member by a specific workspace and user ID, right? So populateMember is just a generic, you know, okay, using this ID, load the member. But getMember is with a combination of this workspace ID and this user ID, what member is that, right? So that's the difference. Ensure that you have populateUser and populateMember. and now let's go ahead and add const populate reactions. So again, we're going to have context, which is query context. And then we're going to have a message ID for which we will be loading reactions. So that's an ID of messages. A quick reminder. So our reactions are inside of our schema here. In the last chapter, we've wrapped up the schema. So now we have reactions which are attached to messages. And that's something we're going to have to populate inside of our API endpoint to fetch all messages. So inside of here, we're going to return context.database. We're going to query reactions with index by message ID. And we are very simply going to do query equals message ID by our param here, message ID. And let's simply collect all reactions that we have. Great. And there's one more thing that we have to build, which is populate a thread. So in case we have some replies sent to a message, we have to add that information to each message object inside of our array. So let's add a const populate a thread here. So that will be an asynchronous method, which will accept a context, which is query context and message ID, which is an ID of messages. Now inside of here we first going to get the messages using await context And then we going to add messages with index by parent message ID which looks like is something that I have missing. So let's go ahead and add the by parent message ID to our messages schema. So let's go inside of schema here and let's find the messages table. And we have a bunch of indexes here. So let's go ahead and after the by conversation ID, let's add index by parent message underscore ID, parent message ID like that. There we go. So now we have by parent message ID here. And then we can actually use the by parent message ID inside of this populate thread method. And let's just modify it. So I'm going to go ahead and immediately return query equals parent message ID, message ID, which we passed in here, right? So we are now loading, after we add collect, of course, we are now loading all messages which are replies to this messages, right? So we don't have a different table for replies. We just reuse the existing messages table by adding the parent message ID to it. So if a message at any point has a parent message ID, then most definitely that's a message that should only be loaded as a reply to a certain message. It should never be loaded right here in the channel. It should be loaded in the sidebar here once we click on replies. Great. So now we have all messages which are a reply to a specific message. And now let's do if messages.length is zero. So if there are no replies, we're going to go ahead and return the following. The count of replies will be zero. The image, which will be undefined, and timestamp will be zero. So if you're wondering what is this? Well, we are not going to populate the entire collection of messages. What we are going to use this method for is to display for each message. All right, this message has five replies and the image will be used as this is the image of the last user that replied similar to this, literally something like this, right? Like it will say, Antonio, 18 seconds ago. That's the last reply. And that's what the timestamp is for. So if there are no replies, we have nothing to render on the front end because we're going to see if count is zero. That means no replies for this message. Otherwise, we're going to have to get the last message. So last message will be messages, messages.length minus one. And let's do const last message member. will be await populate member context and last message dot member id so now we are reusing our populate member method right here now that we have the last message member we're going to do this if there is no last message member we're going to go ahead and return the following count will be messages.length image will be undefined and timestamp will be 0 so we're just going to display some partial information here but if we do have the member we're going to add const last message user to be await populate user context last message member dot user id So now we have the entire user and now we can add full data here. So count will be message.length, messages.length. Image will be last message user, question mark.image. And timestamp will be last message dot underscore creation time like this. And let's go ahead and actually resolve this. So if we can't load last message member, it doesn't make sense to add any partial data. We're just going to return zero the same way if messages, if there will be no replies at all. There we go. So now we have everything needed to load our messages. So now let's actually go ahead and create the get endpoint. So I'm going to go ahead and create that just above our create mutation here. So export const get will have a query, which will have specific arguments and is always an asynchronous handler. Async, let me just see, did I, oh, we need to add query, my apologies, I was wondering why I have those type errors. So make sure you import query, query context and mutation from generated server here. All right, so we have populate thread, populate reactions, populate user, populate member, and get member. These are our helpers. And now let's focus on the get method here. So first of all, let's add the arguments. We're going to accept a channel ID, which will be optional. Because remember, we are also going to be able to load by conversations. So this will be a conversation ID, which will be an ID of conversations. Besides that, we're also going to be able to load messages by parent message ID, which will be an ID of messages. So these are the three combinations by which we are going to be able to get messages. Messages are also going to be infinitely loaded, right? So there could be thousands of messages and we can't load all of them. So we're going to have to add pagination here. Luckily for us, Convex takes care of that. So let's go ahead and add pagination options, OPTS. So make sure that you write it exactly like this. So pagination options will be pagination options validator. And you can import that from Convex slash server. So pagination opt validator. Make sure you add it here under the pagination opts argument. Excellent. So now let's go ahead and let's do what we always do, which is get our user ID using await out, which we should already have imported from .slash out. Get user ID right here. And then we're going to go ahead and check if we don't have a user ID. Let's throw an error. And now we're going to have to specifically define where we have the conversation ID or not. Because remember, even in the create, there is this edge case with conversation IDs. So let me remind you, this edge case is only possible if we are replying in a thread in a one-on-one conversation. And I know it probably still doesn't make sense why we have to do this, but I promise once we actually get to having the threads and having one-on-one conversations, we are going to go ahead and have it clear in sight why that's happening. You're going to see that even though we are sending a message in a conversation, in a reply, we will not be able to send conversation ID. But thanks to the parent message ID and a lack of channel ID, we are going to get the conversation ID from the parent message. So let's go ahead and do that here again. So let underscore conversation ID will be arguments.conversation ID by default. But if we don't have arguments.conversation ID, and if we don't have arguments.channel ID, but we do have arguments.parentMessage ID, that can mean only one thing, that we have to load that parent message using await context database get arguments parent message ID. And if we don't have the parent message here, we're going to go ahead and throw a new error here, parent message not found. So if this happens, we are okay to throw an error because there is no way this combination can happen and we truly don't have anything else to load here. So this is one exception where I will allow an error inside of a query here. Great, so now if, let's go ahead and actually finish this. So if we do have the parent message, we will modify our conversation ID to be parent message dot conversation ID like that. So that's the edge case that we have to cover for conversation ID. Make sure that you have that. I will, of course, come back to this later. Now let's go ahead and let's get our results. So I'm going to go ahead and do await context.database.query messages. And let's do with index by channel ID, parent message ID, and conversation ID. Let's go ahead and get query here. And we're going to now chain query equals channel ID to be our arguments channel ID. We're going to chain parent message ID to be arguments parent message ID. And they need to be in this order. If you know, make sure that inside of your schema for this large index here in the messages. by channel ID, parent message ID, and conversation ID, you have the exact same order. So that's important, right? So we do it in that order. Not a comma, but another chain dot equals conversation ID arguments, actually, my apologies, not that this will be underscore conversation ID, because remember, there's an possible edge case. Let's order them by descending. And important here. We're not going to collect them, but instead we're going to use .paginate method and pass in arguments and paginate options like that. There we go. We now have results, but we're not done yet. What we have to do now is we have to iterate over all of those results and we have to properly populate everything inside. So let's go ahead and first just return back the results here.\nSo we're going to go ahead and come back to this later because I want to show you how it looks like now and why we needed all of these helpers here. So this is pretty straightforward. This is the confusing part. The reason I'm already writing this, even though we don't have this functionality, is because it's very important and I don't want to forget writing this. So let's go ahead and do the following. Let's create our useGetMessages method. so we can go ahead and load what we already have. So I'm going to go inside of source here, inside of my features, and I'm going to go inside of messages. Instead of API, I'm going to create use get messages.ts like that. And let's actually go ahead and write this one from scratch. So let's add use paginated query from convex react. Let's go ahead and let's import API from convex generated API. And let's import the ID type from convex generated data model here. And now we're going to define the batch size. So how many messages do we load at once? And let's set it to 20. We can later experiment with this and set it to a lower number if we want to actually see the infinite load in action without having a million messages. Now let's create an interface use get messages props. So this will accept a optional channel ID, which will be a type of ID, channels. Besides that, a conversation ID, which will be a type of ID, conversations. And besides that, an optional parent message ID, which will be a type of messages. And now let's export type, get messages return type, because we're going to have to type this explicitly. So that will be a type of API messages.get. And then we're going to get the return type and we're going to get individual page, right? Because it's paginated. So this is the individual message that gets returned. ID, the creation time, the image, the channel ID, parent message ID updated, right? So this is the individual message that we are getting. And you can see right now, it's not exactly populated here yet. So we're going to have to work on that as well. And now let's create our export const use get messages here. So I'm going to add use get messages props. And let me see if I misspelled it. I did not. Looks like it works. So let's go ahead and add the props. So channel ID, conversation ID, and parent message ID. And now let's go ahead and let's call use paginated query. In the first argument, pass in the API.messages.get. In the second argument, you're going to pass in the channel ID, the conversation ID, and the parent message ID. And in the third argument, you're going to define the initial number of items, which will be our batch size constant. like that. And from all of this, we're going to return back the results, the status, and load more method. And then we're going to go ahead and return the following results, status, and a method load more, which we are going to create on our own by calling load more once again and add a batch size inside. This way, we don't have to handle this anywhere else but this place, right? So batch size is defined inside of use get messages, and it can only be changed inside of here. Great. So we now have the use get messages method. And now let's go ahead and actually add it to one channel, right? So let's go inside of source app folder, workspace workspace ID channel channel ID page.tsx. And this is already marked as use client and we already have some use get channel here. So how about we add messages here, use get messages like this. I'm going to go ahead and add that right here. And I'm going to pass the single argument channel ID here. And I should be able to get results here. The only thing we're going to do with results for now is console log the results. like this. And if you want to, inside of here, inside of this div flex one, you can also stringify them. So JSON stringify, this would be results. Let me see if this will break the page or not. There we go. So this channel of mine has no messages, but if I send a message here, there we go. You can see how it's immediately populated it here, right? And the problem is I don't think, okay, yeah. So here's the thing. Let's say we want to use this array, right? And this individual object inside to load and display this message. Well, what we can definitely show is when the message was created, we can show that. And we can also show the body, right? We can use this delta OP inside of quill renderer component, which we're going to create, and it will be the exact same text as we wrote in this editor. We're going to have it here, right? So where is it? There we go. Exact same text. We can do that. But what we can't do is display when a specific member, sorry, who was the member that sent that, right? We don't have the image. We don't have the name of the member. We also have no reactions to this message. And we also have no idea how many replies this message has. So it's okay. It's good enough. But, well, it's good enough, you know, for a quick demo to show you that our messages here are working, right? If I go into a new channel, you can see in here, I have much more messages. If I go ahead and create a completely another channel, it's going to be empty in the beginning until I send a message here. So that part works, right? But now let's go ahead and let's improve our use get messages here. So I'm going to go back inside of our API messages get, which we are developing in the convex folder. And now we're going to modify these results and actually, well, populate everything we need inside. So we're going to go ahead and return an object and we're going to spread the results inside. But we are going to override one element of results, which will be called page. Let's go ahead and open the parenthesis here. And let's write await promise.all. And inside of here, inside of the parenthesis, add results.page.map. So for each page, we're going to go ahead and do something here. No semicolon at the end, right? So since we're using .map inside of promise.all, we can initiate asynchronous method here. Otherwise, you can't use async inside of .map. But if you wrap it inside of promise all, that's the syntax you can use. So inside of here, each page will have a message. So let's go ahead and populate everything we need for this individual message. Const message will be await. My apologies. Const member will be await populate member, pass in the context and message.member ID. Now we have the member. Const user will check if we have the member and then do await, populate user, and pass in member.userID. Otherwise, it will be null. And we also have to pass in the context here. My apologies. If we have no member or if we have no user, we don't have the ability to display this message. So just return null and then we're going to filter it out later. Now let's do the same thing with reactions. So const reactions will be await populate reactions context and message ID. Then let's load the thread or replies. So await populate thread will pass in the context and message underscore ID. And then we have to load an image if the message has an image. What do I mean by that? Well, remember, inside of our schema, if we upload an image and attach it to message, we store it as image ID of underscore storage. So that's not a URL that's stored inside of our database. We have to generate the URL from our convex storage, right? So inside of here, you know, I have these files and I have the storage ID. And this storage ID is stored inside of my messages here, right? So let me go ahead and there we go. So these two messages of mine have the image, but this is not a URL. This is just a random storage ID. So now we have to populate that using message.image. And then let's add a question mark. If we have the image, we're going to do await context.storage.getURLMessage.image. Otherwise, undefined for the image constant. There we go. And now that we have the reactions, we have to normalize the reactions data. So the way I want reactions to show is like this. You know, this will be, for example, my message. And then below that, I'd expect, for example, a smiley face and then one. Besides that, I'd want, for example, a fire. This will be four, right? So we have to modify our reactions and normalize the object so that we reduce by each emoji and also count all members that reacted with the same emoji and add that up to the final count, right? So this is what I want to do on the back end so I don't have to do that logic on the front end because it's going to be faster. And I want my front end to receive normalized data here. So let's go ahead and add const reactions with counts. So this will do reactions.mat, individual reaction here. And let's go ahead and return reaction. But count of each reaction will be reactions We using the R shorthand for reaction R dot value is equal to reaction dot value like that And let do dot length here like that We now have reactions with counts We now have to deduplicate the reactions, right? Because just by doing this, we would have things like this. I will have one and then I will have one again. So what I want to do is combine this two into just being this inside of our reactions object, right? So right now we managed to count the reactions, but each reaction has its own count, right? We did not join them in any way. So that's what we're doing now. So let's add const deduped reactions will be reactions with counts dot reduce. Inside of reduce here, we're going to get the accumulator and a reaction like this. And now in order to resolve this immediately, we have to define a type of this. So this will be an array as, and then open parenthesis, document, which you can import from generated data model besides ID. So we're defining, this is basically an initial value of the reduce method, right? So in the first argument of the reduce method, it's our function. Let me show you this. There we go. So we have the callback function and the initial value. We are now defining the initial value and giving it the proper types. So this will be a document reactions. But we're going to extend it by adding a count number to it. And we're also going to add member IDs to it. So ID of individual members. So we're going to know how each member ID that reacted like that. And this entire thing will be an array like that. There we go. So this is the type of our initial value. Now let's go ahead inside and let's actually develop the method here. So const existing reaction will be accumulator.find. Go ahead and use the shorthand R as reaction. So R.value is equal to reaction.value. Like this. If we have an existing reaction, we're going to use set to remove the duplicates. So existing reaction.member IDs will be array from new set. And then spread existing reaction.member IDs and then add the new one. So reaction.member ID like so. else, we're going to go ahead and push the new member IDs. So accumulator.push, spread the reaction, add the member IDs, and this will be a single reaction.member ID like that. And let's not forget to return the accumulator here. There we go. So that's our deduped reaction, deduped method here. We can, of course, revisit this later. And I would suggest that you revisit it later as well when we actually add the reactions. I know it's a bit hard to follow when you don't have the entire thing visible for you, but we simply have to do it in this way. Otherwise, it's extremely complicated to inject it later on. And the last thing I want to do is the following. I want to remove one property, which is member ID, right? I don't want to return that to the front end because I don't need it. I have the populated member in the reaction. I mean, in this array of member IDs. So I'm going to go ahead and do const reactions without member ID property. So pretty descriptive here. Deduped reactions.map. And then go ahead and add the first argument here where we're going to get the member ID. The rest will be spread in the rest constant and just return the rest. So this is kind of a quick way to remove a property from every single item in the array. So we are now removing member ID from every single object of the dupe reaction. There we go. And now we are ready to return everything in the message, but passing the new image, which we now have, The member, which we now have. Let me just pass it as member. The new user, which we now have. Reactions, which will be reactions without member ID property. Thread count, which will be thread.count. Thread image, which will be thread.image. Thread timestamp, which will be thread.timestamp. Like that. There we go. but we are not done just yet. So what we have to do now is we have to, on this page, so on the parentheses, which is ending this page, I don't know how it's easier for you, either go one, two, three, four from below, right? Or simply at the end of this bracket, after the bracket and the parentheses, after another parentheses right here, we have to add dot filter, add a message. and we're simply going to do message is not null. But we have to give it some specific type here. So let's go ahead and do message is non-nullable like this type of message like this. But it looks like it works even without it. So, you know, I have no types errors here. So whether you want to put this or not, this is explicitly adding the type, right? So you can remove it if you don't want to, if it's causing you any problems. There we go. So let's go ahead and try it out now. So now we should have a different value here. So let me just go ahead and refresh the entire thing. Am I still getting errors? I think I'm not getting errors. I think that now this is a single message that I sent. And look at the abundance of information that I now have. So I now have the body as I did before, right? I have the member, which I can see the ID of, and I can see the role of the member that's sent to this, which is quite useful for me, right? I have the thread count. I have the thread timestamp. And I have the user that sent the message. And for the user, I can see their email. And I can also see their name here. Let's go ahead and try. Can I also see their image? I think I also have more information here, but it's getting cut off. Let me try it like this, maybe. There we go. So I should have looked at this because this is way more readable, right? So this is now how individual message will look like. You can see the abundance of information here. We have an array of reactions here. We have the user. We have the user image, the username. We have the workspace ID, creation ID. All of these things which we are going to use. Great. So one thing I quickly want to do before we wrap up this chapter is just fix one thing with our messages. So go ahead and do the following. I want you to remove all of your messages here. So go inside your messages table here, select everything and delete all documents. I don't want you to have a single message inside. And then I want you to go inside of your schema here, inside of messages, and wrap updated at in optional. Like this. I want it to be optional. Make sure you do this. And make sure that you have no errors inside of your Banex convex dev here or MPX convex dev. And then go inside of messages, find the create mutation. And when you create the message, remove the updated at. it's just going to be simpler because we're going to use the comparison. If updated at is newer than created at, we're going to consider that message edited, right? But the problem is that using this, that's always going to be the case, right? Because date.now will not be in sync with what convex will insert for its own creation time. So each element inside of convex has its own creation time. and it's impossible to match that like this. So we are going to be fine with updated ad being undefined. If updated ad does not exist, that means this is an original message. If updated ad exists and it's more recent than creation times, which will be pretty much always, that means this message has been updated. So just go ahead and save that. And let's go ahead and refresh in a random channel. And let's say hello world here. And there we go. you should still be able to see your messages with all of this information. What we're going to do next is build a component called message list and the actual message and start rendering out our messages. Great, great job. Now let's go ahead and let's render our messages. So I'm going to go ahead and revisit my channel ID page. So that's inside of source, app folder, workspace, workspace ID, channel, channel ID. And inside of here, we have page.tsx. And the first thing I want to do here is I want to also get the status and load more from my use get messages here. And I'm going to go ahead and do this channel if channel is loading, but also if status is equal to loading first page. So that's the only time I want to display the full screen loader. Any other subsequent loading will be a different status. So that will be loading more, for example, right? So loading first page is for the initial batch of messages. So this loader is perfectly fine to be loaded with that logic. Great. Now let's go ahead and let's remove this div here. and let's implement a message list component. The message list component will have a channel name, which will be channel.name, a channel creation time, which will be channel underscore creation time data which will be results load more which will be load more method which we just exported Is loading more will be if status is equal to loading more. Can load more will be status can load more like this. And now let's go ahead and create the message list component. So the message list component will be a common reusable component. So we're going to go ahead and build it inside of source components. Let's create a message-list.tsx like that. And now let's go ahead and let's create an interface message list props. I'm going to give it a bunch of possible options. So we're going to have a member name, which will be an optional string, a member image, which will be an optional string, channel name, which will be an optional string, channel creation time, which will be an optional number. And then we're going to have a variant of the message list. So message list component will be reused across the channel variant, a thread, and an individual conversation. So those will be different variants, right? And then we're going to have the data. So the data will be a type of get messages return type, which we define inside of features, messages, API, use get messages. Here, where we define the batch size, remember that we've added the get messages return type. So make sure you've added an export here so that you can then import it here. There we go. So this is our use paginated query, right? The method we recently created. So it's the getMessagesReturnType or undefined. And then we're going to have a loadMore method, which is a very simple void. IsLoadingMore, which is a Boolean, and canLoadMore, which is a Boolean as well. And now let's export const messageList here. Let's assign the props, messageListProps. and let's destructure everything we need inside. So that will be the member name, member image, channel name, channel creation time, data, variant, which by default will be a type of channel, load more, is loading more, and can load more, like so. and then let's go ahead and actually render these things out. So I'm going to go ahead and return a div here with a class name flex1, flex, flex column reverse, padding bottom of four, overflow, y auto, and messages scroll bar. So this is a custom class name that we have to implement. So let's go ahead and do that by quickly revisiting our source app folder globals.css. And let's go all the way to the bottom here and add messages-scrollbar. And let's go ahead and define the scrollbar width to be thin and the scrollbar color to be hashtag dcdcdc. And then f8, f8, f8, like that. Great. Let's go back inside of the message list here. And what we're going to do is the following. We're going to go ahead and simply iterate over our data here. So let's go ahead and see if I can do data.map. And I should be able to have a message inside. And what I'm going to do here is I'm going to return a div and JSON stringify individual message here. like this. So now let's go ahead and let's go inside of the page and let's import the message list from components message list. I'm going to move it here. There we go. And let me just go ahead and log in here. And let's see if we are able to see anything here. There we go. So we do have some messages, but it looks like my scroll here is in reverse. But I think that's only happening. Well, I think it's happening. Let's get inside of my message list. I think I'm missing a class theme here. So we did add overflow Y auto here and we did add flex one flex call reverse. I think it's simply happening due to the fact that this is an extremely long text here, right? So what I want to do now is I want to add a specific grouping of these messages by the time they were sent, right? So what I want to happen is that if I send two messages, let's say one minute apart, I want a little separator between them to be loaded, right? And we are going to go ahead and later change this so it works by day, right? But let's go ahead and try and group our messages like that. So this is what we're going to do. We're going to group them all by their unique dates. And for this, we also need a bun at date FNS or npm install date FNS. So let's go ahead and do const. Grouped messages will be data, reduce. Go ahead and get groups and message. And let's go ahead and define the initial value to be an object as record of string and type of data. and inside of here we're going to do the following const date will be new date message underscore creation time and then const date key will be format which we have to import from date fns so let me just go ahead and import format from date fns package which we've just installed So let's pass in the date and let's format it in year, mm, and day, like that. And then if we can't find the date inside of our groups, I mean the date key, we're going to go ahead and define, oops, let me open the brackets. We're going to define that date key to be an empty array. So groups, date key will be an empty array like this. Otherwise, groups, date key, unshift message, meaning added. It's the opposite of push, right? And let's return groups like this. So these messages are now grouped by their date. So what we can do now is the following. Instead of rendering our data simply like this, right? We can do the following. Object.entries, grouped messages, or an object.map, open parenthesis, and then the structure, date, key, and messages here, and immediately return the following. Let me just fix this. So grouped messages. So this is an object, right? The way this looks is like, I didn't explain that properly. So the way this looks will be like this. For example, 2024, 8th of, I don't know, 25th. And this will be then an array of message one, message two, message three, right? So that's how this is going to look like. So now we are doing object.entries. So we get the individual array from an object. and we're going to do the following. We're going to create a div with a key of date key and then a div with a class name text center, my2 and relative. Inside of that, we're going to add an HR element, which will be a self-closing tag with a class name of absolute, top, one and a half, left zero, right zero, border top, border gray 300. Below the HR element, we're going to have a span with format date label and date key inside. And now let's just quickly develop the format date label. So I want to display a specific time when this batch of messages are being sent, But I don't want to just display the date. If possible, I want to say today. So the messages that are being sent today. And I also want to capture, for example, yesterday. And everything else can just be a normal date. So const format date label. We're going to have a date string, which will be a string. Const date will be new date, date string. If is today from date FNS. So make sure you import is today from date FNS. is yesterday from date FNS and we already have format. Alright. So if is today, date. In that case, return today. If is yesterday, date. Return yesterday. Otherwise, return format date four letter E's four letter M's and then D like this. Now we have the format date label key here and now let's go ahead and just add a class name here. Relative inline block background white px4 py1 rounded pool text extra small border, border gray 300 and shadow SM like this. And now outside of this div right here, but still inside of the date key div, we're going to go ahead and do messages.map, get the individual message and get the index of that message. And inside of here for now, let's go ahead and write return div JSON stringify message. And let go ahead and give this a key of message underscore ID like that And there we go You can see that now we have a separator for our messages So if you wait a day and send a new message it will be sent under, this will be separated by today and this will write yesterday. So now we have that kind of message grouping. Great. And now let's go ahead and further develop this. So what I want to do is start to finally render this message so it looks like something. So let's go ahead and add a message component, which doesn't yet exist. And let's go ahead and pass in key to be message underscore ID, ID to be message underscore ID, member ID to be message dot member ID. Then we're going to have author image to be message.user.image. Then we're going to have author name to be message.user.name. After that, we're going to pass reactions to be message.reactions. Body will be message.body. image will be message.image updated at will be message.updatedat created at will be message.creationTime underscore creationTime and then we're going to have thread count to be message.thread count thread image will be message.thread image thread timestamp will be message.thread timestamp. Right. So a bunch of useful information on how to decide how we want to display this message right here. And we are still missing a couple of other things. So let's actually just add them. I will just prefix them to something else. So is editing this message will be set to false by default. Set editing ID here will just be an empty arrow method. is compact will be false and hide thread button will be false as well so these are pretty much all the things that we need to pass and one more thing here is missing after author name let's add is author and this will be false by default right so these are all the possible things that the message component will have and now let's go ahead and let's actually add the message component so it's going to exist in the same place as message list in the components, in source components. So message.tsx. Let's go ahead and let's create an interface message props. ID will be a type of ID messages. Member ID will be a type of ID members. Author image will be an optional string. author name will be an optional string as well. Is author will be a boolean. Reactions will be an array of omit document, which you can import from the same place you imported ID. Document of reactions like this. And we're going to omit member ID from it. and we're then going to extend it to have count number and member IDs, ID members, and that's an array. The reason we are doing that is because remember, inside of our messages API, inside of our, where is my get method here? So this is my get method, right? My get query. Inside of here, we modify how reactions look like, right? document reactions, count and member IDs. So that's what we are doing here, count and member IDs. And then also remember that we are removing the member ID property. So that's why we're using the omit. And then we remove the member ID from it like that. All right, we have that. And now let's add the body. So body, we can simply do document type of messages and then just body like that. null or undefined like this created at can again be document messages underscore creation time updated at can be updated at is editing will be a boolean is compact will be an optional boolean. Set editing ID will accept ID, which will be a type of messages, or it can accept null if we want to remove the editing preview. Hide thread button will be an optional boolean. Thread count will be an optional number. Thread image will be an optional string, and thread timestamp will be an optional number. So a bunch of properties for this message component. Now let's export const message. Let's go ahead and let's assign the message props to the type here and let's destructure everything we need. So we need our ID is author member ID and author page. an author image and for author name, in case it doesn't exist, we're going to fall back to member, right? Let's at least have something. The author image is fine because we can create the image from the name. So if anything, let's go ahead and at least have the name. And then we can just go ahead and add everything else we need. So that's the reactions, the body, the image, created at, updated ad, is editing, is compact, set editing ID, hide thread button, thread count, thread image, and thread timestamp. Basically ensure that you have everything that you have in the message props right here. And let's go ahead and just return a div, JSON stringify, and you can pass in the body like this. And now let's go inside of the message list and let's import the message component from dot slash message like this. There we go. So now, there we go. You should be seeing a much smaller JSON output now. Basically, it should now render this kind of message. Great. So what I want to do now is I want to build a renderer component, which will be able to turn this special kind of syntax into exactly what I see here. For example, what if I bold it and put it in italic, right? You can see that now we have some attributes here. So I want to build a component which will be able to read this and properly display it here on the screen. So let's go ahead and let's build the renderer component. So instead of the message here, instead of using JSON stringified body, we're going to go ahead and develop a renderer and pass in the value as body. And it's going to be a self-closing component. So now let's go ahead and build the renderer. So I'm going to go inside of the components, the same place where we have the message list and the message itself. And let's add the renderer.dss like this. Let's import quill from quill. like so. Let's import useEffect from React, useRef, from React, and useState from React. Let's create an interface renderer props to accept the value of a string. Let's go ahead and define the const renderer. And just like an editor, we will not do export const. We're going to do export default here because we're going to have to dynamically load it. So we're going to go ahead and type this with renderer props. Let's go ahead and define is empty and set is empty to be used state and by default is going to be false. Let's define the renderer ref to be use ref. html div element and by default null. Let's go ahead and add a use effect here. Inside of here, if we don't have the renderer ref dot current, we're going to return this method. Then let's go ahead and define the container constant to be renderer ref dot current. renderer ref like this. Let's go ahead and initiate quill using new quill document create element div. And for the properties here, we're just going to add a theme of snow, nothing else, because we're not building an editor. We just want to render the text. So for that, we're going ensure that quill is not enabled. So quill.enable false. This will make it read only mode. And now let's go ahead and add the contents to be JSON parse value because we store it as a string of JSON. So now we turn it into an actual object and now we can do quill set contents contents like that. And now let's go ahead and do the following. Let's check if is empty. So if is empty, quill.getText, and we can use a little help from our editor component inside of source. We should have the editor. Inside of source components, you should have the editor here. And go ahead and copy this, .replace.trimLength equals zero from our isEmpty method. So text.replace with this exact regex, .trimLength is zero. and then set is empty, is empty, like that. And then container.innerHTML will be quill root innerHTML, like that. And in the unmount method, we have to properly clean this up. So I'm going to do if container is present.\ncontainer.innerHTML will be an empty string. There we go. And let's just add value inside a use effect here. And then if is empty, we're going to return null. So there is a chance this can happen if someone just sent an image. In that case, the renderer will not be able to display anything. Otherwise, let's go ahead and return a div, which will be a self-closing tag. and it's going to be very simple. Ref here will be renderer ref and class name will be QL editor and also QL renderer. So make sure you have both of them. Let's go ahead back inside of our app folder, globals.css. Make sure that you have QL renderer and make sure that you have QL editor. You should have both of them here. And now let's go ahead inside of our message component, which is inside of components here, and let's import renderer, but let's import it dynamically, right? So I'm going to import dynamic from next dynamic, and then I'm going to go ahead and do const renderer to be dynamic like this. return a method inside, which will be import at slash components slash renderer like this and pass in SSR to be false. And we have to go back quickly inside of the renderer because I forgot to do expert default renderer. There we go. So now if you take a look, you will see that my text is styled correctly. So for example, a list, one, two, three, and now let's go ahead and write something else which is bolded, and then let's add an italic here, for example, italic, and then let's add an italic strike through, and if I go ahead and send that, there we go. You can see that it renders exactly as we saw it here in the editor. Perfect. So what I want to do now is I want to give my message component some styles here. So let's go back inside of the message component here and let's give this div a class name of flex, flex column, Gap 2 Padding of 1.5 PX of 5 On hover Background Gray 100 Gray with an A Gray 160 Group and relative Like this And I think that already There we go It should start looking a bit better Right? We now have this hover effect And it's kind of pushed to the side It's not stuck with this sidebar here So much better already great and now let's go ahead and render when it was created so we're going to do that above the renderer still so inside of here add a div with a class name flex items start and gap of two and now let's add a native button element which will use format from date FNS. So just make sure you add this. This button will add new date, created at, and it will just use hours and minutes here. There we go. So you should now see a button with hours and minutes when a certain message was sent like that. And let's go ahead and give this button a class name of text extra small, text muted foreground, opacity 0, group hover, opacity 100, width of 40 pixels, like that, leading of 22 pixels. We're going to have text center and hover underline, like that. There we go. So now the time shows only when you hover on it. And now let's go ahead and let's wrap our button inside of a hint component. So go ahead and import hint from .slash hint, because this is in the source components where all of these are kept together. Let's go ahead and give the hint a label of format full time. and for format full time we're going to have to define our own method again so just pass in format full time new date created at let's go ahead and create the format new time method here so const format new time will accept the date of date and inside of here it will return a string but open template literals like this So we're going to go ahead and do is today from date FNS, date. In that case, we're going to write today. So make sure you import that is today. And let's also add is yesterday. So if it's today, we'll render today. Otherwise, if is yesterday, date, we're going to render yesterday. Okay, else we're going to go ahead and do format, date, mmm, d, comma, and then four y's here. And then we're going to continue at format, date, hmmssa. SSA. So basically, we're going to have either today or yesterday or a very specific month, day, year at this exact time. So basically, if someone hovers on the hint here, they're going to have a much more specific time when this was sent, including the seconds as well. right or an exact date if it will either say yesterday if you wait a day here or if you sent it a couple days in the past it will say the exact date so you are aware of when these messages here were being sent great so we now have that and now let's go ahead and style this even further we're actually going to modify this a bit by using our is compact here so if is compact then we're going to return this because we just created a compact version of a message right so if you go ahead inside of your message list here and add a variant to it do we define oh my apologies is compact uh just added to true there we go then all of these messages here are visible right so we're going to go ahead and bring this back to false now and we're going to style them in a different way so if it's compact will return one thing but if it's not compact it's going to return a similar thing so for now you can copy it but we are going to slightly modify this entire thing here. So first thing that we have to change here is inside of this div before the renderer. So let's start a div with a class name, flexItemStart and gapOfTo. And let's add a button, my apologies, a native button element, which will have an avatar from .slashtuiAvatar. And let's also add everything else we need from dot slash UI avatar. So that's going to be the avatar image and the avatar fallback like this. Make sure you have all of those. So let's go ahead and add the avatar. Inside of that, let's add the avatar image, which will be source author image. And then we're going to have an avatar fallback. and for this we can use the solution from well any other place I think we have one called user item so if we go inside of source app folder workspace we should have user item here so let's go ahead and first get the avatar fallback here instead of message let's just add const avatar fallback and this will simply use the author name like this and let's go inside of user item and let's copy the entire uh well we can copy the entire avatar from here actually because it has all these classes we're just going to slightly modify the props so let me just paste that here there we go and the source will be author image and then this will be an author fallback like this So there we go. I can now see my little message here like this. Great. And now what we have to do is we have to add a renderer inside of that div right here. So right next to the button. So now it should be looking slightly better, right? They should be next to each other like that. But let's go ahead and style it even further. So I'm going to wrap this renderer inside of a div. And I'm going to give the div here a class name of flex, flex column. Whoops. Full width and overflow hidden. Then inside, I'm going to add another div with a class name text small. So we're going to keep the renderer just in the first div here, right? We're not going to put it inside of this one. So inside of this div text small, we're going to have a native button element, which will render the author name. And the class name for this will be font bold text primary and hover underline. And let's also add an onClick here, which will be an empty arrow function for now. Like this. There we go. So we now have my name here as well. Great Now let go ahead and go outside of this button and let add a span element And we going to go ahead and forcefully write NBSP which is Unicode for white space So we are basically adding some space here, some invisible space here. We can also use Tailwind Gap X2 if you want to. And let's go ahead and add a button here, which will have a class name, text extra small. text muted foreground and hover underline. And inside of it, we're going to use format, new date, created at, and just simply HMMA, like this. There we go. So next to the name, we now have the time it was sent. And as we did in the compact version, let's wrap this around a hint. and let's give the hint a label of format full time new date created at so now when you hover there we go you get a more specific time perfect so we have that great so we have the renderer here and what i'm going to do now is the following if we have updated at I'm going to go ahead and add a span and say edited here. If I don't, I'm just going to do null. And let's give this span a class name, text extra small and text muted foreground. And let's change this to true. And there we go. You will now see how this looks like when it's edited. So when we edit a message, the updated at field will be added. And then we're going to render the edited text here. So for now, change it back to updated at like this. Great. So what I want to do now is I quickly want to go back to my message list and I want to turn the is compact to true. So there we go. Now they look a little bit differently and they actually are incorrect. So let's go ahead and properly add them. So inside, let's go ahead and scroll up inside of the message to where we can find if is compact here. And first of all, let's move the renderer inside of this div plexitems start, right? So right below the hint component. So there we go. Now time is next to the message here. But that's not all. I also want to go ahead and add a div here around this renderer and give it a class name of flex, flex, call, and full width like that. And below this, I'm going to do the same thing. Update it at. And add a span. Edit it. And let's go ahead and add otherwise null. and the class name here will be text extra small and text muted foreground. There we go, like that. And now if you go ahead and turn this into true, your compacts should also have this. So now to answer the question, when do we use the compact and when do we use the regular variant of our message here? When does the is compact turn to true and when does it turn to false. So basically, if the same user sends a message not even minutes apart, the second message of that user and any subsequent message after that will be a compact message because this is way too much information. I don't need to see this user's name and image so many times if they are writing in a very short interval. So let's go inside of the message list and let's fix that. So what I'm going to do is I'm going to add a time threshold right here. I'm going to add a little constant in my message list now. So constant time threshold will be five, as in five minutes until I am going to fall back to a non-compact message. So let's go inside of this object entries inside of messages.map. I have prepared. I didn't do a return immediately. Instead, I opened curly brackets on purpose and then wrote a return inside. So I have some room here to add some specific code. So const previous message will be messages index minus one. And then const is compact will be decided for the following. If we have a previous message and if previous message.user?id is equal to current message.user?id and if difference in minutes from date FNS, so make sure you import difference in minutes from date FNS, is the following new date message creation time. In the second argument, new date, previous message creation time is smaller than the time threshold. Then isCompact will be true and you can replace the hard-coded false to isCompact. And there we go. So now if I go ahead and for example, send a message, I have an image, but then I send it again. I don't have an image. And again, I don't have an image. So that's what we want to achieve. But it looks like it's not exactly aligned perfectly. So let's go ahead and resolve that. So let's go ahead back inside of my message component here. And not in the is compact, but inside of this one, right? the last return here, let's remove the size 5 from here. And there we go. So now this looks bigger. Let me just see. Should I also remove? Yes. Let me also remove MR1 here like that. So now we have an image here and this is taking exactly the same room as my image above. Great. So this now looks much, much better. And what we can also do is we don't have to type rounded MD every time. We can just go into avatar and change this from rounded full. So the avatar is located inside of your source folder, components UI avatar. We added it from ShadCN. And let's change it here to be rounded MD like this. and let's also so we can keep this uh avatar image aspects where h full w full let's also add object cover here and in the avatar pullback here let's just modify the rounded full to also be a rounded md there we go so now we don't have to type in rounded md every single time right We can now go back inside of the message component individually here, and we can remove all of these rounded MD class names here. And perhaps we can even use this background sky and text white and text extra small. Let's copy that and put it inside of the avatar fallback here. So instead of background muted, we're just going to add that background sky, text white and text extra small. And then you don't need any class names at all. There we go. So I'm just going to go ahead now and try and log out. And I'm going to go ahead and log in inside of my other account. And let me try and send a message here. There we go. Another one. There we go. works perfectly. And I kind of want to increase this avatar instead of extra small, let's just put it SM here by default. There we go like that. Perfect. So now we have that working. What we have to do next is we have to implement a toolbar so that when we hover on an image, When we hover on an individual message here, I want a toolbar here to display. But just before we do that, there is one quick thing that we can do, and that is show an image if the message has an image, right? That should not be too hard to implement. So let's do that and wrap up this chapter. So for that, we have to go back inside of our message component here. and let's first resolve the last return. So we are always going to skip the is compact and let's ensure that the main return has this feature first. So below our renderer here, we're going to have a thumbnail which will have the URL of image. Let's go ahead and create that on the same level as message list and message. So inside of source components, let's add a thumbnail.tsx like that. And let's create an interface thumbnail props to accept the URL, which can be string or null or undefined. Let's export const thumbnail. Let's go ahead and destructure the props. So we just have one, which is URL. and let's go ahead and import everything we need from the dialog because this will also open in a dialog if needed. So dialog, dialog, content, header, title, and trigger. And let's also prepare an X icon from Lucid React here. So first things first inside of this component. If there is no URL, we're going to return null. And then let go ahead and let return a div with a class name which will be relative overflow hidden maximum width of 360 pixels border rounded large my of 2 and cursor zoom in Inside of here we going to have a regular image element we're going to pass in the source to be url we're going to pass in an alt of message image and the class name of rounded and the object cover and you can do size full like this. If you're getting an slint error here, what you can do is you can add the following rule in the beginning. So slint disable next, next, no image element because next.js doesn't want you to use the image element. So you can disable that by adding this linter rule. Go back inside of message now and import thumbnail from dot slash thumbnail. So the reason I'm using a regular image instead of a convex one is because it was just easier to style the way I want it to be styled. So I've been logged out. So let me go ahead and log in again. And now I'm going to go ahead and attempt to send a message. So I'm going to write test here. I'm going to write hello. And let's first send that. There we go. That works just fine. Hello with image. Let's try that. I'm going to go ahead and select a random image that I have. Just a second. There we go. Hello with image. And let's try it out now. So it looks like image is not yet being passed. Let's see why. Where did we do a mistake here? So we probably didn't use the image. Let's see. Instead of a message, I do have the image. Oh, well, the issue is that this is a compact component, right? So, okay, let's repeat it. Let's copy the thumbnail URL and let's add it inside of our is compact just below the renderer here. And there we go. We can now see the image. If you want to try it out on the first one, go ahead and create a new channel. And in the first message, go ahead and try and send something. And there we go. It works in the non-compact one as well. Also try whether it works when you just send like this. There we go. So you can now do all of those combinations. Great. And I want more combination to just check is if it's not compact and if it's only an image. There we go. Everything works just fine. So what I want to do now is I want to go inside of the thumbnail.esx. and when we click on it, let's just enlarge it. So this is actually very simple to do. We don't even need any states. So dialog will wrap the entire thing inside of our thumbnail.tsx. We're going to have a dialog trigger here, which will wrap this entire div here like that. And then we're going to have a dialog content. And this dialog content will have a class name of max with 800 pixels. Border none. Background transparent. Padding zero and shadow none. And now let's go ahead and give it an image inside. And we can just copy the entire thing here. So just copy this entire image. Paste it inside. We're just going to slightly modify it. Actually, no, it will be exactly the same like this. So now when you click here, there we go. And you have the close button right here. Amazing, amazing job. You can now send images and you can also enlarge them. Now let's go ahead and let's add some cosmetics to our message list. So I primarily want to implement a header which will be visible at the top of every last message in each channel. So right here, I want a little hero. So let's go ahead inside of our message list right here. And after we close our dynamic rendering of all of the messages, let's go ahead and let's add a dynamic check. if variant is channel, and if we have channel name, and if we have channel creation time, in that case, we're going to go ahead and render channel hero. The channel hero component will have a channel name, or we can just call it title in this case. Actually, let's call it name, which will be our channel name and creation time. This will be channel creation time like this. Now let's go ahead and let's create that on the same level as all the other components we've done so far. So instead of source components, let's create channel-hero.tsx. Let's import format, not from path, but from date FNS. and let's create an interface first channel hero props to accept name which is a string and creation time which will be a number then let's export const channel hero here to have channel i apologize name and creation time and assign the channel hero props here now inside of here let's go ahead and return a div with a class name margin top of 88 pixels mx of 5 and margin bottom of 4 inside of here a div with a class name text to Excel, font bold, flex, items center, and margin bottom of two. Inside of that, a hashtag and a name like this. Below that, we can add another div. And while we're here, we can turn these divs into paragraphs, right? Let's go ahead and turn this into a paragraph as well. and let's add a class name here, font normal and text slate 800 and margin bottom of four. And inside of here, we're going to add a slightly description of this channel. So this channel was created on format creation time. and let's use MMMDOYYY like this. And let's add a dot. This is the very beginning of the, and let's add a strong element. Again, channel name here, and let's add an empty, okay, let's close this. Like this. And not channel name, but name. So name channel. Like this. And now let's go ahead and go inside of the message list. And let's import the channel hero from dot slash channel hero. And now every channel should have a nice hero message at the top. Excellent. So if you create a completely new channel, hello there. All you can see here is the hero. until I send a message. After that, a message will be visible. Great. So what I want to do next is I just want to go ahead and prepare everything I need here for the message component so it has some further functionality. So let's start by removing some hardcoded things here, specifically the hide thread button. So we can go ahead and change that to be true only if variant of the message list is a thread. So if we are already in a thread, we will not be able to create a reply on a reply, right? So that's why we want to protect this behavior. And now let's go ahead and let's add this editing IDs here. So that will be a very simple state. So we can go ahead and do that here in the beginning. So const editing ID, set editing ID will be use state from React. And by default, it will be null. Make sure you've imported use state from React. I'm just going to move it here to the top like this. And I'm going to go ahead and give it a type of ID, which you can import from convex generated data model here. ID of this will be messages or it can be null. Great. So we have that. And now let's also pass this editing ID and set editing ID to the message here. So the message we'll have is editing if editing ID is equal to message ID. And set editing ID will very simple be set editing ID like that. And now let's resolve the is author. Let's go ahead and make this not hard coded. So we're going to go ahead and get the workspace ID from use workspace ID. You can import this from hooks use workspace ID like this. and then let's go ahead and let's get our use current member which you can import from features members API use current member it requires a workspace id to be passed so we know which member we are looking for so let's add data to be current member here like so and then inside of here is author, what we are going to compare is if message.member.id, my apologies, we can just use message.member.id. So if message.member.id is identical to current member question mark id, then that is the author and it's underscore id. There we go. So we are now ready to go further inside of the message and give it some new features such as a toolbar from which we will be able to edit a message delete a message and add reactions to a message great great job Now that we have all the necessary props inside of our message component let's go ahead and let's implement a toolbar. So the toolbar will appear when an author of a message hovers over their message. The toolbar will appear right here, and it will give the user options to edit the message, add a reaction, or remove a message. Let's go ahead and do that. Ensure that you have all the necessary props as I do and go inside of the message.dsx like this. And inside of here, you should have the is editing Boolean right here in the props, right? Because we are now successfully passing that and we have something to compare it here. So is editing, editing ID is equal to message ID. So what we're going to do now is first resolve it inside of our normal return. So let's go ahead inside of here. And what we're going to do is we're going to go right here before this div ends. And if we are not editing, in that case, we're going to go ahead and render a toolbar component, which we now have to create. So let's go ahead and pass it all the necessary props. Is author will be is author. Is pending will for now be explicit false. Handle edit. We'll go ahead and call setEditingID and pass in the current ID. HandleThread will for now just be an empty arrow function. HandleDelete will also be an empty arrow function. HideThreadButton will be HideThreadButton. Like this. So the only function which we can actually activate already is the set editing ID. So very simple. Inside of our parent message list component, we are keeping track of which message we are editing using the editing ID state. And we are passing that prop inside of the message alongside with set editing ID so that each message can set their own ID as the message we want to edit. So that's why this is the only function we can currently call from this toolbar. And we are passing the ID, which we have as a prop in the message component. Now let's go ahead and let's create the toolbar component. So we're going to create it inside of source components, toolbar.tsx, like this. And let's start by creating an interface toolbar props. Let's add isAlther. which will be a boolean. Let's add a spending, which is a boolean. Let's add all three of our methods, handle edit, handle thread, and handle delete. And let's also add handle reaction, which will be a value and a string. And lastly, we also have a hide thread button boolean. Let's export const toolbar. And let's go ahead and assign the toolbar props. inside of here, let's go ahead and pass in all of those variables from above. So is author, is pending, handle edit, handle thread, delete, and handle reaction and hide the thread button. And I think I somehow missed the handle reaction method from here. So let's also add that handle reaction will just be an empty arrow function as well because we can't do that either. So inside of the toolbar here, let's go ahead and for now just return a empty div and write toolbar like so. Let's go back inside of the message component and let's import toolbar from dot slash toolbar like this. and now you should just see the text toolbar in every message. So let's go ahead inside of the toolbar and let's properly modify it now. So I'm going to go ahead and give this a class name of absolute, top 0, and right 5. Inside of here, I'm going to add another div with a class name, group hover, opacity 100, it, opacity, zero by default, transition on opacity, border, background color of white, rounded MD, and shadow SM. Now, we're going to go ahead and we're going to add a button component from .ui button. Make sure you add that import. and inside of here, we're going to add a smile icon from Lucid React. So we're going to start adding the icons as well. So now when you hover, there we go. You can see that a big smile icon appears. So how does this group hover actually work? Because we didn't mark anything inside of here as a group. Well, remember, the toolbar is rendered inside of our message, right? And our message here has the group class name. This will currently also only work on the main message. So if you send another message, which is compact, this one will not have the toolbar. Only the main message will have the toolbar. So make sure you're testing this out on a main message. You can create a new channel and then just send the first message here to create a main message. Let's go back inside of our new toolbar component. And let's give our button here a couple of props. So the variant here will be a ghost. The size will be icon small. And disabled here will be is pending. We have is pending passed here. That's great. All right. So this is how it looks like now. That's better. And let's give our icon a size for class name. There we go. much better now. So now I'm going to go ahead and copy and paste this and I'm going to go ahead and give this a message square text icon from Lucid React as well. So make sure you have that. So there we go. We now have an emoji and we have the thread icon. And now let's go ahead and add two more. So the next one will be pencil from Lucid React. And last one will be trash from Lucid React. So make sure you have message, square, text icon, pencil, smile, and trash. And now you should have these four icons right here. Great. So let's go ahead and add our hint component here from dot slash hint. And let's go ahead and actually, well, render some useful information about each and every one of these buttons. Let's start with the last one, which is the trash button, which will obviously be delete message. Let's go ahead and go to the one above it. whoops so this one will be edit message then the thread one will be the following so for the thread we're going to say reply in thread and for the smile we're not going to have a hint because we're gonna reuse our emoji popover component so emoji popover from dot slash emoji popover which we've already created for this use case right so let's go ahead and whoops emoji popover here let's add it wrap our button inside of emoji popover. Let's go ahead and add a hint of add reaction. And on emoji select, we'll get an emoji and we're going to pass it inside of handle reaction. Emoji.native, which will be the string, right? So make sure you don't misspell this part. And now inside of here, okay, looks like something here went wrong. So let's see exactly where did we mess up. Inside of emoji popover here, we do have as child here. So that should be correct. Let's see what else could be wrong. I want to refresh one more time. Let me try and click here. Okay, there we go. looks like it was a one-off bug. All right, great. So we can now open the emoji here and we're going to now have to implement all of these methods right here. But before we do that, let's also hide some of them if we are not the author or if we maybe have the hide thread button on. So the reaction button will be available for everyone at all times. The reply will only be available if we are not having the hide thread button. So make sure you put an exclamation point here in the beginning. So if not hide thread button, then go ahead and wrap and show this button. And for the edit and delete message, we're only going to show if is author. So if we are not an author, we're not going to have the options, well, to edit or delete a message like this. There we go. Is author. And inside of here. There we go. So as you can see, I have all of these because I've sent this message. But let me try and maybe going into some other. There we go. You can see that for this other user, I can only add a reaction or I can reply in thread. But for my messages, I have all of these options right here. Great. So what we're going to do next is we're going to implement the edit message functionality. Great, great job. Now let's implement the edit and delete message functionality. So for that, we're going to go back inside of our messages API. So let's go inside of the convex folder, messages. And inside of here, above our get method, I'm going to export const update, which will be a mutation. We're going to prepare our arguments. And as usual, we're going to prepare our handler. So let's go ahead\nand add all the arguments which we are going to need. So we need the ID of the message, which we are trying to update, and we need the body, which will be the new updated body or the value. Now, inside of the handler, we're, of course, going to have the context and the arguments. Let's go ahead and do what we usually do, which is check if we have the user ID. If we don't have the user ID, we are unauthorized. And now let's go ahead and let's get the message by using await context database get arguments.id. If we can't get the message, let's go ahead and, well, this is a mutation, so we can throw new error message not found. Otherwise, let's go ahead and let's get the member. So const member will be await get member context. And then we have to pass in the workspace ID, which we can get from our message. So message, which we just loaded, dot workspace ID, and then user ID in the second argument, in the third argument, actually. If there is no member, we're going to go ahead and throw a new error, member not found, actually unauthorized. That's what we are going to throw. And we can also do this if there is no member or if member ID is not equal to message.member ID. In both of those cases, we're going to throw back unauthorized. And then let's finally do await context database patch arguments.id. And let's simply update the body to be arguments.body. and updated at will get a new date dot now. So we know this is updated. And let's finally just return arguments dot ID back. There we go. That's our update method. And now let's go ahead and just create a hook for that. So inside of features, messages, API, let's copy use create message and paste it. And let's rename it use update message. Let's go ahead and simply request the body. And let's request ID, which is a type of ID messages. There we go. And response type is ID messages. We're going to call this use update message like this. And let's change the mutation to be API messages update. There we go. No more errors inside of this hook anymore. So now let's go ahead inside of the message component. So let's find that inside of source components. And you can find the message component here. Oh, and one thing we forgot to do is we have to add the toolbar inside of the is compact version as well. So I forgot that. So let's go ahead and quickly add it here. I believe it's right here in the end, like this. Make sure you add it in the isCompact, the same way we have it here. So now when you hover here, you should have the exact same thing, right? There we go. Perfect. So now let's actually add that mutation here. So I'm going to go ahead and I'm going to add useUpdateMessage method. You can import useUpdateMessage from featuresMessagesAPI useUpdateMessage. So let me go ahead and add it here. And from here, we can destructure the mutation. And let's call it UpdateMessage. And isPending will be isUpdatingMessage. Like that. There we go. Now let's go ahead and let's create one universal isPending here. And let's go ahead and assign it to isUpdatingMessage for now. Later, we're going to map with something else here. And now let's go ahead and let's add the handleUpdate method. So const handleUpdate method will accept a body inside of an object. So this will be a body string. And inside of here, we're going to call the updateMessage. We're going to pass in the ID, which we have, and body here. And then on success, we're going to go ahead and call toast.success message updated. And we're going to set editing ID back to null. On error here, we're going to call toast.error failed to update message. There we go. Make sure you've added the toast import from Sonar. I'm just going to go ahead and move it above. There we go. So we have that now. Perfect. We have the handle update method. But what I want to do now is I want to conditionally render something else if editing ID is set to true. So right now, I'm going to go ahead and do this. Let's go inside of our last return. So not the compact one, the regular one. And let's do the following. Let's go ahead and mark our first class name as dynamic. So for that, we're going to use the CN from libutils. So wrap this entire class name inside of CN like that. And make sure you've imported CN from libutils. I'm just going to move it here. I'm going to collapse the first class name as the first argument here. And then I'm going to say if is editing. In that case, background color will become F2C74433. And I'm going to go ahead and copy it once more. And just write on hover, background color will be the same. Like this. So now, if I click on edit message, let's see if this works or not. Looks like it's not working. So inside of my toolbar here, handle edit sets the editing ID to ID, right? Oh, yes. Make sure you send it on the, make sure you're trying this on a compact message, but looks like it's still not working here. Let me try once more. Okay. It looks like something is wrong. So let's explore what. Inside of our toolbar here, I'm not actually calling handle edit or handle thread or handle delete. That's what the issue is. So let's go back inside of the toolbar. Let's get our handle edit. And let's use it inside of the button which holds the pencil. So inside of here, I'm going to add on click, handle edit. For the delete, let's add handle delete here. And we have that, we have that. And we should also handle thread here on this one. So on click, handle thread. There we go. So now I'm going to refresh one more time and let's see it now. So make sure you're trying again on the main message, not the compact one. If I click this, there we go. It becomes yellow and my toolbar is gone. Why? Well, because in message.tsx, we only show the toolbar if we are not editing. So in order to remove this, you have to refresh now. But this is what we are going to prepare now. Let's go ahead back inside of our message. and let's import the same way we've imported the renderer. Let's now import our editor components. So const editor dynamic import add slash components editor. And let's add SSR pulse. There we go. We now have the editor component. And now we are dynamically going to render it. So let's go ahead and do this. We are doing this first in the main return function, right? So not the compact one. So we have to dynamically choose between rendering this renderer here or the editor. So what we are going to do is the following. Find where the button for the avatar ends, right here. And inside of here, we're going to do if is editing. We are going to add a div editor, just a text for now. Otherwise, go ahead and render this entire div like this. So this entire logic. so now if you go ahead and switch for example you should see you know your normal messages and when you click edit message it should just say editor so now what we're going to do is we're going to go ahead and give this a class name of full width and full height and inside render the editor component we're going to go ahead and pass in the on submit here to be handle update we're going to pass into disabled to be is updating message or we can just do the general is updating here why not that uh oh we don't have it my apologies is updating message okay and default value here will be json.parse body on cancel will be an arrow function which calls set editing ID to null. And we're going to pass in our variant to update. So that's the power of our editor. Let's go ahead and try this now. So this is what it currently says. Try and send a message here. So I'm going to go ahead and click here. And there we go. You can see how that is now the content of our editor. And also notice how we are not adding the image here because we took care that if the variant is passed as update, we do not allow an image to be passed inside of here. Perfect. So let's just see if handle update here should work. So it should work. And let's also use this pending. So that's what I wanted to do. So instead of the editor let disable it if is pending is true like this And we can also pass the isPending to the toolbar here in the isEditing and also in this toolbar in the compact version Right. There we go. So now I believe this should work. So if I change this to edited message, let's see what happens. There we go. Edited message. And it even has the edited tag right here. Perfect. Now let's enable that for compact message here as well, because currently nothing is happening for the compact one. So first thing we're going to do is we're going to copy this div, or at least the class name, right? That's what we have to do. And let's modify it here, because I think the class names are exactly the same. Yes, flex, flex, global gap one. I think that's true. So now if I go ahead and click this, it becomes yellow. Great. And now what we have to do is the same way we decided to render, you know, this part dynamically. Let's go ahead and do is editing question mark. Let's just write a div here. Editor. Otherwise, render this entire div right here. Like this. And I'm going to go ahead and give this a class name, full width, full height, and render the editor. And you can just go ahead and copy all props from the editor. My apologies. Where is it? It's right here in your main return because we are now doing the compact return, right? So you can just paste all of them here. And it should all be working. So I have this compact message here. And I'm going to click edit message. And there we go. Let me edit it and click save. There we go. Working flawlessly. Amazing. And now let's go ahead and let's add the delete functionality. So the delete functionality, we also have to start by going back inside of our messages API. So inside of convex messages right here. And just as we've done the update, let's do the remove. So I will copy the entire update method here and paste it. And I'm going to rename it to remove. It's going to be very similar, but it's not going to need the body at all. This will be exactly the same. Looking at the message will be the same. We're going to have to confirm whether we have the member. We're also going to have to confirm whether the member is the actual author of the message by comparing the member ID here. And instead of patch, we're going to use delete. And that's it. no other arguments here will be passed, just arguments ID, right? That is the only thing we actually have to look, and we can return back arguments.id like this. Now let's go ahead back inside of our source folder features, messages. Let's copy use update message, and let's rename it to use remove message. And let's remove the body from the request type. Let's rename this to use remove message. And let's change the mutation to API messages remove. There we go. Now we can go back inside of our message component and we can add this as well. So I'm going to go ahead and go inside of my source folder, my components, my message.tsx. And I'm going to copy and paste the use update message and call it use remove message. You can import it from the same location as a use update message. right here. There we go. Let's go ahead and rename this mutate to be remove message and is pending here will be is removing message. While we are here, let's also prepare our confirm dialog and our confirm method from use confirm. You can import use confirm from hooks use confirm. Now let's go ahead and give the use confirm some prompts. So we're going to add delete message as the title. And as a description, are you sure you want to delete this message? This cannot be undone. Like that. There we go. And now let's also add our handle delete method. So const handle delete method. will be an asynchronous method in which we are going to get okay by awaiting confirm, which we've destructured from our use confirm. Make sure that you have the confirm here. And then if not okay, we will break the and exit the method early. Otherwise, let's call the delete message, remove message, and let's call this handle remove. Like that. So remove message and pass in the ID on success here. Let's go ahead and do post.success message deleted. And let's go ahead and do to-do close thread if opened. So if a thread of the same message is opened, we have to close it because that message was just deleted by us. For other users, they're just going to see an error in the thread section, which is okay. And let's do those error here, fail to delete message. There we go. We now have the handle remove method and let's add it to both toolbars. So first in the if is compact, let's pass in the handle delete here. I think this can work just normally like this. And in the one below, let's also do it there. Handle delete. like this. So let's pick a random message now and let's try and delete it. Looks like it's not working. So let's see what exactly we did wrong. So handle delete is a void. Handle delete is a void here. Do we actually add it anywhere? So we should add it to delete message on click and it looks like we are. So why is it not working? Let me refresh and try again. I'm going to click delete. And still, it looks like nothing is happening. Do we have any errors in the console? No. Okay. Let's go ahead and debug one by one. So I'm going to go ahead and see. We have handle remove here, which is an asynchronous method. Oh, I know what's happening. We are not rendering the confirmed dialog anywhere. Let's go ahead and do that. So we have to render it in two places. We have to render it individually in the isCompact return and in the main return below. So let's wrap the isCompact return inside of a fragment here. And at the top, let's simply insert the confirm dialog. There we go. So inside of here, confirm dialog. Now let's do the same thing in our main return. So wrapping the entire thing in a div. My apologies in a fragment. and then simply add a confirm dialog here. There we go. Let's refresh and let's try it out. I'm going to go ahead and pick a compact message. Click cancel. Nothing happens. Click confirm. There we go. Message deleted. And I want to do one quick thing. So I want to add a cool effect while this is deleting. So I'm going to go here in my main return method. inside of this div, which already has the is editing. And let's now add is removing. So is, how exactly did I name this? Why did I name them so differently? Oh, okay. So is removing message. Okay, let's call it like that. Is removing message. And this is what we're going to do. We're going to do a cool collapsing effect, right? So I'm going to do it like this. bj rows 550 slash 50 transform transition or scale y zero origin bottom duration 200 and now look at this cool effect let me just copy this and add it to the is confirmed as well So make sure that you remember to also add all of your features to the compact version of the message. So let's try it out now. And here's the cool thing. If I go ahead and remove this now, you saw a nice little effect. And you can see how this compact message now becomes the main message. And I can click delete again here. There we go. What a cool effect. So make sure that both your edits are working inside the compact messages and also inside of your main messages. Make sure your deletes are working inside of your compact messages and your delete and your main messages as well. Great, great job. Now that we have the ability to edit a message and to delete the message, let's go ahead and let's add the ability to add a reaction. So in order to do that, I want to go ahead and create the reactions API. So instead of the convex folder, I'm going to create a new file reactions.ts. And let's quickly go inside of messages here. And let's just copy the get member helper. I think it's quite handy so we don't have to repeat the code. So let's go inside of the reactions. Let's add it here. Let's import query context from generated server and ID from data model like that. And then let's export const toggle, which will be a mutation, which you can import from a generated server here. Let's define the arguments and let's define the handler like that. There we go. The arguments for the toggle will be a message ID for which we are adding a reaction and also a value, which will be the emoji that we are adding. and now we have to import convex values here. So let's just add this as well. There we go. Now let's go ahead and go inside of the handler and let's add the context and the arguments inside of the parameters here. And let's start with our usual checking if we have the user ID and if we don't, we throw an error and we have to import out from dot slash out like this. So this will serve as a toggle method, right? Meaning that if we have already added the same reaction and we triggered this method again that will remove that reaction right So before we get to that logic first let get the message we are trying to add a reaction to So wait, context database, get arguments.id. My apologies, message id. If there is no message, we're going to go ahead and throw a new error, message not found. And now we're going to get the member from await get member and pass in the context message workspace ID and our user ID. If there is no member, we're going to throw new error here unauthorized. So we don't have to check whether the member is the author of the message in this case because anyone can add a reaction to any message. So first, let's go ahead and let's get the existing message from this user. So I'm going to go ahead and do const existing message reaction from user to be await context.database query reactions.filter. Let's go ahead and add this query to include all of these filters. So query equals query field message ID, the arguments message ID. Then we're going to go ahead and copy this two more times. The second one will be member ID to match our member ID. And last thing we're going to have is the value, which will be arguments dot value. So the reason I'm not using an index here is because I found that it doesn't work well with this value set right here. But we could maybe try it later and see if perhaps I did something wrong instead. And let's get the first like this. And now we're going to do the following. If we have an existing message reaction from the user for this exact message with the exact same value, we're going to go ahead and do await context database and simply delete that reaction. So existing message reaction from user underscore ID. Else, we're going to go ahead and insert that reaction. So context, the database insert into reactions. And we're going to pass in the value to the arguments value. Member ID to be member underscore ID. Message ID to be arguments message ID. Workspace ID to be arguments workspace. Oh, it will be message.workspaceID. And we can technically also use messageID for the messageID like this. There we go. That is our toggle method here. And what I want to do here is always kind of return something. So let's say inside of here, I'm going to return the existing message reaction from the user ID. and inside of here, I'm going to get the new reaction ID and I'm going to return the new reaction ID. So I want my mutations to always return at least something, there we go. Now it's always gonna be an ID of reactions in both cases, if we removed it or if we just created it. Great, so we can now go back inside of our source, inside of our features and let's create a new folder reactions. And inside of here, an API folder, and then let's copy any mutation. For example, messages, use create message. We can copy that and paste it inside of reactions. And let's go ahead and rename it to use toggle reaction. Like that. I'm gonna go ahead and close everything besides that. And in my body, I'm gonna go ahead and accept the value, which will be a string. And I'm going to accept a message ID, which will be a type of ID messages. Everything else will not be required. And this will return back an ID of reactions or nothing like this. Let's go ahead and call this use toggle reaction. And let's modify the mutation here to be API reactions toggle. And once you save this change, there should be no errors in your code. Great. Now let's go ahead and let's actually use this. So let's go back inside of our message.tsx, which is located inside of source components, message.tsx. And we've already added the handle update, handle remove methods. So now let's go ahead and let's add the use toggle reaction, which we can get from features reactions API use toggle reaction. So I'm going to add the mutation here to be toggle reaction. Is pending will be is toggling reaction like this. And now let's go ahead and let's actually add the handle reaction method. So const handle reaction will accept the value of string here. and simply call toggle reaction with our message ID to be ID from this props of the message and the value passed like this. There we go. And the only thing I want to catch is the on error. So in case we are not able to send a reaction, we're going to give an error feedback to the user failed to set reaction, failed to toggle reaction, right? We don't have to add a success message because they're going to see it happen immediately. So now we have to go ahead and add this to our reaction method. Well, not our reaction method, our toolbar, my apologies. So let's go ahead and find both of our toolbars. Remember, there's two of them. So we have to add it here and we have to add it in the main return here. handle reaction like this. So what we can maybe already do, instead of this message, we should have all the props, including reactions, but we don't have a way to render them properly yet. So this is what I'm going to do. Inside of my compact, let's go inside of if is compact here. I'm going to go ahead and find this updated at and span. And below that, I'm going to JSON on stringify my reactions. So I'm kind of going to use a dirty way of showing all reactions on a message. And now do the same thing instead of your main return. So after this conditional updated at which renders the edited text, simply render a stringified reaction model. So there we go. You can see that now all of my messages have empty arrays here. So if I now add a reaction, Let's see what happens. There we go. You can see the exact value of the reaction and you can see exact members that added this reaction right here, right? So if I go ahead and click on it again, what should happen is that it should get removed and it does get removed. Perfect. And if I change it, you know, to any other emoji, there we go. It changes to that other emoji. Perfect. Exactly what we wanted. So now what we have to do is we have to implement the actual reactions bar. So let's start that by doing it inside of our main return method here. So we're going to replace the JSON string of reactions and add an actual reactions component here. And for these reactions, we're going to pass in, let's go ahead and call this data to be reactions. And on change, here will be handle reaction. Let's go ahead and now develop the reactions in the same components folder, reactions.dsx, like this. Let's create an interface, reactions props, which will accept the data to be an array of omni. and what we're going to emit is document from convex of reactions and we're simply going to emit the member ID from it and then we're going to extend the entire thing by having a count of number and member IDs of ID which comes from the same import as our doc members and an array like that and our what would we name this on change will accept the value of string and return a void. There we go. So basically we are doing inside of here, you know, the exact same thing that we handled, I believe, inside of the message type here. There we go. You can see it's exact same thing here. So you can copy it from your message props. Okay, we now have that. And now let's go ahead and export const reactions. let's assign the props reactions props let's destructure the data and on change here and now what we're going to do is actually render out our reactions so let them return here reactions so let's go inside of the message.dsx component and let's import reactions from dot slash reactions, the same way we did with our toolbar thumbnail, for example. And let's immediately add it in both our compact and non-compact components. So it's already in the correct place in this main return. So just after thumbnail and after the edited, that's when we're gonna add the reactions. And I want you to copy that and do the same thing inside of your first if is compact edit here. So instead of JSON stringify reactions, after thumbnail, after updated add, simply add the proper reactions import here. So now all of these messages should have a big reaction text below it. Now let's go ahead and actually render our reactions here. So what we're going to need is also to know if we are the ones that added a reaction. So for that, we need a workspace ID from use workspace ID. You can get that from the hooks use workspace ID import above. Then we're going to need the data from use current member. You can import that from features members API use current member. and let alias this from data to current member like this and pass in the workspace ID And you can rename this to member since that the only member we going to have I believe You know what? Let's be specific. Let's write current member just in case we modify something later. Okay. And now what we need is the const current member ID. So I'm just going to alias that to data underscore ID like this. If we don't have reactions, my apologies, if data.length is zero, or if we don't have the current member ID, and this is not data, this is current member, like this. So if we don't have current member ID or data.length, we are going to return back null, like this. So now, these messages that don't have reactions don't have the big reactions text. Only those that have reactions have it. So if I add an emoji here, there we go. It has a big reactions text here. Now let's go ahead and actually render the reactions. So for the main container here, let's add a flex, item center, gap one, margin top of one, and margin bottom of one. Now inside, let's go ahead and add reactions.map. I apologize, data.map. Let's get the individual reaction here. and let's add a native button element, which will simply have the reaction.value inside. So there we go. Now we can see the reactions. So if I go ahead and add a new reaction here, there we go. You can see how they appear next to each other. But we still have no idea which reaction was the one we added. And we don't know how many people added the same reaction. So let's go ahead and develop that. So besides the reaction value, we're going to add a span, which will do reaction.count. Like this. So now you can see the number one next to each reaction here, right? So if you go ahead now and log out, for example, and log in with another user, which has access to the same workspace. and if you add the same reaction, right? So let's say this heart eyes, there we go. Now it says two. So it doesn't add it, you know, as an additional heart eye and one. No, it combines all member IDs that reacted with the same value. Great, so that is working, perfect. And now let's actually go ahead and style this properly. So we're going to need to import CN from libutils. So import CN from lib utils. And let's go ahead and give this button a class name of CN. The first value will be our default value. So height of 6, PX of 2, rounded of full, background slate 200 slash 70, border, border transparent, text slate 800, flex, item center, and gap x of 1. There we go. Now they're looking much better. And now let's do the following. If we have added a reaction to it, which we are going to know like this, If reaction in the current iteration, dot member IDs includes our current member ID, which we have as a constant here. In that case, we can go ahead and render our background blue 100 with a 70% opacity, border blue 500, and text white. there we go so this is how this looks now but i don't believe this text is correct so text should also be blue 500 there we go so you can now see which reactions you have added great uh and yeah we actually we can leave this text at white because we're going to manually change this span here so let's go ahead and do that as well. So instead of this span, go ahead and give it a class name as well, which will be CN. So by default, text extra small font semi bold and the text muted foreground. That's going to be the default. And then if reaction dot member IDs includes current member ID. In that case, text will be blue 500 like that. There we go. This is looking much, much better now. So let's go ahead now and also add the ability to add a reaction from clicking on existing reactions because currently we can only do it, you know, by adding it through this model here. So now let's add the ability to add it by simply clicking on it. So for that, we already passed the on change method. So what we're going to do is we're going to extend this button to have an on click method. And very simply, it's going to be an arrow function, which calls the on change and passes in the current reaction value. So you can easily add yours as well. So if you want to add another kiss here, there we go. You just click on it. want to remove it, you just click on it and it toggles back. Amazing. So let's go ahead now and continue developing this even further. So what I want to do is I want to add a hint around this entire button here. So let me just go ahead and do this. There we go. And let's import our hint component. So let's import hint from dot slash hint. I'm going to change this to slash components hint like this. And let's wrap the entire button inside of a hint. So this entire thing. And let's give it a key of reaction ID. And let's give it a label, open backpix, reaction.count, reaction.count is equal to one person, otherwise people, reacted with reaction.value, like this. So now when you hover, you can see how many people or person reacted with a value. And what I want to do next is also add a little plus icon here to choose an additional emoji without going here. So that's going to be quite simple. At the end here, let's simply add an emoji popover component. You can get this from .emojipopover like this. and go ahead and add a hint of add reaction. On emoji select, get the emoji and pass in the on reaction to be, whoops, on change, to be emoji.native. And inside add a button and then end the outline add reaction. You can import this icon. from react icons and d like this so now you have this plus to add a reaction and now let's just style the button first let's give the icon a class name of size four and let's give the button a class name of height six px three rounded full background slate 200 with a 70 percent opacity border border transparent cover border slate 500, text slate 800, flex items center and gap X of 1. Like this. There we go. And you can now go ahead and add an additional reaction, for example. And it will only appear in these messages, which already have some reactions. There we go. You just implemented reactions. Amazing. What you have to do now, what we have to do now is implement reply in thread functionality. And then we are ready to implement one-on-one conversation. And that will be the end of all the big main functionalities that this Slack clone has. Amazing, amazing job. So one thing that we have to develop before we go on to threads is infinite load. Because if you, for example, go inside of source, features, messages, API, use get messages, and change the batch size to three, and I refresh here, you can see that only three messages are loaded. But we know that there are more messages. So only the latest three messages. So this is the latest, right? So this will then be appended to the last three. That's not an issue, right? But if I refresh now, you can see that the one above is now missing because we only load the last three messages. In this example, we're going to turn this back to 20. But I suggest that you put it to a lower number now so that you can see this issue. And now let's actually implement a button which will load new messages. And then we're going to turn that into infinite load, which is not a button, but just a viewport observer reaction. So let's go ahead inside of our components here. And inside of here, we should find the message list right here. And what I want to do is I want to go to the end of this object entries, right? This entire thing. So right before we do the variant channel. And inside of here, I'm just going to add a div here and then a little button inside load more. And I'm going to add an on click here and just call the, you know, load more. And we should have the load more inside of our props here in the message list. So inside of your, I believe, page in the channel ID, you should have the use get messages where you destruct load more and you pass it here to the message list. So now I have this button load more. And there we go. You can see that it loads more in batches of three messages. until it gets to the end. Perfect. So that works. And now let's go ahead and import\nprove this by also adding a little loading indicator if we are loading more. So I'm going to go ahead and do this. If is loading more, which should also be available to you as a prop here, and it should be passed here through status loading more, we're going to render a component we already have, which is this div right here, this HR element, and then this span format date label date key. So basically this, right? I want to copy that element. So let me copy that. Let me go back here. Is loading more? And I'm going to render that. But inside of here, I'm just going to add a loader from Lucid React. And I'm going to give it a class name of size 4 and animate a spin. Make sure you've imported the loader from Lucid React. I'm just going to move it here. so now let's refresh everything i only have three messages and when i click load more there we go you can see a nice little indicator that it's loading more messages it's very fast right but the reason i want it to appear like that is because this will not work on a button but instead this will work on an observer so now let's go ahead and do the following let's go and change this div to instead be a self-closing div. And let's give it a class name height of one. So it needs to be visible inside of our viewport. And then let's write a ref. Let's get the element. If we have the element, we define the observer to be new intersection observer. inside of the argument here in the array, get the entry, open the function, if entry is intersecting, and if we are able to load more, so can load more is also a prop you should be having in the message list component, can load more, it should be passed as status can load more in your channel ID page. So you should have this as well. So if we are intersecting with this small little pixel here and we have the ability to load more, we are simply going to load more messages. And then we're going to pass in some additional options here. So threshold will be 1.0 like this. And then let's simply do observer dot observe and pass in the element ref, which we get from here. And let's go ahead and add a return here. Observer dot disconnect. So we have a proper cleanup here. There we go. So now if you refresh, you can see how my messages are loading when I'm scrolling. So I'm going to go ahead and zoom in a lot. So it doesn't immediately load all messages. So now when I go up, you can see how it loads more messages until it reaches the end and all messages. There we go. So now no need for the button because you are for sure going to have all of your messages loaded, right? You can see how when I go up, the more messages are loaded. Perfect. And you can now safely go back to use get messages and change this batch size to 20 because we are able to load more than 20 messages at once. You can even put it to 100 if you want to. But the lower you put it, the more you're going to see that infinite load effect. Great, great job. Now let's go ahead and let's implement the thread functionality. So in order to do that, let's go ahead and let's add a package. One add NUQS or NPM install NUQS or NUX. Let's go ahead and call it like that. It's simpler and faster. So now let's go ahead and let's create a hook, which we are going to use to control the parent message ID. So I'm going to choose the URL state to control the parent message ID. The reason I want to do that is, first of all, it's extremely simple. It's reliable. And also because it will reset on every subsequent channel change here. So I don't have to manually turn off the selectant parent message ID and then keep track of, is that parent message ID in the right channel? And all of those confusing things. right? So I'm going to go ahead and use that inside of my URL state. And that's why we just installed the Nux package here. So let's go ahead and see how this package works. So I'm going to go ahead inside of my features, and I'm going to go inside of messages. And I'm going to create a new folder called store. And inside of here, I'm going to create use parent message ID.ts. I'm going to import use query state from Nux and I will export const use parent message ID here to be an arrow function which very simply returns use query state and then the actual query key which will be inside of the URL. So I'm going to call this parent message ID like this. So thanks to this package Nux, we will now have our usual, you know, const parent message ID, set parent message ID, use state. But this time, thanks to Nux, it will be automatically synchronized to my URL, right? So something like this, parent message ID will be null. So if this is null, this will be my URL. If I change this to 1 to 3, my parent message ID will change to 1 to 3 as well. Or if I manually change this to 3 to 1, my state will change to 3 to 1. So it's going to be directional here, and that will help us a lot in managing our state. You can, of course, implement this yourselves, but honestly, this is a great package, And I am considering using this in every future project that I will be building. So that's how this package will be used. And I just want to separate it in its own hook here in its message feature. And now let's add it into one more reusable hook, which we're going to create inside of source hooks. It's going to be general, like undefined for what exactly we're going to use it. So let's add usePanel.ts. And inside of here, I'm going to import our use parent message ID from features messages store use parent message ID. And I will export const use panel here. And now inside of here, I will get the parent message ID and set parent message ID to come from use parent message ID like that. And then I'm going to add const on open message. We will get the message ID, which will be a type of string. And in here, we're going to simply do set parent message ID and passing the message ID like that. And on close, we'll simply do set parent message ID and revert it to null. And let's go ahead and let's return the parent message ID here and on close and also on open message. There we go. So now that we have that, let's go ahead and let's use it inside of our layout for our workspace. So let's go inside of the app folder, workspace, workspace ID. And inside of here, we have the layout with our resizable panels. So what I'm going to do now is I'm going to add a constant use panel from hooks use panel. So let me just move it here. and in here I will destructure the current panel message ID, parent message ID and on close. And I'm going to add const show panel to be a boolean if we have the parent message ID like this. So later on I'm going to add some more elements here, right? So that's why I'm adding it in this way. But for now we just have this option. So I'm going to show the panel if this is true and this double exclamation point turns a string into true or false depending on if it's falsy or truthy. So now let's go ahead and actually render this resizable panel here. So let's find our last resizable panel here which renders the children and then let's do if show panel is active go ahead and add a fragment and add another resizable handle here with handle and add another resizable panel here. This resizable panel will have a minimum size of 20 and a default size of 29. And inside of here, we can just say load thread like that. So right now, nothing should really be changed. But if you go inside of your URL, So I'm going to try and do this manually now. And if you append a parent message ID, let's say one, two, three, there we go. You can see that now we have a third panel here. So our Nux here detected inside of usePanel, inside of useParentMessageID, which is basically just this, detected that we now have a parent message ID. And then that turned the showPanelConstant into true, and that rendered another panel here. Great. So now we're going to reuse all of those components which we've created, the message, the message list, the chat input, the editor, the renderer, and we're going to create a mini chat inside of this panel right here. So one more thing that we have to do here is actually open this up when we click on a reply in thread, right? So I just wanted to demonstrate to you how I do it manually, you know, by adding something in the ID. But now let's go ahead and let's do it by clicking reply in thread here. So let's go inside of our message.tsx component, which is located inside of source components here. Let's find the message. And above the use confirm, let's go ahead and let's add our use panel method from Hook's use panel. So let me go ahead and move use panel here. And now inside of here, let's go ahead and let's get on open message like this. And let's also add on close. And now let append the on open message to two toolbars that we have So first one is in the is if is compact return So find the toolbar and modify the handle thread to call the on open message and pass in the ID of the message And do the same thing for your toolbar below. There we go. And one more thing that we have to modify is if we deleted the message. So I'm going to go ahead inside of my handle remove method here and I have a to do close thread if opened here. So what I'm going to do is, first of all, also get the parent message ID from use panel. So I'm going to do if parent message ID is equal to the ID we just deleted. In that case, let's call on close. So we close that thread. So that's in handle remove, remove message on success here. Now let's try it out. So I'm going to refresh. I don't have anything inside of my URL and I will click reply and thread. And there we go. We now have a new resizable panel right here and we can go ahead and now load the actual content inside. So let's go back inside of the layout.tsx. And first of all, let's create a default loading state inside of here. So the loading state will be a div with a loader from Lucid React. So just make sure you've imported the Lucid React loader here. I'm going to go ahead and give this loader a class name of size 5, animate spin, and text muted foreground. And I'm going to give the div a class name of flex height full, items center, and justify center. So by default, if there is an additional panel opened, it will be a loader like this. But if we have the parent message ID, in that case, we're going to go ahead and render a div and thread inside. Otherwise, we're going to go ahead and render the default loader here. Like this. So let me just improve this by adding the parentheses up there. And there we go. Now it's more readable. And I will just join these two together like that. So if we have the parent message ID, we will load the thread. Otherwise it's a loader. So that will be an edge case. It should never technically happen, but you know, just in case we will have a fallback here. And now let's go ahead. And instead of rendering this div, let's read, render the thread self closing component, which will have the following. The thread will have a message ID, which will be a type of parent message ID as ID. Make sure you import ID from data model in convex. Now remember, inside of our use panel, use parent message ID, this is by default a string. And Nux gives you the ability to add custom types, but not to the extent where you can add, for example, the type we need ID from convex, right? So technically that is a string and we can leave it as a string here. That's completely fine. So what we're going to do is just add as ID of messages here. That's how we're going to resolve that issue. And let's go ahead and add on close here to be on close. Of course, you know, types will not mess up your code. It will just mess up the limter here. So let's go ahead and create the actual thread component. So that will be located inside of features, messages inside of here. And let's go ahead and let's create a new folder components. And inside, let's create a thread.tsx like this. And inside of here, I will create an interface thread props with a message ID, which will be a type of ID messages. and on close, which will be a void method. And let's export const thread here. Let's go ahead and let's destructure the message ID and on close method from thread props. And inside of here, let's go ahead and return the basic thread inside. So I'm going to go ahead and give this a class name of full height flex and flex call. Inside of that, I'm going to add a div with a class name flex justify between items center, a heading of for and border bottom. And then I'm going to add a paragraph thread. And then I'm going to add a button from components UI button. So make sure you have this as well. And the button will simply be an X icon from Lucid React, which will have a class name, size 5, stroke 1.5. The button will have an on-click of on-close, a size of icon small, and a variant of ghost. and inside of the thread here we're going to have a class name text large and font bold. So now let's go inside of the layout and let's actually import the thread from features messages components thread. I'm going to go ahead and move this here like that. There we go. So we should now have the thread component loaded here and there we go. So I should now be able to close it as well. So now if I click here, that opens a thread. And if I click again, it closes the thread like this. There we go. So we can now control that. And now let's actually load the message that we have selected when we click here. And now that I think of it, I think it would be cool if the thread header had the same height as my chat header here. I think that would be cool to match. so let me just see inside of my thread here I just have a padding 4 set here but inside of my header component inside of workspace workspace id channel channel id header I think inside of here I have defined the height yes I did so height 49 pixels and px of 4 let's do that in the thread And so instead of P4, it will be PX4 and it will have a defined height. There we go. This is now much, much cooler. Great. And now let's go ahead and let's load that initial message. So we're going to go ahead back inside of the thread here and let's prepare our use get message. If we even have that hook, let me just quickly check. We don't. We have used get messages, but we are missing the individual get by ID. So let's go ahead and create that. So we have to go back inside of our convex folder and inside of messages.ds. And we have the global get method here. Now let's create the get by ID. So export const get by ID will be a query, which will accept arguments and ID, VID messages. and then a handler, which will be an asynchronous context and arguments method here. And we're going to do the thing we always do, which is check if we have the user ID. Then we're going to go ahead and load the message using await context database get arguments.id. If there is no message, we're going to throw new... Actually, this is a query, so we're returning things, right? We're not throwing errors. So return null, return null like this. Then let's go ahead and let's get the member. So const member will be await populate member, which will be context and message dot member ID like this. If we don't have the member, we will return back null. Then we need the user. So const user will be await populate user context and member user ID. If we don't have user again, return null. We can't populate this message completely in that case. And if we have all of those, what's left is to get the reactions. So const reactions here will be await populate reactions context and message underscore ID. and then what we have to do is we can copy from our get method here how we do reactions with counts so we can copy this reactions with counts and let's add it to our get by id method here like this there we go and now we have to add the deduped reactions so that's in the same method here but below it so const deduped reactions and let's copy the entire thing with this initial value and the type here. So let's go ahead and add that and see if perhaps we're going to have to do some changes. So back inside of the get by ID here. There we go. It looks like it works perfectly fine. It only uses the reactions with counts, which uses the reactions. Everything else here is pretty normal. Great. So we now have reactions added. Let me just see where this ends. So it ends here. Great. And now we also have to copy the reactions without ID property, without member ID. There we go. Here it is. So let's add that as well. And now we have a one-on-one match of how this message would look like in the get all messages API endpoint. It will look the same now and be populated in the same way when we individually load it. Great. And now let's go ahead and do return. Spread the entire message. Image will be message.image. If we have it, We're going to do await context storage, get URL message dot image. Otherwise, image will be undefined. Then we're going to have a user, a member and reactions, which will be reactions without member ID property. And this will just be member, not member ID. There we go. And what I also want to do here is just prevent non-member users from accessing this. So I'm going to add const current member here. Do we have a shorthand for that? Yes, we do. Get member. So we can pass in the workspace ID and the current user ID Let do that So let go inside not inside of remove not inside of update There we go Inside of get by ID once we get the message if we confirm that the message exists, we're going to do current member is await get member. We're going to pass in the context message, workspace ID and the currently logged in user ID. If we don't have the current member, that means you cannot load this message, right? Because you don't have permissions. You're not part of this workspace. So if you somehow hit this endpoint, we're not going to give you anything back. And then we can continue with populating the member of the list. So this is the current member, which is trying to access the message. And this member is the member that actually wrote the message. So we are populating that member and that user. But we don't have to populate the current member. We just have to confirm it exists. Great. So we now have get by ID. So let's go ahead inside of features, messages API, the same way we had use get messages, copy and paste it, and now create use get message like this. Go inside of use get message, rename the type to use get message props, and add it here, use get message props like this. Oh, and I picked the one that uses the paginated query. Let's go ahead and just use any other. is going to be simpler. For example, channels, API, use get channel. Just copy everything inside, go back inside of your messages, API, use get message. Make sure you are inside of this new one. Don't accidentally override your use get messages. So be careful. And you can remove everything from here and paste the use get channel and rename this to use get message. It will accept the ID of messages, add the use get message props here. This will be use get message. And these will be API messages get by ID. There we go. We now have use get message individually inside of our features messages API. We can now go back inside of our thread component. And we can go ahead and get use get message, individual message, right? ID, message ID. So that's why we had to type the message ID to be an ID of messages because our API will only accept that kind of type, right? So that's why inside of my layout here, I had to use this as ID messages because by default, this is just a string, which is technically correct. This is also a string, but it also gives convex some specific context, which we need. So that's why we're using that kind of trick. Great. So inside of here, we're going to have data and we're going to have is loading. So let's go ahead and alias that to something else here. So I'm going to have the data to be my message and is loading will be is message loading or simply loading message. Now let's go ahead and let's add some loading states here and some error states here. So I'm going to do if loading message. You can go ahead and return the exact same thing we had, for example, here in the workspace ID layout. So just this loader that we keep using. I probably should have created a reusable component for this at this point. So feel free to do that if you want to create a reusable component for this kind of loader. So if it's loading message, we will display a loader here in the thread. If there is no message, let's go ahead and do the following. So I actually want to copy this entire thing here like this. And then outside of this div here, I will just add and copy this, for example. and instead of loader, it will be alert triangle from Lucid React. So make sure you've added alert triangle here. And we are not going to do animate spin here. And we're going to add flex column and gap Y of 2. And below the alert triangle, we're going to add a paragraph message not found. So this will happen in case it's been deleted while our thread was opened. So let's add the class name here, text small and text muted foreground like this. And I think that should be enough. So now let me also go ahead inside of the individual message here outside of the div. I'm going to do JSON stringify the message that we just loaded. There we go. So this is the message now that we load here. So when I go ahead and click this, there we go. You can see all the attributes and thing. And now if you go ahead and delete a message, oh, what actually happens is that we get that caught inside of our message.dsx. There we go. We close the panel if the parent message ID from the URL is active to the ID which we just deleted. So if I comment this out, for example, and refresh my page and just open a random message here, and then I delete that message. Is this one? Yes, basically. So if I click delete message, there we go. Now it will have this kind of state message not found. And I think that's a pretty cool state to have here. Great. And I also actually want to have one more case. So let's bring this back. Instead of thread here, let me go ahead and change this to true. So I want to see this loading state. So this loading state is fine, but it doesn't give me the ability to close this. So what I want to do is I want to copy the entire thing from if there is no message and just add it here. And then let's just bring back the loader here, remove the text and add animate spin back. So now it will be loading, but you also have the ability to close it in case it's been loading, you know, for too long here. Like that. So I want you to always have the ability to close your thread from here. So bring this back to loading message. There we go. Great. So what I want to do now is I want to be able to actually load the individual message. So let's go ahead and do that in our main return here. So instead of this div JSON stringify message, we're actually going to go ahead and render the message component from components message. So our reusable component here, and we're going to give it all the properties we need. So first of all, we're going to activate the hide thread button because we are not going to be able to open another panel from here, right? So hide the thread button. The member ID will be message member ID. We're going to have author image to be message user image. we're going to have author name to be message username we're going to have is author for now let's just put it to false body will be message body image will be message image created ad will be message creation time updated ad will be message updated ad ID will be message underscore ID. We also added the image. We have this. Okay. Reactions will be message dot reactions. Is editing will be false. And set is editing. Set editing ID will be an empty arrow function for now. So now you should have something of a message here. There we go. So you can see that now we can at least see the message that we are planning on replying here. So it's not exactly perfect, but it's visible right here. Great. So now let's go ahead and let's give it some more options. Now let's add the necessary properties so that we can properly add the is author and is editing fields inside of this message, which is now nicely displayed here every time we click reply in thread. So I'm going to go ahead and I'm going to load the current member. So const data will be current member. Use get current or is it use current member like this? And let's go ahead and pass in the workspace ID. Let's get the workspace ID from use the workspace ID here. So make sure you've imported both of this. Like this. And I will just change this to features, messages, API, use, get message. So I can separate the features imports. Now we have the current member here. And what we're going to do is we're going to go inside of the individual message and we're going to check is author to be if message dot member member ID is equal to current member ID. And let's add a question mark here. So now we will be aware if this is an author or not. So I can you can see from here, I can edit and delete my message. But if I open someone else's thread, then I can just add a reaction here. But it looks like we do have some issues regarding this emoji. Does it happen all the time? So here it doesn't happen. Here it also doesn't happen. I don't know if this is like an edge case. I will explore that a bit further. Maybe we got something useful in here. I don't know, but it looks like it goes away after refresh, but we can certainly come back to this. I will try it out every now and then to see what's happening with this. Great. Okay. This is fine. Let's see if adding a reaction here works. There we go. So I can add a reaction from here. I can add it from here. I can remove it from here and I can remove it from here. Perfect. What I currently cannot do, I believe, is let's see, can I delete the message from here? I can delete it. Awesome. But can I edit the message from here? I cannot. Okay. So let's enable that. So we're going to add the editing ID inside of the thread here. So const editing ID, set editing ID will be used state and null by default. And it will accept an ID of messages or null And we have to import used state from React So let me just move that here to the top Editing ID and set editing ID And now we can go ahead and pass in the is editing here to check if editing ID is equal to the message ID And set editing ID can be set editing ID here. Let's try it out like this. So now if I click here, rename in, sorry, edit in thread and click save. There we go. We can now edit in thread as well. Perfect. So we now finalized the thread load the message functionality. So what I want to do next is I want to load the editor here and be able to send a reply to this thread. Great, great job. Now let's add the editor component to our thread sidebar here. So let's start by importing our next dynamic import. So import dynamic from next dynamic. And then let's go ahead and define const editor to be dynamic import add components editor SSR bolts. There we go. We now have our editor component. We're going to add it here. So after this encapsulating div around the message, let's add a new div and let's render the editor inside. Let's give this a class name of px4. Let's go ahead and pass in the onSubmit method for now to be an empty arrow function. Let's go ahead and give it a disabled of explicit false. Let's give it a key. Well, we can't do anything yet. So let's just keep it like this for now. and a placeholder of reply like that. So now already, there we go. We should have the reply input right here. Excellent. So now let's go ahead and let's actually implement some of the methods here. So the first thing I want to add is I want to add the create message and use generate upload URL hooks here. So I think it's very useful. if we open up chat input component, because we could copy and paste a lot of things from here. So chat input inside of source app, workspace, workspace ID, channel, channel ID, chat input. So we can actually start with all of these things as well. Let's copy these two states, editor key and is pending. And let's add it to our thread component here. Like this. Then let's also add the editor ref here. Let's import use ref from React and Quill from Quill. So I'm going to move Quill import here. Like that. Besides editor ref, let's import these two. Create message and generate upload URL. So I'm going to add them below these two. And maybe even separate them because these are queries and, you know, these are mutations. So let's at least separate them like this. We also have the add the imports for use create message and use generate upload URL. So let me go ahead and copy these two as well. I'm just going to add them here. There we go. So now we have editor key is pending editor ref, create message and generate upload URL. Now let's also go ahead and copy the create message values here and let's add it below our thread props and let's lightly modify it. So we're going to need the channel ID, the workspace ID, but also the parent message ID, which will be an ID of messages like this, because this will be a reply. Let's also add the channel ID here from use channel ID hook. So let me just move the hook here. There we go. And I believe we now have everything ready to re-implement our onSubmit method. So we can copy that as well from the chat input component here. Let's copy the handleSubmit entirely here. I'm going to copy it. And let's see if we have anything missing here. It looks like we only have the toast missing, which is great. Okay, let's go ahead and do that. Let's import toast from Sonner. Whoops. Is it toast like this? There we go. Toast from Sonner. And what's missing in this handle submit method in the, you know, thread component, which we've just copied from the chat input, is that inside of the values here, I'm not passing the parent message ID, which I have clearly defined here that it's required, right? So it's not required in the general chat input here, but it is required here because this is the thread response, right? So let's go ahead and add that to our values here. So after workspace ID, let's add parent message ID to be the current message which we have just loaded. Or we can just use the message ID here. So you can either use the data message ID or you can use the message ID and add a comma here. There we go. So now we should be able to add replies. So let's see if this is true or not. The problem is we will not be able to see that yet. So if I go ahead and add hello reply and press enter. Oh, and I think there's one more thing that I have to do here. Maybe I have to also pass this a variant of thread. No, okay, nothing. My apologies. I confused the two. so if i go ahead and press this nothing happens because i never passed on submit so let's go inside of here and let's actually pass handle submit here a key of editor key a disabled of a spending and inner ref of editor ref so you should have all of this the editor key because we copied it the handle submit which which we just modify the editor RF here and is pending, which is controlled here. Let's try this again. So I will now go ahead and also prepare my data here, my messages. And I hope these are sorted by creation. So I'm going to go ahead and add a reply here. There we go. So no errors, which means this worked. And hopefully it also passed the parent message ID here, right? Hopefully, this was also submitted. And what I can already see is that it didn't appear here in the main get messages, right? So if you go ahead and check out the, let's go ahead and do this. Where is it? My channel ID page.dsx. Inside of here, we use the use get messages and we pass in the channel ID. Let's see, do we have inside of API messages get, do we actually have the logic to filter out those without parent message ID? Great. So we use the filter here. We basically pass this as null. Okay. Great. Now let's go ahead and try out and see, are these my latest messages? I'm not sure, but looks like they are because in this one and not a single other, I have the parent message ID. And here it is. Here is the message. Let's see what this message says. Edit in thread. And let's see. There we go. That was exactly the message I replied to. And what I replied was, I think, reply or something. So if I go back to messages here and click on this body, reply here. Perfect. So it is definitely working. What we have to do next is we have to add our own little version of message list here. So it actually loads the reply messages, but also does the whole infinite thing loading, the infinite loading thing. So in order to load the replies here, I want to close everything besides the thread component and open the message list component. We could technically reuse the message list. The problem is it will cause some scroll issues, right? So it's a very nitpicky reason from my side why I don't want to reuse it. And, you know, it's definitely a better practice to reuse this component, which we created to be reused. But, you know, it just happens that it scrolls in such a weird way. and it's hard to modify the message list component to handle both the channel variant, the conversation variant, and also the thread variant with featured message on top. It can be done, but I honestly just think it's easier to copy some features here from the message list inside of the thread here and we're gonna have our messages here. And later you can play around and try and reuse the message list component in a certain way. So let's go ahead and do that. So I'm going to go ahead and go inside of the thread and let's do the use get messages here. So I'm going to get const data messages. And actually, I forgot that use get messages is a paginated query, right? So instead of our use get messages, which we can get from here or with at slash features messages, API use get messages, depending on where you want it, we are actually returning some other things. So we are getting the results, status and load more. And inside of the use get messages, we have to pass in channel ID and also parent message ID, which is message ID. So this Parent message ID is very important because I want this to only load the replies to this message ID, which we open, right? I don't want it to load everything else, just the replies for this, for example. So now let's also define the can load more and is loading more constants. So can load more, is loading more the same way we do in the message list here, I believe. Where is it? Oh, I think we actually do that in the channel ID page, right? So we are basically doing this, right? Right. So we have that, we have load more, we have these two, and now let's go\nHead and let's implement our grouped messages trick so we can group them by date. So let's go inside of the message list and let's copy the grouped messages here. You can copy them. And before you do your first return here, go ahead and add grouped messages. Now, this will not be data. Instead, it will be results like this. And we have to import. Well, first, we have to change this to be a type of results as well. and we have to import the format from date fns so let's just go ahead and move this to the top like this we have grouped messages now and let's also modify this if loading the individual message which we are replying to but also if the status is first loading first page in any of those cases So if we are loading all replies to this, also show the little loading indicator, right? Okay, so we now have grouped messages here, which is great. We can now use them. So what I want to do now is go here where I have my div, which encapsulates the featured message. And let's modify this div now. Where is it? Okay, let's modify this div. So it has this flex right here, right? So flex one, flex, flex call reverse, padding bottom four, overflow Y auto and messages scroll bar. So I'm going to add this to this div, which is encapsulating the individual message. So right now this is set at the bottom because we use flex call reverse. So what we're going to do now is we're going to add the messages here in the code above the featured message. But in reality, it will be below the featured message. Quick reminder, we are using Plex call reverse because we are loading the messages in reversed order, right? So we are not loading more content as we scroll down. We are loading more content as we scroll up, right? So that's why we do this logic. So now what I want to do inside of here is, well, go inside of the message list and copy our object entries thing, right? We can copy the entire thing, right? the entire object entries and we have grouped messages. We're going to have to add this format date label here. We have to import the difference in minutes, I suppose, and we already have this component. So let's see what happens. I'm going to go ahead and paste that here. I'm going to indent it and let's fix error by error, bug by bug. So format date label is missing and I think we can just copy that from here. Format date label. You can also, you know, add it in a reusable utils file. So let me just add format date label here in my thread component. Let me import is today. Let me import is yesterday. So all of this from date FNS. And that should fix one error. So let me go here to see. There we go. So we can now load this. And now inside of here, we are missing the difference in minutes, which is just an import from date FNS as well. Besides this, we are missing the threshold. So let's go ahead and copy it from the message list here. And let's also add it here. I'm going to do it above the thread props. There we go. So that resolved that issue. And now we have the hide thread button, which we can just, you know, set it manually to true because we know we are going to remove it. Everything else I think should be just fine. We have the editing ID, set editing ID, message member ID, current member ID, you know, everything else is exactly the same. So let me go ahead and try and refresh this and see if anything has changed or not. Oh, I still have some errors here. Oh, no, I don't. Okay, so if I write reply, there we go. This is a reply. Awesome. We can now see the replies here. So now let's go ahead and also finish up by adding the infinite load here. So I'm going to go back inside of the message list. And instead of the message list, I can still see, oh, we're not using this to things. we're going to use that later okay so after we do the whole object entries we have this observer div so let's go ahead and copy the observer div and let's also add it after our object entries so in the thread components that's just above the featured message here so let me add that here and i love when it has no bugs at all perfect we have everything we need here we have the load more destructed from here. Perfect. And now let's also add the is loading more. And that will be the entire thing we need because we don't need this, right? We don't need the channel hero. So just add this at the bottom and we should have the is loading more here. We already have the loader and that should be the entire thread functionality now. So if I go ahead and open And this, there we go. It loaded the one from before. So I can now send a lot of messages here. And they will all use the same infinite load method. They still have all the compact and non-compact rules. I can add reactions to them. I can go ahead and edit if I'm the author. I can delete them. Everything is working just fine. The only thing I can't do is open a thread where we are looking at a reply message, right? So I can only open threads from main messages here. Hello there. There we go. Amazing, amazing job. What's left to do is to implement direct one-on-one conversations. Now let's implement the individual one-on-one conversation. So we're going to have to fix this 404 page. So let's quickly go ahead and revisit our user item inside of source, app folder, workspace, workspace ID. You should be able to find the user item. And double check that this is the href it's redirecting to, slash workspace, workspace ID, member, and then the individual ID of the member. So let me go ahead and create that route. We're going to go inside of source, inside of the app folder, workspace, inside of workspace ID, just like we had the channel, we now have the member. And inside of here, we're going to have a member ID inside of square brackets. And inside of that, a page.dsx file. Let's go ahead and return this member ID page, div member ID page. And now let's go ahead and try and visit a user again. And there we go. We are now redirected to member ID page right here. Great. So what I want to do now is I want to mark this inside of use client like this. And I want to prepare my workspace ID from use workspace ID like this. And I also want to create a use member ID. So let me go inside of hooks. We have used channel ID and use workspace ID, so let's copy one of them and rename them to use member ID. Inside of here, rename the hook to use member ID and return params.member ID as ID members. Now let's go inside of the page and let's also add const member ID to be the new use member ID. Go ahead and simply JSON stringify member ID and workspace ID here. So we can see it happening. There we go. This is my member ID and this is my workspace ID. So I should be able to switch things now, for example, and go. You can even choose yourself, right? So this is my member ID and this is my workspace ID. Perfect. But to test this out properly, I would suggest picking up another user. But you will be able to have conversations with yourself as well. That will be fully supported. Great. So now what we have to do is whenever we visit this website, we have to either find an existing conversation with member one and member two ID or create a new conversation if those two members never spoke before. So let's go ahead and create that. We're going to go ahead and go inside of the convex folder. And let's create a new conversations.ts endpoint here. And let's export const create or get. And let's make this a mutation from generated server like this. Inside of here, we're going to have some arguments and we're going to have an asynchronous handler. In the arguments, we're going to request a workspace ID from v.id workspaces. And make sure you import the values from convex. And besides the workspace ID, we're also going to have the member ID with whom we are trying to chat. So members, I mean member ID and workspace ID. So then we have the context and the arguments inside of the handler here. And let's start as usual. So we get the user ID from our out, which you can import from dot slash out. We get the user and we protect the route in case the user ID is missing. Now let's get the current member to be await context dot database query the members with index by workspace ID and user ID. query equals workspace ID arguments workspace ID dot equals user ID our user ID and unique. There we go. So only members within the workspace will be able to create this types of conversations, So now let's go ahead and let's do const other member. Will be await context database dot get arguments member ID. So we are talking this is the member ID to whom we are trying to chat with and this is us right So now we going to do if there is no current member or if there is no other member we cannot initiate this conversation. So just throw new error here, member not found. and now let's attempt to find an existing conversation between these two members in this exact workspace so const existing conversation here will be await context dot database dot query conversations and unfortunately we can't use the index here because we don't know at which position member id1 will be or member id2 will be so technically we could have two indexes, you know, both in reverse order and then call two different, you know, queries. Yeah, maybe that's a solution. If you want to, you can experiment with that, but I will use, you know, filters here. So query equals, first of all, let's do query field workspace ID needs to match arguments workspace ID. That's the first thing. So we are looking inside of this workspace ID and then chain another filter. And then inside of here, you're going to do query.or. So one of these two combinations, query.end. In the first combination, we are going to check if the field member1id matches the current member id and if the field member2id matches the other member id. So that's the first combination and then we copy this and do the reverse thing. So if member1id is the other member and member2id is the current member. So the reason there can be two combinations possible is because it will depend on who initiated the conversation. And then later on, when another user tries to visit this conversation, perhaps the conversation already exists. So we can't always guarantee that the user trying to access this will be member one. That's why we have to check for both combinations. So make sure you do this properly. And of course, chain this with unique like this. And then what we are going to do is if we have an existing conversation, we're going to return the entire existing conversation here. Otherwise, we're going to create a new conversation right here. So await context database insert conversations. And we're going to pass in the workspace ID to the arguments workspace ID. Member1ID will be our current member underscore ID. And member2ID will be other member ID. And then you can get the entire conversation here by doing a wait context database get conversation ID, which we've just created. In case we are not able to get that conversation, which we've just created, we have to throw back a new error here because something is wrong. So conversation not found. And then we can return the new conversation, which we've fully queried here. So this endpoint will return the entire conversation object inside of a promise here like this. Great. Now let's go ahead and let's create the use create or get conversation. So let's go ahead inside of source features, and let's create a new folder, conversations. Inside of here, let's create a new folder API and let's copy a mutation. For example, inside of members, we don't have one. Inside of messages, I think we have use create message. So copy that and paste it inside of conversations API and rename this one to use create or get conversation. So the more descriptive, the better, right? And now let's focus on this. So the request type inside of here is the workspace ID and the member ID. And both are required. So this is an ID of members. And it will return back a document of conversations. So make sure you add the doc file and you add conversations here. And then this will be use, create, or get conversation hook. and inside of here we're going to use the api.conversations.create or get and this should not throw any errors anymore and the response type should be the entire conversation with the id workspace id member one id member two id all of those things perfect so now that we have this mutation let's go back to our newly created page which is located inside of the app folder inside of workspace member, member ID page dot DSX. And now what we're going to do is the following const data, mutate and is pending from use, create or get a conversation and pass in. Whoops, this is a mutation. I forgot. So we don't pass that here. We will pass it later when we execute the method. So again, make sure you have the use client boundary here in the page. And now we can see if we use use effect inside of here. So when the user mounts on this page, we're going to go ahead and actually call the mutate method here and pass in the workspace ID and the member ID like this. And then simply have the member ID, workspace ID, and mutate inside of your dependency array here. And now we're going to do if is pending, we're going to return a div. And let me just find any loader really. You can just copy any of these, which we've wrote a hundred times already. So I'm using the example from, let's see, join workspace ID page.tsx. Basically, just any loader copied and pasted here. Let's add the loader from Lucid React. So I'm going to go ahead and move both of these to the top. There we go. So if it's pending, that's what we're going to return. We can copy and paste this. So in case it's not pending, which means we've passed through this, but we still don't have the data, we're going to return alert triangle from Lucid React. We're going to turn this into flex call and gap Y2. We are going to remove the animate spin here, and we're going to add a span, which will say conversation not found. And we're going to give the span a class name text small and text muted foreground. Make sure you've imported the alert triangle from Lucid React. And then we are finally going to let's stringify the data which we found here. So now if I go ahead and refresh here. Okay so it says conversation not found so let's see why that is happening. Let's see we should have the data here in the use creator get conversation we are calling mutate here. So how about we do on success here and get the data in console log the data. And on error, let's go ahead and get the error and console log the error. So let's see what's going wrong here, whether we are actually doing this or not. So I'm going to refresh this. And it looks like we have the data. Right here. So this is created. So why do I still get that error if it's pending? And this is if no data has been found. And then inside of here, we stringify the data. That's interesting. What if I go ahead and click here again? Same thing seems to happen. All right. Let's go ahead and debug a little bit here. I think it's because the data itself coming from the mutation is not exactly the most reliable thing. Let's see whether we are doing something wrong here. Set data. Maybe when we call this mutate again, the data gets reset to null. Perhaps that's what's happening here. Set status. Do we clear the data maybe? I think this is all right. So let's see. uh, instead of page member ID, what happens if I do a console log of the data? So I want to watch the process happening here. Let's see. It's null in the beginning. Yes. And then, you know, we have data here and then it's null again. Uh, all right. So let's debug why that's happening. Uh, perhaps we can use this. How about we do conversation, set conversation use state like this and make it null by default and give it a type of document which will be conversations like this. So let's kind of put it in a state. Maybe that will improve it. But then we can't keep it reactive. Yeah, that's an issue. Okay, let me go ahead and pause and see how we can resolve this. All right, so we are not going to rely on the data coming from a mutation to render our things inside of here. This is what we're going to do instead. We're going to add a state, conversation ID, set conversation ID. And use state and go ahead and make it null in the beginning. but give it an option of ID conversations or null. And go ahead and import the ID. And we don't need the document from here. And what I'm actually going to do is I'm going to revisit my use, create or get conversation inside of the mutation itself. So the endpoint in the convex folder. And I'm going to do the thing I always do. I will just return the IDs, right? That's the only thing I actually need. So inside of here, I'm just returning the, I don't even need to do this part. I can just return the new conversation ID. That's it. And then let's go back instead of the use, create, or get conversation. And let's modify the response type to be the ID of conversations as it always was up until this point. And what we going to do now is we going to go inside of page here And inside of the on success we simply going to do set conversation ID data like this And on the error I don think we even have to do anything Well, we could. And we could just do a toast. Error. Fail to create or get conversation. And import toast from Sonner. So at least the user is alerted if something goes wrong here. And now I'm going to use that, the conversation ID. And I'm just going to pass in the conversation ID here. Let's see if that will improve this. There we go. So now inside of here, I should get the conversation IDs. So if I go ahead inside of my data now and check inside of my conversations, there we go. This is one of the conversation IDs right here, and they are connected to members. Perfect. So this way, our page is stable because it relies on the state with which we set here. And this is what we're going to use. You can also remove the data from here. We're going to use this to create our main component here, which we are going to call conversation. So let's go ahead and add conversation here and pass in the ID to be data. data, actually just the conversation ID like this. There we go. And the ID will never, you know, update. So we don't have to worry about that. We just have to redirect to this component. And now let's actually go ahead and create the conversation ID component. So inside of the member ID here, let's create a new file conversation.tsx. Let's go ahead and create an interface, conversation, props. It will accept an ID, which is a type of ID conversations. Let's export const conversation here. Let's return a div. And let's assign and destructure the props. So ID and conversation props right here. We can now go back inside of the page.tsx and we can import the conversation from dot slash conversation like this. And you should now just see a text which says conversation here. Or actually no, because we don't render anything inside, which is good because it means this is the component that is currently loading. Great. So what we have to do now is we have to implement or we have to reuse the use get member, hook. Let's see inside of features, inside of members. Do we get use get members? No. We have use get members, but we don't have the individual use get member. So let's go ahead and implement. Inside of convex, inside of members, we have the current, we have get. Let's go ahead and let's do expert const get by ID, which will be a query. and let's do arguments, id, v.id of members, and let's pass in the handler, which will have the context and the arguments in here. And now let's go ahead and let's do the usual thing, which is checking if the current user trying to access this is logged in. Then let's go ahead and do const member, to be await context.database.getarguments.id. If we can't find that member, we are immediately just going to return back an early null here. Then let's go ahead and do const user to be await populate user, pass in the context and member.userID. And if there is no user, we cannot load this member. So return back null and then return the spread of member with a combination of the user inside like this. There we go. And you should have the populate user helper above. One thing I also want to do here is once we confirm that we have the member, let's also confirm that the current member exists. So I'm going to do await context database query members here with index. By workspace ID and user ID. Let's get the query. Query equals workspace ID will be the member workspace ID. And we're going to chain user ID to be user ID. If there is no current member, it means this user is not part of that workspace. So we are not going to load the user they want. So let's just confirm that this is all right. We are querying by members using by workspace ID and user ID. We confirmed that the workspace ID is from where this member we are trying to look for is located in. And comparing that with this user ID, I think this should be just fine. And now let's go ahead inside of our source folder, inside of our features, inside of members. And let's copy the existing use get members and paste it. And let's rename it to use get member. Like that. Let's go ahead and rename the props to use get member props. Let's rename this to use get member. And the only thing, what did we say we need? We need the, my apologies, it was just the ID. Okay, so we need the ID of the member. This will be the ID then. And this will be get by ID. There we go. All right, now let's go ahead and let's go back inside of our page. So source folder, app folder, workspace, member, member ID, conversation.tsx right here. And inside of here, we're going to go ahead and load the member. So const member ID will be use member ID. From hooks, use member ID. Make sure you've added that. And then we're going to go ahead and add use get member here. From features, members, API, use get member. and inside of here, we're going to pass in the ID to be the member ID passed like this. Inside of here, we get the data, which is the member and is loading, which will be member loading like this. Perfect. And now we are also going to do the following. const results status and load more from use get messages from features messages api use get messages and since this time we have conversation id and not channel id nor the parent message id that's what we're going to use that's this id prop right here so let's add conversation id here id there we go and we now have results status and can load more here. So let's go ahead and quickly go back to this page and let's just copy this if is pending here and paste it inside of the conversation.tsx. The is pending will this time be if member loading is true or if status is loading first page and let's import the loader from lucid react as well. There we go. We now have this ready and now let's go ahead and render a div here with a class name, flex, flex column, and a full height like this. And now what I want us to do is I want us to add a header component here like this. And I want to reuse the existing header, which we've had, I believe it was the workspace ID channel. There we go. So we need this header. So let's copy this entire header and paste it inside of member ID this time. And you can import it from here. So let's import header from dot slash header, right? Make sure you're doing this inside of your member member ID. We just pasted the header here, right? And now you should have the header. And now we will just slightly, not in channel ID, make sure you're modifying this one. Now we're just going to go ahead and, you know, slightly modify this header. I think it's going to be even simpler, actually. So the props will be member name, which is an optional string, member image, which will be an optional string as well, onClick, which will be a void like this. And then expert const header here will have the member name, which will be defaulted to member in case we cannot load it. Member image will be here and onClick will be here like this. and the only thing we're actually going to need out of pretty much everything here yeah we can remove all of these things right it's going to be a much simpler header component so remove this entire thing and you know just add the avatar fallback to be member name character at zero to uppercase that's the only thing we currently care about the this div with this class name is fine you can remove the confirm dialog from here. And you can actually remove pretty much everything inside. This is a huge component. You can remove everything inside of this div, right? And what I want you to add here is a native button element. You should already have this important, I believe. We have way too many imports now. So just leave the button, which is the one we need. inside of this button we're going to go ahead and give this a variant of ghost a class name of text large font semi bold px2 overflow hidden and width of auto we're going to give it a size of small and we're going to pass in the on click here to be well just on click from our products actually like this and we now need to import everything from the avatar itself so avatar avatar fallback and avatar image from components ui avatar and let's go ahead and add the avatar here let's add a class name of size 6 and mr of 2 inside let's add an avatar image here let's give it a source of member image and now for the avatar fallback i just want to render the avatar fallback inside and i don't think we need to add any class names because i think we've added this which essentially makes it default gives it a default look every time we use it and outside of this avatar create a span element with member name next to it and give the span a last name of truncate and then add an FA Chevron down from React icons FA So I just going to move this here There we go. And let's go ahead and give this a class name of size 2.5 and ML of 2. Like this. There we go. So let's go back inside of the conversation here. And now let's give it some proper inputs. So I'm going to give it a member name here of member question mark user. Oh, it looks like we let's see. So I have this member here from use get member get by ID. So we have the member and I have the user. Oh, I'm accidentally wrote user ID here when I meant to write user. So confirm that inside of your convex members, instead of get by ID, you return a spread of member and then the user. I was accidentally passing user ID. So that's why my user object did not exist here where I'm trying to pass it. So this will be member.user.name. Then we're going to have a member image, which will be member.user.image, like this. And on click for now will just be an empty arrow function. So now we should have a nice header here. There we go. You can see how it looks great both on those that have an image and those that don't have an image. Perfect. And it has the exact same style as our channel, right? And later, this will open a sidebar similar to the ones we have for thread replying here. But we don't have that implemented yet. All right. So what I want to do now is I want to add a chat input here. So let's go ahead and close everything else. And let's focus on the chat input. So we can also copy the chat input from our workspace workspace ID channel channel ID here. So let's copy that and let's paste it inside of member ID on the same place we had the header. There we go. We now have the chat input here. And I think all imports should be working just fine. And the only thing we're going to change here is the chat input props. So it's going to accept the placeholder, but also a conversation ID, which will be a type of ID conversations. So why am I passing this as a prop? Well, remember, my URL does not have the conversation ID. I only have member IDs, right? So that's why we are going to have to explicitly pass this conversation ID here. And I think that what we have to modify further is remove the channel ID from this chat input. So make sure you're modifying the one inside of member, member ID here. Instead, we're going to be working with conversation ID, which will be an ID of conversations like this. And then inside of here, we can destructure the conversation ID like this. You can remove the channel ID, and you can remove the use channel ID import entirely here. And now when you get to the values, instead of passing the channel ID, you will pass the conversation ID here like that. And everything else, I'm pretty sure can stay exactly the same. I don't think there should be any change at all. So let's go back inside of the conversation now. And let's add the message my policy chat input from .slash chat input here. Let's give it a placeholder of message. And then use member.user.name. and give it a conversation ID of the ID prop. And there we go. Now it says message my name here. If I pick another user, it uses their name. In my case, they are both the same. Perfect. And now we can reuse our message list component from components message list. So let's go ahead and add that somewhere here like this. Let me just go ahead and align this. There we go. And now let's go ahead and just add these results, which we have here to the message list, right? So data results like this variant here will be conversation. So you should have that inside of your message list. The message list props should have a channel thread or the conversation variant. But there's one thing missing here, which is this. So we only have the channel hero. We should also create the member hero. So we're going to add that later. And then we're going to use these, the member ID, the member name, and the member image here. So let's go ahead and give it a variant of conversation. A member image, which will be member.user.image. A member name, which will be member.user.name. The load more, which will be load more. Is loading more, which will be status. Loading more. And the can load more, which will be status. can load more. But I think this is already enough for us to start, you know, sending some private messages. So I'm going to say, hello world. And there we go. And let me just go ahead and fix one more thing here. So you can see that I'm currently talking to this user, but they are not selected. So let's go inside of the workspace sidebar, which is located inside of source app and i think you know just the workspace component itself not uh okay so this one workspace id and inside of here we should have the workspace sidebar correct and instead of the user item here let's do the following let's add is active or variant here and we also need the member id so let's quickly add that here because we now have that useful hook. So const member ID will be use member ID. So you can already see why we need to use the member ID in the URL instead of the conversation ID. Otherwise, it would be 10 times more complicated to keep track of which conversation ID relates to what member ID inside of our sidebar. It would just be too difficult to handle. So let's go ahead and do this. If the current item ID is equal to the member ID inside of our URL. In that case, it's active. Otherwise, it's default. And there we go. Perfect. Let's go ahead and double check this by logging out here. And let me log in into my other account. And let me go ahead and make sure that I'm in the GitHub workspace and click on this other user. and there we go. These are the messages here. And I should even be able to reply in thread here. So reply in thread in one-on-one convo. There we go. We can handle that as well. Amazing. And I just noticed that we are missing one important thing, which is that when I reply in thread, I should have an indicator that there are existing replies for this message. Great. But before we do that, which we can handle in the next chapter, I want to wrap up the one-on-one conversations by also adding a nice little, like we have, you know, for channels, this hero, I now want to implement the member hero. So let's go ahead and do that. I'm going to go ahead inside of my message list component. And we're now going to leverage this member name and member image. So let's go ahead and copy the channel hero rules here. If a variant is conversation here, and if we have the member name, and if we have the member image, we're going to go ahead and render the conversation hero here. And we are going to pass in the name, which can be member name, and we can pass in the image, which will simply be member image. And now let's go ahead and let's develop the conversation hero here. So I'm going to go ahead inside of the components and create conversation-hero.tsx like this. Let's see if we can copy anything from channel hero. So let's copy the entire thing and let's paste it inside of the conversation hero. Let's rename this all instances to conversation hero. So conversation hero props. And this will be an image and also a string. and let's make both of them optional because they are not required to exist, I believe. Oh, we actually, no, we don't have to. It's fine. So this will be an image in this case. Margin top can stay the same. This can stay the same. And now inside of here, let's remove, let's actually keep this. So this is fine. Font, bold, item center. That's okay. But let's just remove this. let's just keep it text to Excel and font bold. And inside of here, we can just render, you know, the name of the member. And this channel, instead of writing this, we can write inside this conversation is just between you and and then use a strong element and render the name of the other member like this. And then we also have to add our avatar example here. So let's just add avatar here. and let's do our usual. So I'm going to add a div here with a class name, flexItemsCenter, gapX of one and marginBottom of two. Inside of here, I'm going to add our avatar component with a class name, size14, roundedMD, which we actually don't need. So just MR2, avatarImage, which is going to have a source of member image, which is actually just image here, sorry. Let's go ahead and create our avatar fallback here to use the name prop character at zero to uppercase. And then inside of here, we should be able to just pass to avatar fallback the avatar fallback. Like this, okay. And let's also put this paragraph here inside of this div like this. And let's go inside of the message list and let's import the conversation hero from dot slash conversation hero here.\nAnd now if I refresh this, let's see, I still don't have that. So why is that? Let's see, I'm passing in the variant to be a type of conversation here. And let me revisit my, let's see, inside of the not workspace ID, member, member ID, conversation. I have the message list. I'm passing the member image. I'm passing the member name here. and I'm passing the variant conversation. So what exactly is making this not render anything? If I write something here, this is working, but I'm not seeing this. Let's see why for a second. All right, I think I know what it is. Yeah, so I can see it on regular users, but I can't see it in users who don't have an image because of my rule set here. So let me remove this rule set. And let me go inside of conversation hero and make this optional and make this optional and pull back the name to member. There we go. So now, there we go. This user has their own hero header as well. Amazing. So now we can also quickly explain, finally, our underscore conversation ID, which appears a couple of times here. So I told you that when we use the get method here, we have this edge case where we don't have a conversation ID and we don't have channel ID, but we have the parent message ID. So when does that happen? Well, for example, inside of a one-on-one conversation, inside of a thread, this is that exact case. Loading these messages is that case. We don't know what conversation does this belong to. Why? Well, let's take a look. If we go inside of our thread here, let's see what happens. So we have the channel ID and the workspace ID, right? But inside of conversation, we don't have the channel ID, right? We have member ID, which is not something we can send at all. So when I go ahead and call the handle submit method here and I pass in the channel ID, this is undefined, but this is not. And also the conversation ID, which is an option, is also undefined, right? I can almost never pass that through URL. So both of these don't exist in that case. Let me just not mess this up. But parent message exists. So I detect that here and I know that's the edge case. And that's how I get the actual conversation ID from the parent message. And same thing happens in this submit form in the thread as well. So let's go back inside of the thread here. So you can see the same thing happening here, right? I can't send the channel ID. I only have the parent message ID. So that's the only thing that can happen. And here instead of the, where is my use get messages, same thing, no channel ID. So that's the edge case I was talking about. I hope it's at least a little bit clearer now. Great. So what we have to do next is we have to fix our thread indicator, right? So if there are replies, we should have a little indicator here below that there are replies. And we also have to implement the user profile so we can change the role. Great, great job. Now let's go ahead and let's implement the thread bar component. So I want to go back inside of my message.tsx inside of my source components here. and first of all let's go inside of the if is compact return and after we pass the reactions let's add the thread bar which we are going to create so the thread bar will have a count prop which will be thread count it's going to have an image which will be thread image it's going to have a timestamp which will be thread timestamp like this and let's go ahead and copy this and let's add it right here in the lower return like this and now let's go ahead and create the thread bar inside of the components folder so thread-bar.tsx let's create an interface threadbar props. And let's go ahead and give it a count of number and optional image of string timestamp of number on click an empty arrow function. And let's export const threadbar. and let's use the threadbar props here and let's destructure all the necessary elements which are count, image, timestamp, whoops, and on click like that. Now let's go ahead and do the following. If we don't have the count or if we don't have the timestamp. Or if we don't have an image, let's return null. So for now, we were only going to be able to test this on users that have an image, meaning use a Google or GitHub login to test this out. And now let's go ahead and let's return a native button element here. We're going to give it an on click of on click. That's going to be the first and the simplest prop here. And now let's go ahead and give this button a class name. Adding of one, rounded MD, hover, background white, border, border transparent, hover, border border, flex, items center, justify start. We're going to go ahead and give it the name of group slash thread dash bar. This is important. Don't just give it group because you already have group inside of the message component. After that, transition and a max width of 600 pixels. That is the entire class name for our button. And now inside of the button, let's go ahead and let's add a div with a class name, plexitemscenter, gap2, and overflowhidden. Inside of here, we're going to add the avatar. So let's go ahead and prepare the imports. Avatar, avatar image, and let's add an avatar fallback here. like this. And I think we can copy it from somewhere. We probably can. Let's see. Instead of the message component itself, we already have it, the avatar. Let's just copy it, and let's just paste it inside. There we go. Let's go ahead and give the avatar a class name, which is going to be size 6 and shrink 0. The source will be image and the avatar fallback here will for now just be the letter M as in member. And let's return this actually. Let's remove this actually. So image will not be required. Just the count and the timestamp will be required so we don't render null, right? So we now render the avatar callback here after fallback. And outside of the avatar itself, add a span element and go ahead and give it a class name, text, extra small, text sky 700, cover, underline, font bold and truncate. And inside of here, add count. And then if count is larger than 1, replies. Otherwise, reply. Then another span with a class name. Text extra small. Text muted foreground. Truncate. Group dash hover slash thread dash bar. So group hover should be exactly like this. And then slash exactly what you wrote here. Whoops. In the first div. Thread dash bar. So make sure that you don't misspell this. So if we hover on the thread bar, it will become block. But otherwise, it's hidden by default. And inside of this span, we're going to write last reply. And then format distance to now from date FNS. So let's add that import. And go ahead and pass in the timestamp and add the options, add suffix to be true. And continue writing. Actually, that's it. We don't have to write anything else. And below this, add a span view thread. and give this span a class name of text extra small text muted foreground truncate group cover slash thread bar will be block otherwise hidden as well and already actually let's go inside of the message.tsx and let's find one of the thread bar with errors and let's now import it from dot slash thread bar and that should get rid of our errors here and you should now start seeing uh the replies here as you can see there we go we have the replies great let's go ahead and develop them further uh so now what i want to do is i want to go outside of this div and add a chevron write which you can import from lucid react so i'm just going to add it here like this, and give it a class name of size4, textmutedforground, mlauto, opacity0, group, dash hover slash thread dash bar like this opacity100 transition shrink0 And that will be it like this So there we go. You can see how this looks like now. And let's just see, can I improve this text view thread? So let's go ahead and do this. I made a mistake here. So one should be visible by default and one should be visible on hover. So let's do it like this. For the last reply, blah, blah, blah, we're going to change the group hover thread bar to be hidden on hover, right? But block by default. So now there we go. You can see how it says last reply about four hours ago. But when I hover, it says view thread. And now we just have to give the ability to this thread bar to actually do the onclick. So let's go inside of the message here. And our onclick here will simply be onopenmessage. So let's find the first thread bar. And let's give it an onclick, onopenmessage, and paste in the ID. and then copy this and find the second thread bar here and just add that here. So now when you go ahead and click here, there we go. That opens the thread. Perfect. So we now have a much better thread functionality, but we have one mistake. So for example, if the last reply is from the user that does not have an image, we can't exactly render that. So let's fix that. And in order to fix that, what I believe we have to do is we have to revisit our messages.tsx in the convex. So our API here, and specifically this big get query. So somewhere in here, we are populating the thread. There we go, populate the thread. So what we have to do is go inside of the populate thread method. Let's open it. And let's give it some more useful information. So I'm gonna go ahead and also add a name here to just be an empty string like this. and let's go ahead and add a name here to be an empty string. And then finally, let's add the name here. And let's see, can I do, so I am populating the member. Can, oh, we have the last message user. Perfect. So name dot name. There we go. So we now also have the name of the last message that sent the image, but we're not done yet. So now that we have the populate thread, let's go ahead and actually go back inside of the get query. We now have the thread here, which has the name, which is a new field now. And we have to go down here and add alongside thread image. Let's also add thread name here, thread dot name like this. There we go. Let's go back inside of the message dot TSX here. And I think we also have to modify the props now. So we have a thread image here. Let's also add thread name here to be a string. and let's also add the thread name here as well. And now we have to go to the message list component, I believe. Am I in the wrong? Let's see, where are my messages here actually? Oh, I have collapsed them, my apologies. So they should be inside of here. There we go. So instead of your message list, find where you return the message inside of this whole big, you know, map. And just as you've added the thread image, you can now add the thread name here, the message dot thread name. And let me just go ahead and do a quick search of everywhere where we are doing this. So two places, message list, and also the thread dot TSX component, even though technically this should not be happening here. But go inside of your source features messages components thread.tsx and find the message here and add the thread name here to be the message thread name like this. So now all cases of the message components should have the thread name. Well, at least those that matter. and now let's go ahead and see if we can modify our thread bar to also accept name which will be an optional string let's get the name here and let's give it a default value of member right here like that and let me copy one avatar fallback from literally anywhere so i will just copy this one from the user item component and i will paste it here so avatar fallback here will be the name and then we can use the outer fallback instead of the m letter so just outer fallback like this let's go back instead of the message.tsx because we're not done yet uh we also have to add this to the other thread bar which i'm missing here so thread name okay so make sure that both of your thread bars now have the new thread name prop which we have just passed throughout our message list all the way. And there we go. We now have that edge case solved as well. Amazing. So you now have the fully working thread bar. What we're going to develop next is the ability to open user profile and to change their role. Great, great job. Now let's go ahead and let's implement the member profile. So just as we've implemented the thread panel, when we click here, we get a new URL appended here, the parent message ID, and then the thread opens inside of here. Now I want the same thing to happen if I click on the user image or their name. So let's go ahead and let's start by copying our use parent message ID hook, which I believe is inside of features messages store. So use parent message ID. So I'm going to copy the entire store folder and I'm going to add it inside of the members folder here. And inside of here, I'm going to rename this to use profile member ID. I don't want to call it use member ID because we have that inside of hooks, right? So this is simply to extract it from params. But what we are developing now is to get the profile of that member. So let's call it like that. Let's rename this to use profile member ID. and let's simply rename the key of the query state to be profile member ID. There we go. Now let's go ahead and let's go inside of our hooks usePanel.ds and let's import useProfileMemberID. Now what I want to do is I want to add the onOpenProfile so we can copy and paste this onOpenMessage here. and let's rename it to onOpenProfile. And the param here will be the member ID and it will set profile member ID and set profile member ID from use profile member ID. So let's go ahead and pass in the member ID here and let's use set profile member ID here on the onOpenProfile like this. But now that we have both of this available, I know that they will not be able to be opened at the same time. So every time I open a profile, I will set the profile member ID, but I will also set the parent message ID to null. And I will also do the reverse in the on open message. So I will do set profile member ID to null like this. And the on close, I will always close both of them. So set profile member ID to null here as well. and then go ahead and return the profile member ID here and on open profile here as well. There we go. That's our use panel. And now we have to go back inside of our workspace layout. So that's inside of the app folder, workspace, workspace ID, layout.tsx. And inside of here, we can now also destructure the profile member ID. So we can add if we have parent message ID or if we have profile member ID. Make sure you are using double exclamation points here and here. So if any of those exists in the URL, we're going to show an additional panel. And now inside of here, we're going to decide whether we are showing the thread, which is already solved here. And let's add an additional if clause here. So if we have profile member ID, in that case, go ahead and open another option and then add an else here again. And inside of here, we're going to add a profile component, which we are going to create. And let's go ahead and pass in the member ID here to be the profile member ID as ID members. So we are using the same trick as above. And let's pass in the onClose to be onClose. There we go. So now we have to go ahead and develop the profile component. So let's create that inside of the features folder inside of members. And let's go ahead and create a new folder components. And inside of here, a new file, which we are going to call profile.tsx. Like that. Let's create an interface profile props to accept a member ID, which is a type of ID members. And an unclose method here, which is a void. then let's export const profile and let's assign the profile props here member id and on close let's go ahead and return a div profile so this should be enough for us to at least be able to open a specific profile let's go inside of a layout and let's import the profile from features members components profile. So as we did right here, I'm going to move it here alongside with the thread. Great. But we don't really have any way of opening this at the moment, except by manually adding profile. Is it profile member ID? Yes, it is. It's profile member ID. There we go. And then you can see the profile here opens But if we click on thread for example you can see that the profile is no longer the part of the URL only the parent message ID is the part of the URL So only one can be opened at the same time So let's go ahead and properly open this by clicking on the specific member. So for that, we have to revisit our message.tsx inside of source components here. and we are not using anywhere the member ID here, which we always pass. So now we're going to use it. So from the use panel, let's also do an open profile like this. And now let's find all the places where we need to add this. And I already noticed something here. So we have this is toggling reaction from use toggle reaction. Let's just add it to this is pending. So it's toggling reaction like this. Great. And now let's go ahead and use the on open profile. So in the compact message, there is nothing to click on that can open the profile, right? So nothing here should really open the profile. But in the main message, clicking on the image and on the name should open the profile. So let's go ahead and let's find, let's see, where is the avatar component? Oh, so let's go to the main return here, right? And this button, which encapsulates the avatar, will now have an onClick here, which will be an arrow function on open profile and passing the member ID. Like this. And we're going to have it in one more place, which is inside of this else. So if not editing, we're going to have this button of the author name. So let's go ahead and just assign that onClick here as well, on open profile member ID. So now if you click on this name, there we go. The profile opens. If you click on the thread, the thread opens. We can close the thread. And if we click here, there we go. The profile opens as well. Perfect. And both can be overridden. And if you look at your URL, the profile member ID changes depending on which user you click on. Now let's go ahead and let's actually develop the profile. So I think we should also keep our thread open just in case we want to quickly copy something from here. So the first thing we're going to have to do is we're going to have to get the member. So I believe we've developed that recently. Inside of my members API, there we go. We have use get member. Perfect. So let's go ahead and let's do const use get member. And let's pass in the ID to be member ID. And let's import use get member from dot dot slash API use get member like this. From here, we can destructure the data, which we can remap to member and isLoading, which can be isMemberLoading or isLoadingMember. Like this. And now let's go ahead and let's copy some of our existing loading states here, right? We can copy the ifNoMessage and ifLoadingMessage, and it will just slightly modify it. So I'm copying this from the messagesComponents thread here because it's quite similar. So first of all, we're going to modify this. If is loading member, we're going to go ahead and add a button component. We're going to go ahead and add an X icon here from Lucid React. So all of these, let me just go ahead and move this here. Let me move this to the top like this. Let me add the loader from Lucid React like that. And this is the data. So if no member in that case, we're going to go ahead and say profile here. in the top and let's have an alert triangle here from lucid react and let's change this to member or profile not found right so now while you are loading here let's go ahead and see can i uh can i mark this as true for example and okay so it still says thread here in my loading so let's go ahead and change this to profile like this. There we go. So you now have the profile option. When you click on thread, it loads the thread. Here is the profile option, which can be closed if it's loading for too long. So let's change this to is loading member here. And this is if no member is available. So let me try and hard coding this to true as well. See what happens. There we go. Profile not found. So in case the user gets deleted while we are opening it, there we go. We have that solved as well. and now what I want to do is I want to render the proper preview of the user here so let's go ahead and let's copy this let's copy the entire div here and let's add it here like this and now we're going to go ahead and slightly modify it here so this can stay profile like this and then inside of this div here, what we're going to do is the following. This will be the body, right? So I'm going to change this to, let's see, item center, height full. I just think I don't want the gap here and I don't need the height full. Justify center is also fine. Let's give it a padding of four instead. So I just want a larger space here. Let me get right here so you can see. I just want like a larger space here in the beginning of the user profile. And inside of here, what I'm going to do is I'm going to render the user avatar. So for that, let's go ahead and let's import everything we need from the avatar component. And inside of here, we're going to add the avatar, the avatar image, and the avatar pullback. And now we have to do what we usually do here. so let's create a fallback quickly here when we load the member so let's ensure that after we have the member let's add a constant fallback so that will be member.user.name or we can just use an m for the fallback in case we and let's call it avatar fallback right the member comes from this use get member so now we have avatar fallback which we can add here so now there we go you should see like a big uh like a small user profile here and let's also add a source here to be member user image there we go so if they have an image they will have an image otherwise you can see it's just their profile and now let's go ahead and let's style this so i'm going to go ahead and give this avatar a class name of max width 256 pixels, max height of 256 pixels, and size full. So now, instead of here, this looks fine, but looks like my avatar fallback here does not work well. So what if I give this a height full? Looks like it doesn't work. What if I give it an aspect square. There we go. So now it's a little bit better like this. And let's also go ahead and give this a text of extra large or 2XL or 4XL. Do we have 6XL? There we go. So now I think it's the same size as the regular image one. There we go. So that's kind of the preview of the profile, which we are looking. And it should be kind of scalable, as you can see. Let's see if this is as well. There we go. So they both seem to behave the same. All right. And now let's go ahead and let's further develop this. So below this div, encapsulating the avatar, we're going to prepare the room for some buttons, right? And some contact information. So we're going to have class name flex, flex call, and padding of four here. Then inside of here, a paragraph member.user.name. So now inside of here, there we go, we have the name of the user. Let's give this paragraph a class name of text extra large in font bold like this. And then let's go ahead and, Well, I want to skip what we have to do next, which is we have to develop like the buttons to leave and change role. I want to do that later. So instead, I'm going to go ahead and first finish the contact information. So let's add the separator from components UI separator. Make sure you've added this like that. And then go ahead and add a div and you can copy this class name. So I just want to keep the same spacing here. and I'm going to add a paragraph contact information. So there we go. After my name, we should have the contact information here. And let's go ahead and give this a class name. Text, small font, bold and margin bottom up four. And then inside of here, I'm going to add a div with a class name. Flex, item center and gap two. I'm going to add another div, which will encapsulate a mail icon from Lucid React. So just make sure you've imported mail icon from Lucid React. Let's give the mail icon a class name of size 4. Like this. Let's give the div encapsulating the mail icon a class name of size 9, rounded medium, background muted, flex item center, and justify center. center. Select a little container for our icon here like this. There we go. And then outside of that, we're going to create a div, which will hold the user email information. So this will be class name flex, flex column. Let's add a paragraph email address. There we go. Let's go ahead and style the email address text here to be text 13 pixels, font semi bold, text muted foreground like this. And let's add a link component from next link. So make sure you've imported the link from next link. I'm just going to move it here with the global imports. And the href here will be open back text mail to member user email like this and class name will be text small hover underline and text will have a special hex color of 126483 like this and inside we going to render member user email And let's see if this works. And there we go. You can now see various user information in their profile right here. Perfect. The only thing I'm kind of worried about is scaling this special avatar fallback type because we use this huge text. So let's go ahead and see if I zoom in a lot. Yeah, I mean, this is a very weird edge case, but okay, it looks like it never overflows, which is fine. Great, so we have that. And now what I want to develop is I want to develop the options to change the user role and to kick the user. Or if the user is looking at their own profile, they should have the ability to leave the workspace. Great, great job. So now let's go ahead and let's implement the change role and remove or leave workspace functionality. So let's go inside of convex inside of our members API here. So far, we've developed the get by ID, the get and the current queries. So now let's go ahead and let's export const update, which will be a mutation, which we have to import from generated server here. It will accept the arguments and our asynchronous handler. For the arguments, we want the ID, which will be an ID of members. And we also want one thing we will be able to change about the member, which will be the role. So that is v.union and then two options, admin and member like this. And we have to have this inside of the arguments object. My apologies. So ID for the member, which we are trying to update, and then the role, which we are trying to update. In here, we get the context and the arguments like this. Let's go ahead and get the user ID from await out get user ID and pass in the context. If there is no user ID, we're just going to throw new error unauthorized here. Then let's get the current member, which is await context dot database query members. And well, we have the query by workspace ID, but we don't yet have it. So let's first do const member, which we are trying to update, is await context database get arguments ID. If there is no such member, we're going to throw new error back, member not found. And then we can continue with the current member query. So we're going to use the with index by workspace ID and by user ID. Let's get the query and let's go ahead and do query equals workspace ID to be member workspace ID and user ID to be our user ID. And let's add dot unique. And now, if we were unable to find the current member, or if current member role is not an admin, in any of those cases, we will not be able to proceed further with making this change. So let's throw a new error unauthorized here. And then finally, let's go ahead and do await context database patch arguments ID role arguments role. And let's go ahead and let's return back arguments ID here for the member we are updating. There we go. So now that we have that, we also have to develop the remove method. So we can just copy the update method. and let's call this one remove. And this will just take the ID of the member here like this. We can do the exact same logic here, the current member. If there is no current member or the role is not admin, in any of those cases, we are going to prevent the user, right? So what I want to do now is also add some additional checks here. Before we do this, I will add a to-do remove member. So there are some specific things we have to check before allowing anyone, even an admin, to do this. So first of all, we're going to do this. If the member we are trying to remove is an admin, we're going to throw a new error here. Admin cannot be removed. We will not be able to remove admins. So we first have to downgrade them to members. Then we're going to check if is self by using the current member ID and check it with arguments ID. Then we're going to check if is self admin as well. We're going to check if current member role is an admin like this. So, oh, we already checked for this if we are an admin. So we don't have to do that up there, right? So inside of here, we're also going to do if, well, we can just do this in the if clause, my apologies. So if we're trying to remove ourselves, we're also going to throw new error here, unauthorized like this. But I just realized that this will not allow us to remove ourselves from the workspace, right? So this is how we are going to do this. We're going to remove this check here. the remove method right so the first thing we're going to check is like this if the member we are trying to remove is an admin we won't be able to remove them then if the current member is trying to remove themselves and also if that member is an admin then we're going to throw back the error so if current member dot role is an admin we're going to go ahead and throw cannot remove self self if an admin. Self is an admin. Right, so we cannot remove ourselves if we are an admin. But we are going to allow ourselves to remove ourselves in other combinations. So now let's write await context database delete arguments ID. But before we can try this out, we also have to delete all relations here. So let's go ahead and let's prepare all the messages, reactions, and conversations that this member has created. So await promise all. Like this. So in the first item in the array, we're going to query the database for messages that this member has sent. So messages with index by member ID, query, query equals member ID, member underscore ID. And let's also add dot collect here. So we get all of them. So that's the messages here. then we're going to have to query all reactions that this member added and we have the same index right so inside of your schema you can see that for my messages here i have by member id and for my reactions i also have by member id so i can easily remove all of them when a member leaves and let's go ahead and add one more here so this one will be conversations and the reason the way we're going to check this is by using a filter here so that filter query query or the first thing we're going to check if the field member one id matches member id like this Perhaps we can put this like this. There we go. So either this combination or member2id equals the member ID. So we're going to check if there are any conversations with us as the member inside. So in the second is reactions and in the third is conversations like this. And then before we do the removal of the actual member, we're going to do for const message of messages, await context database delete message ID. And then we can go ahead and duplicate this two more times and do the same thing for the other entities here. So this will be for const reaction of reactions, delete the reaction ID and for const conversation of, whoops, conversations, remove the conversation ID. There we go. So we now have the ability to remove and to update our user. So let's go ahead and create the hooks for that. So instead of the source folder, inside of features here, inside of members, instead of API, let's find one maybe messages API use create message so copy this one and paste it inside of members API use create message and let's rename it to use update user update member like this make sure you're inside of this new file let's go ahead and change some things here so we need the member ID or is it just ID I think which we have to do some members here and we will also need to pass in the role which will be either admin or member so these two and it will return back the ID of members back and let's change this to use update member. And let's change this to the API members update. And let's see what exactly is wrong inside of this. So members that update here, ID of members and role of admin or member. Oh, the ID is required. Yes. Like this. There we go. No errors for the use update member. We can Now copy and paste this and create a use remove member method. And instead of here, there will be\nno role. Let's call this use remove member and let's change this to API members remove. And there we go. We now have the new use remove member and use update member hooks. And we can go back now inside of our source features members components profile.tsx. Let's go ahead and let's add them to our mutations here. So const use update member and const use remove member. So both of them are available here like this because they are in the members folder. So you can access them from here in the API just by going back. And let's go ahead and let's destructure the mutate here and let's call it update member. And is pending will be is updating member. Then we're going to have a mutate here. Remove member is pending is removing member. There we go. What I want to also do here is initialize the use current member as well. So we need that. That will be quite helpful. So we can see if we are an admin or not. Can we actually do this? So use the current member here. We'll have the data of current member and is loading current member. and inside of here we have to pass in the workspace id which we can get from use workspace id from hooks use workspace id there we go so we now have pretty much everything we need here and let me just use the is loading current member here as well in this is if is loading member like this so if we are loading the current member or or if you're loading the member, we display the spinner here. Great. So now let's go ahead and see whether we can actually display those options to the user or not. So after our avatar fallback here, we're going to go ahead and develop just below this member username. We're going to check if current member.role is an admin. In that case, go ahead and let's do the following. Let's check if current member ID is equal to member ID. So in this case, it basically means, let's go ahead and write it like this for now. It basically means that we are an admin and we are trying to look at our own options here. So let's go ahead and let's give ourselves a button here. and the button will very simply have a member.role option and a chevron down icon from Lucid React. And let's give this a class name of size 4 and ml of 2. And let's give the button a variant of outline and a class name, pull with and capitalize. So I think that now if you click on yourself here, oh, but we also have to confirm that we are an admin, right? So we are not an admin of this workspace, but if I go ahead and pick maybe the one where I am an admin and click here, there we go. You can see that I have this big admin button now like this. Great. All right. So we have that option. And now what we have to do is we also have to add another button next to it, which will be removed like this. and we should encapsulate this into a div with a class name flex items center gap to and margin top of four. There we go. Let's give this remove button a variant of outline, a class name of with full and capitalize. well this one doesn't need capitalize right this one needs it because member.role is lowercase right there we go so we now have the options to change the role and to remove ourselves from here okay and i think that this ternary way of doing if clauses has confused me a bit So what we're seeing right now should be the view for an admin looking at another person's profile, right? So we ourselves, when we click here, should not see remove. We should see the leave option. And since we know that inside of our backend logic, we ourselves will not be able to change our role, right? So we should just see the leave option here if we are not an admin, for example. So let's go ahead and quickly fix this. So the first clause is correct. If the current member role is an admin and if the current member ID is not the member ID, then we're going to showcase this. So there we go. Now, if I'm an admin and I click on myself, I see nothing yet. Right. But if I go to another workspace here, let's arm. So what I have to do is log out now and just go back inside of my other account here, which is an admin here. So let's just try it out. So now if I click on another user and click on their profile, there we go. You can see that now I have the proper options here. So I'm an admin. You can see this is my different account here. And I'm clicking on this other user. I can now see their option that they are a member and I have the option to remove them. But if I click on myself, I don't have any options yet. So that's what we're going to develop now. So now let's add an else here, if clause, if current member question mark underscore ID is equal to member ID. So if we are looking at ourselves this time, and we also have to do another check. So if we're looking at ourselves and if the current member role is not admin. In that case, we're going to have just a single button here. And let's immediately add an else null here. We're going to add just a button here leave like this. So let me just go ahead and give this variant to this button. and let's give this a class name margin top of four like this. So let's see. If I'm an admin, I should actually not have any options for myself. So I shouldn't be able to change my role and I shouldn't be able to leave the server if I'm an admin, right? Because the server needs to have an admin. But if I go ahead and log out now and if I revisit my other account, like this, and go inside of the GitHub Workspace where I'm not an admin, I should have the big leave button, but I don't have the button to change anyone's role. And if I click on an admin, for example, there we go. I don't have, I can't do anything for them. I can't kick them and I can't change their role. So that's what we wanted to achieve. Great. So now let's go ahead and let's implement the leave and the remove role, which will be used by individual users who want to leave themselves or admins who want to kick a user. And let's also implement the on role change, right? So I should not have used ternary here. I'm sorry for that. Feel free to, you know, separate this into some more readable constants, but let's go through it together. So if the current member is an admin and it's looking at a profile that's not theirs, they're going to have the option to change their role and to remove that user. Otherwise, if this current user is looking at themselves and they are not an admin, they will have an option to leave, right? So there is a leftover case. Okay. What if an admin is looking at themselves? Well, we don't want them to give them the ability to do anything, right? Because we need to have an admin in the server. All right. So I know it's a little bit confusing, so feel free to pause and carefully, you know, copy this part. Okay. So now that we have that, let's actually add some methods here, which will have some nice toasters and stuff like that. So let's start with the on remove and on leave methods. So const on remove here will call the remove member method. and it will pass in the ID, member ID, which we have from the props here. On success here, we are very simply going to do toast.success member remove. And let's go ahead and call the toast from Sonner. And let's also do the on close method here. So inside of our thread, when we deleted a message, for example. Or actually, it's inside of our message.tsx. Where is it? Our handle remove. Inside of here, we have to check if the parent message ID matches the currently opened ID. That's because the message component can be deleted from anywhere. So for example, I can open a thread here and delete the message from here. So that's why we had to close the thread like that. But in a profile, we will only be able to delete the profile from the panel. So we can safely close it on success here. And on error here, we'll have a toast error failed to remove member. Great. So we have this. Now let's copy and remove. And let's go ahead and let's create an on leave, which will be exactly the same, but it will have different toast messages. And also we're going to have different confirmation models later. So now let's go ahead and add failed to leave the workspace like this. And one more thing we have to develop is the onRoll method. So we can copy and paste the onLeave one, for example. And let's go ahead and create an onRoll change or let's call it simply onUpdate. and let accept the role value of either admin or member Let go ahead and let use the update member And let pass in the role here And on toast success it will be role changed On toast error, it will be failed to change role. Like this. And now let's also implement our confirmation dialogues here. So we're going to need a leave. Let's go ahead and leave dialogue and confirm leave. Use confirm. From hooks use confirm. Let me just move it here like this. So inside of here, we're going to tell the user leave workspace. And then are you sure you want to leave this workspace? We're going to copy this and then we're going to add a remove dialog and confirm remove, which will say remove member. Are you sure you want to remove this member? And we're going to have a update dialog and confirm update. This will be change role. Are you sure you want to change this member's role like this? So now let's add the update dialog, leave dialog, and the remove dialog to our code here. So I'm going to wrap the entire thing here inside of a fragment. So my main return method, right? Like this. and I'm going to add the remove dialog, the leave dialog, and the update dialog. And I'm going to go back to all of my three methods here. The on remove will become asynchronous now, and it will have a const okay of a way to confirm remove. If not okay, we do an early return. Same thing for the on leave, turn it into an asynchronous method, and go ahead and check this. but instead of confirm remove, this will be confirm leave. So the user is prompted with different questions. And then we have an asynchronous on update here. And this will use the confirm update. Great. So now let's go ahead and let's use the on remove and on leave methods because for the on update, we still have to implement the dropdown. So let's use the on remove somewhere. Let's go ahead inside of here and let's see at the logic we have. So we have the button remove. So that's the one that should be called for removing someone right here. And for the leave, it's this one on leave. So let's see what option do we have here. So since I am not an admin inside of this workspace, I am a guest. And I will click on myself. There we go. I have the option to leave the workspace. And let's see if it works. There we go. I left the workspace. You can see this fallback of ours, member, member, member, right? And we can also improve this on leave. So when we leave somewhere, we should probably redirect the user away from this workspace so they don't see this broken screen. So const router, use router here from next navigation. Like this. Let's go ahead and on leave, we're going to go ahead and do router replace. let's just push the user to the root page like this and let's see what happens if we do this there we go so some errors but that's fine right i just don't want things to break there we go you can see how this looks like now a complete failure from there but excellent so now i want to see do i have uh do i have any server where i have two users where i am an admin. Let's see inside of this. Hello world. What am I here? Am I an admin? Yes, I'm an admin here. I can see that by this as well. So let me go ahead and log out and let me continue with GitHub here for my other user. So it's signed here in invite. So I'm going to go ahead and send a hi there message. So we just tested out that leave is working. And now let's test out the role change and the remove user. So I will go ahead and log out here and I will go back inside of my other account where I am an admin. There we go. So there we go. I have the option to remove this user now, but I don't want to test out just yet because I first want to develop the member functionality here, the role change, because we just confirmed that the API endpoints to remove or to leave works, and it's exactly the same endpoint. So, you know, it's not a problem. So let's go ahead now and let's add everything we need from the dropdown menu. So I'm going to add that here. The dropdown menu, the content, the trigger, the radio group, and the radio item. So now we're going to have to turn this button where the member role is into a drop-down where it will have basically more options to change. So let's go ahead and mark the entire button inside of a drop-down menu. Then go ahead and give this a drop-down menu trigger and an as child prop. then outside of the drop down menu trigger add a drop down menu content and let's go ahead and give this content a class name of with full and let's create a drop down menu radio group and a drop down menu radio item which will say admin here. And let's go ahead and give this a value of admin in lowercase. So make sure you have the drop-down menu radio item and radio group. And let's give the group value here a default value of member.role. On value change will call the role and pass in the on role on update. role as admin or member. And now let's also add another drop down menu item here to be member and the value of member like this. So now when you click here, there we go. You can choose between admin and member. And when you click, you should get a confirmation here. So if I click confirm here, role has been changed. And there we go. You can see that this is now an admin here. And I believe that we won't be able to remove him. There we go. So fail to remove members. So can I change the role? Let's see. So I can change the role because I'm also an admin, right? But I cannot remove them while they're an admin. I can remove them now. So let's double check this. So let's see if this is actually an admin. I mean, it is, right? It's an admin. We can log out a confirm. Let's go ahead and go inside of my GitHub here. My invite workspace. So now there we go. I can change the preferences. I can delete the workspace, which means I am an admin, right? And for example, for this other admin, I can now downgrade them. There we go. They're now downgraded. And I can also remove them now. So let's remove them. There we go. That member has been removed. And you can see how their messages have been removed as well. Amazing. Amazing job. Now let's go ahead and let's add some to-dos, which we wrote. So I'm going to search my entire project for to-do. And I see one in channels.ts right here. So inside of convex channels.ts, inside of my remove mutation, when I remove a channel, I also want to remove all associated messages from that channel. So let's do that. I'm going to go ahead and add a constant messages from await promise all. And inside of here, I'm going to add context.database.query messages with index by channel ID. And let's add a query checker here. Query equals channel ID to be arguments ID. And let's collect all of those messages. And then inside of here, we can do for const message of messages await context database delete message ID. There we go. But besides that, I think I'm also missing something inside of my workspaces. So let's go inside of the convex workspaces here. And let's focus on the remove mutation here as well in the workspaces. So currently, the only thing I remove after I delete the workspace are the members. So let's go ahead and copy and paste this. And let's now remove all channels. So everything has the exact same query. Buy workspace ID for reactions. Buy workspace ID for the messages. Buy workspace ID for conversations for the channels. Ensure that you have this index in pretty much all except workspaces in our tables, right? So we remove the channels now. After channels, we have conversations. After conversations, we have the messages. And after messages, we have reactions. And then we have members in the first one, channels next, conversations, messages, and reactions like this. And then we can copy and paste this for const channel of channels, delete channel ID like this. Then let's go ahead and copy this again for const conversation of conversations. Delete the conversation here. Then for const message of messages, remove message ID. And last one will be all reactions we have in the workspace. So reaction of reactions, whoops, reaction ID. So now removing the workspace will pretty much clear every single thing we've created inside of it. Great. So we have that resolved. What I want to do now is just check do we have any empty on clicks for example Let see We do have inside of conversation dot TSX Great It a good thing we checked for that So let go inside of source app workspace workspace ID member ID conversation. And inside of here, we have a header. So that is referring to this. When we click on, when we go inside of one-on-one conversation, we have this header, which currently does nothing, right so let's go ahead and get our use panel import from hooks use panel so we are doing this inside of the conversation.dsx here inside of the member id make sure you're doing it inside of the member id here and let's do the on open profile here like this and then i'm going to pass in the and click here on open profile member ID, which we should have here from the use member ID. So now when I click here, there we go. I can now talk to users in that way as well. Let me check. Do I have any channels with multiple users? It looks like not now, but yeah, you can just click here now and it will appear right here. Great. Let's check if we have any disabled set to false. We don't. Perfect. So we don't have any leftovers. So what I want to do now is I want to add a little command tool when we click on the search here, which will load all users and all channels. So let's go inside of our toolbar, which is located inside of source components here. And inside of here, is it a toolbar? Let me just see. No, it's not the toolbar. My apologies. It's the navbar. How do they call it? Header? Let's see. I'm not sure where this is located. So it should be inside of, let's see, workspace, the toolbar right here. So this is the toolbar I intended to use. Great. So this one, search and then data name for your workspace, which we loaded here. So what I want to do here is I want to add the command component from ShadCN. So let's go ahead and pick our installation here. So I will use bun, chat, send your latest, add command here. And now that we have command, let's go ahead and let's import everything we are going to need here from the command inside of this toolbar. I'm going to add it here like this. And let's see how we use it. Okay, so command, we have the command input here. Let me just see. I think they should have an example of command they do right here. And how does the code for that look like? OK, let's see. So it starts with the command dialog being wrapped around. And where exactly is? Oh, OK. So that's how we're going to do it. Let's go ahead and copy this command dialog here, this entire thing like this. And I'm just going to add it. Let's see. Where can we add the command dialog here? Well, I think we can just add it below the button here. like this. And what we're going to do now is I have to intend this entire thing, right? But also let's, okay. So we hit command empty. We have command group and let's just have one command item here, which will be test item. And let's remove every other command item until we reach the end of the group. And then we have the command separator. And then let's have another one here, which will be another test item. So I will simplify this for you so you can pause and copy it. There we go. So this is our entire code, right? So command dialog, command input here, and then command list with two groups, one for suggestions and one for settings like this. Now let's go ahead and implement the open and set open here. So open, set open. use state and false and import use state from react i'm just going to move that here like this there we go so now by default i think that should not be visible so what we're going to do is that when we click here it should open up that dialogue so let's go ahead and add a button here on click, set open, be true. So now when I click on search invite, there we go. It opens up this command dialogue. And inside of here, I want to have my channels and my members. So let's go ahead and try and implement that here. So I'm going to change the heading here to be channels. and here it will be members like this. Let's go ahead and add our loading here. So I'm going to move this here and let's get use get channels and I think I can just pass in the workspace ID. I believe it works as simple as that. Use get channels from features API use get channels and inside of here we should have the data channels and then we should have the members. So this will be use get members and also by workspace ID. So we should have all of these hooks by now because we are basically doing the same thing we did in this sidebar here. We are loading the channels and the members. So now what we're going to do is inside of the channels group, we can go ahead and go over channels, question mark map, get the individual channel and render the channel item here, the command item with channel.name inside. So let's see if I click here. There we go. My channels. So if I go ahead and create a new channel and click again here, there we go. We have the option of a new channel here. Perfect. And now let's go ahead and do the same thing, but for my members here. So we're going to look through members, get the individual member, and we're going to do member. And now can I do user.name? I can. Perfect. Member user.name. There we go. We also have the members now. Perfect. And what I want to do now is give them some, can I use an href here? I can't. Can I mark them as link like this? And can I give this as child prop? I can. Perfect. So let's import link from link. is it like this there we go link from next link and let's give this an href of slash so for members it's workspace workspace id member and then member id let me copy this link and add it here for the channels and close it. And then we're going to use channel. So channel and channel ID. So I think that now when I click in this, there we go. It should open the channel, right? But the only thing that seems to not be happening is that it does not close. So can I add an on click here? set close set open false will that redirect and close at the same time now it looks like it does not okay so what we're going to do instead is the following we're going to define const on channel click which we'll call set open to false. And then we're simply going to do use router from next navigation. So make sure you add this here as well. There we go. And if the channel has been clicked, we will do a router dot push. And we can just copy the one from here. So we request the channel ID inside of here. Which will be a string. Like this. And then copy and paste this. On channel click. This will be on member click. Member ID. Member. Member ID. And then we can just add the on clicks here. So I'm going to call this on channel click. Channel ID. Like this. And remove this link. we don't need it and same thing here no need for as child and on click here we'll simply do on member click member id and remove the link from here and i think that now it should both redirect and close so if i click here oh looks like on click is not working uh okay so let's just quickly see why that's happening I think it's because of on click we should use on select here. So let's try that now. If I click this, there we go. So now we can use this to look for members or to, well, switch channels. Amazing, amazing job. Now I want to fix this flashing effect which happens every time we refresh, which is also this warning we have that one of our panels does not have a default size. So let's go ahead and fix this layout shift by going inside of the source app folder workspace workspace id layout.tsx and let's find the resizable panel which encapsulates our children and let's give it the default size of 80. So I'm not exactly sure what are the limits of how this sizing works, I assume 100 is maximum. So if the default size of this panel is 20, I suppose this one is left with 80, right? So now when you refresh, you can see that there is a much, much lower layout shift happening here. And even if you have a panel opened, it looks great. Excellent. So we no longer have that large layout shift. There is still a shift because of our local storage, right? So we added the autosave ID here. So during server-side rendering, it doesn know where it will resize There is a fix for that If you want to you can explore the resizable panel Let me just find it The react resizable panels has a guide on keeping the layout shift with cookies alongside local storage. So that way it will be already known on server side. But I think this is already an improvement over what we have. The second thing I kind of want to resolve is this emoji picker. So if you want to, you can leave it as it is. But, you know, a few times in my project, it crashed. So let's see whether we can add another emoji picker. Let's go inside of the emoji pop over here. And basically, we're using Emoji Mart. I Googled and it does have some weird issues, but I couldn't find a way to fix them. So now let's do bun add emoji picker react. which is a package from npm here and let's import that instead inside of the emoji popover component so let's import emoji picker from emoji picker react and let's render that in here so it will will just be emoji picker and we don't pass in any data and instead of on emoji select this will be on emoji click and let's remove the imports from emoji mark here and the on select here will pass an emoji but I have no idea what this emoji actually is so I will first do a console log of this emoji here. And I will comment out everything else. So let's try this out again. So I will refresh this and I will open a console, my inspect element here. So let me try and click emoji. It definitely opens the emoji bar. That's great. And if I click this, I get the emoji back. And so instead of .native, it is emoji. right so that's the difference that's the difference so I will go ahead and send this in on emoji select but now we have some problems right so let's find the emoji popover component and I think we're using it in two places three places it seems the editor the reactions and the toolbar so let's see how any of them work so inside of the if you if you even want to do this change. So if your emoji is working fine, you don't have to do this change. But in case it's not working, let's go ahead and fix this. And let me just see something. Do we ever use anything besides the emoji native? No, we always use it. Okay, let's improve this. Let's go inside of the emoji popover. And instead of sending the entire emoji, let's call this value. And let's simply do value dot emoji. And I'm not sure if maybe this is even strictly typed. Does it have, it does have type picker props, uh, emoji click data. Perhaps that will be the type here. So value will be emoji click data, which you can import from emoji picker react. And that's a type like this. So then you can see you have it hard coded dot emoji here. There we go. And go ahead and change this to be a value of string because we will just be passing this value.emoji. That's all we need. Now let's find all the places we use emoji popover, starting with the editor component here. So that's inside of the source components editor. We have the emoji popover and on emoji select. Let's see that function here. So instead of here, we can do this much simpler by this is just a value and string or let's call this emoji value so we don't override the existing value constant and simply pass in the emoji value here like this. Let's go ahead and search further. We have it in the reactions component, which is already throwing an error here. So inside of source components reactions, on change, instead of emoji.native, we're just passing the emoji, right? Because this is now a string. We changed it so that on emoji select from the emoji popover directly gives us the emoji. And the last one. is inside of the toolbar. So the inside of source components toolbar, and we also have an error here, and we also just have to remove the dot native because we just improved this by a lot. So let's try it out now. Let's see if it works in all places. So I'm going to go ahead and add a smiley, and if remove the smiley, as we expected, let's go ahead and add this. There we go. Let's go ahead and add here. All of this seemed to work just fine. I can add it from here as well. Perfect. So that's how you change your emoji picker if the older one was causing you any problems. Amazing, amazing job. Now let's go ahead and let's deploy the project. So go ahead and create a new GitHub repository. I'm going to go ahead and call this next14 Slack clone. I'm then going to go ahead and select the private repository option and create repository. Inside of here, I'm going to select the second choice, which is an existing repository. So I will copy those three lines. And in my non-running app, I'm going to paste all of them here. Once I refresh this page, there we go. My entire project is now here. Now let's go to Vercel.com and deploy the app. Once you found the add new project button on Vercel.com, you should be able to see the Next14 Slack clone right here and select import. And now let's go ahead and let's follow along with Vercel here, but also with convex instructions on deploying to Vercel. So let me zoom in here. So we have a Vercel account. We linked the project. And now we have to do the following. Overwrite the build command. So we have to override the build command to be this. npx convex deploy. And then there's their cmd npm run build. So I'm going to be using npx and npm here. But I think we can also replace this with bunx and bun run build. So let's go ahead and go inside of new project Vercel. and let's open this and let's click over right here. Instead of next build is going to be npx convex deploy dash there CMD npm run build like this. All right, so we've set that up. Now we have to set up the convex deploy key environment variable. So let's go ahead and go inside of our convex dashboard, then settings page and then generate production deploy key. So let's prepare the convex deploy key here inside of our cell. So environment variables, convex deploy key. Let's go inside of our database, settings. And I already forgot what it was. There we go. So deployment URL and deploy key. So let's create a production deployment here. Like this. Let's go inside of settings again. So now you can see that inside of here, I have the option of going to production or development for my Slack tutorial, right? So right now I'm in production. So let's go inside of settings again. and inside of here, I now have the option to generate deploy keys. So let's click generate production deploy key. I'm going to call this production. Click save. And there we go. We now have this key right here. So let's copy that key. If that's what we are supposed to do, let me just double check. Yes, generate production deploy key. And then we have to paste that inside. Let's go ahead and go to Vercel here and add the key. It should have the prod prefix here. And let's click add. There we go. And I also want to go inside of my dot environment dot local. Next public. Okay, so I don't think we have to add anything else here. I think that should be it. The only thing we can double check. Well, actually, let's hit deploy. And then we'll see if we have any errors. We'll just go ahead and fix them. So I'm going to pause the screen now and I'm going to unpause when this is finished or failed so we can resolve it. All right, so my deployment failed because it seems like I'm missing some key props right here. So let's go ahead inside of my workspace, workspace ID toolbar dot TSX. Let's resolve that. So source app folder workspace workspace ID toolbar dot TSX. and unfortunately I am not getting the, it's a shame that I'm not getting the errors here. I think that my linter is maybe taking a break. So if I reload the window, will I maybe get the errors now? There we go. Now I'm getting the errors. Okay. So let's go ahead and give our command item a key here of channel ID. And in here, a key of member ID. There we go. So now in order to resolve this, we have to go back inside of our terminal, do git add, git commit, fix build errors, and git push. And git push will automatically call another deployment process here. so you can click back here and you can just find your project here you can see I have a bunch of projects so going to your latest one here and inside of your deployments you should now see a new deployment has started right so you can go ahead and click here whoops this opened the source code so find a way to open it like this I will pause the screen again and we'll see if we have something more to fix. And there we go. Looks like this time it passed. So let's go ahead and let's click on this main domain. Always look at this. This is your main domain. Everything else are just previews. So let's go ahead and click here and let's see if we can break something. So now you should have no channels, nothing, no users inside. So I'm going to go ahead and try and continue with GitHub. And it looks like we can't do that here because I think we need to set additional environment variables here. So let me remind you that inside of our convex here, where is my, you can see that in production, I don't have any environment variables here, right? But inside of my development here, I have a bunch of these environment variables. So we're going to have to configure those. So if we visit convex out again, and if we visit the production instructions here, you can see that they have the dash dash production flag. So let's see if that will help us here. So let's go ahead and start.\nof our project and either run npx this or bunex this and let's see if that will now fix there we go so enter the url where your site is hosted so for that we have to copy this url so make sure that you copied this one and not the any of the preview ones so this is your url now so let's add this but i will have to slightly modify it by just removing slash out here There we go. So this is my full URL where I'm hosting my website. So now it's configuring the private and the public key. And for the GitHub and for the Google, we're going to have to set it up ourselves. So now if we visit our database again, there we go. So this is on development. Let's go and click production again here. There we go. Now we have these three. Perfect. So we can now, I believe, already start creating our accounts. but let's go ahead and let's configure the Google and GitHub, but this time for production deployment. So let's go ahead and set up our GitHub settings like the first time we did. And let's go inside of developer settings here, OAuth apps, and let's create a new OAuth app here. And I'm going to call this Slack production. Now for the homepage URL, this is our homepage, right? Basically our new Vercel URL here. And for the authorization callback, we don't have to go inside of our database. Make sure you're looking at the production view. You need to have prod written here. And now we have to find, I think it's HTTP actions URL. I think this is the one. And then you go here and add this slash API slash out slash callback slash GitHub like this and click register application like this. And now you have the client ID and the client secret. So there are different ways you can add this to your production on convex. Now you can either go here and just manually add it, or if you want to use this, I think you can do Bunex and I just not entirely sure So I think is convex environment set out It will be GitHub ID and then paste it here And I think they should have the dash dash prod flag here. So if I do that, there we go. If you add prod, it will add on prod here. There we go, out GitHub ID. So that's how you can add it in production. And now let's go ahead and generate the secret from here. I just have to verify. Now let's copy the secret. And let's do Banex convex environment set out GitHub secret and paste that and dash dash prod. There we go. Now this is on production as well. So if I go ahead and revisit it again, I have out GitHub ID and out GitHub secret available here. So that should be exactly as I have in my development, right? There we go. outgithub.id and outgithub.secret. And we need to do the same thing for Google here. But let's just quickly try out our deployment here. Is this even working at the moment? Can I connect my GitHub now? There we go. I can connect my GitHub and I should be redirected here and say hello world. And there we go. General is right here. Hello. We are sending messages and our emoji bar seems to be working. Let's see if the threads are working. Hello from production. Let's try it like this. Hello from production. There we go. Let's try sending an image here. Hello image. Let's see if that will be working. There we go. That is working in production as well. Perfect. And I think I've never tried sending an image in thread, but I think image in thread should work just fine. There we go. It works here as well. Perfect. Amazing. And now what I want to do is also show you how to do the Google Cloud solution here. So go inside of your Google Cloud platform and let's go ahead and create a new one So I going to call this Slack production Now we create a new organization here Let wait a second for a Slack production here to be finished Basically we doing the exact same thing we did in our first authentication chapter, right? So make sure you're in Slack production. Let's go to APIs and services here. Let's go instead of OAuth consent screen here. And now let me just close this. It will be external. Let's click create. We're going to call this Slack prod. Let's add an email here. Skip the logo and add another email here as well. Click save and continue. You can go ahead and click save and continue here and save and continue here. and for authorized domains, I think that maybe now we can actually add this URL here. So let's try doing that. Can I click edit here? And let's try adding authorized domains like this. Can I do that? Or just this? Can it even be a subdomain? Okay, I think we can do that. Perfect. Now let's go inside of credentials. Let's click create credentials or our client ID. And let's go ahead and select web application. It doesn't matter. Authorized JavaScript origins here will also be our URL like this. And authorized redirect URL, URIs, here will be our production. Make sure you're in production here. URL and deploy key, HTTP actions URL. So make sure it's the production one, the same one you've added previously to GitHub. And so let's go ahead and add this here. Slash API, slash out, slash callback, slash Google this time. And let's click create. And now we should receive the client ID. So let go ahead and do Linux convex environment set out Google ID Dash dash prod Make sure you add dash dash prod at the end And then copy the client secret So that will be Bunex convex environment set out Google secret dash dash prod as well. And now if I go ahead and visit my environment variables here, there we go. I have out Google ID, out Google a secret, all of these things. So if I try my deployment here, there we go. I'm going to go ahead and log out. And let me try and log in using Google this time. And I will select an account of mine here. Let's see if this will work or not. There we go. Looks like it's working just fine. This is my Google account here. Perfect. Amazing, amazing job. So one thing I've noticed is that if we go to the root of our application, let me do this so you can see my url there we go so if i go to the root of my application like without any workspace specified i have this weird you know loader in the corner and my user button so if you want to you can resolve that as well by going inside of source app folder page.tsx and simply do one of our you know a loader things so just copy it from any page with the H4 flex item center and the loader here and just render this instead. No need to render anything else at all. We don't need the user button like this. And I think that that should be it. So instead of layout here, this should be fine. Yeah, so now what you would have to do to update this is simply git add, git commit, fix loading and do git push. and you can follow along inside of your cell here by clicking here on deployments. There we go. It's building another deployment and then that will be fixed. Great. You now have both the production and the development environment. You've set up the out and we've shown that the upload works, the emoji works, everything works just fine. Amazing, amazing job. Thank you so much for watching and see you in the next tutorial.",
  "transcript_chars": 308067,
  "transcript_filled_at": "2026-06-06T15:27:22.241691+00:00",
  "transcript_filled_by": "tk-bulk-groq-retry-20260606"
}