So now let's go ahead and let's build the cursor presence component which is going to render other users cursors in real time. So in order to do this chapter make sure that you have at least two users inside of your board, right? So you need to invite someone to your organization or if for any reason you're having trouble with that and you skip that part you can also modify the API live blocks out right here. So if you want to you don't have to throw 403 for a person which isn't part of your organization. So you can comment this out if for any reason you're having trouble with the invites and just want to share the URL across browsers.
So you can do that little trick here. Great! So just ensure that at least two users are inside of your application and now let's go ahead inside of the app folder board.board.id.components.canvas.csx and now what we are going to do is just below this toolbar we're gonna open up an svg element so let's write svg like this and inside we're gonna open a g element like that And then let's go ahead and give this svg element a class name of height of 100vh and a width of 100vw like this. So inside of this G element is where we're gonna store all of our drawable, resizable, translatable elements. All the way from texts to sticky notes everything will be inside of this SVG element and I don't know what this error is but I'm pretty sure it's just some weird cache.
Okay, and inside of this G element I want to go ahead and add a component Cursors. Presence. And the reason I want to do that inside of SVG, why wouldn't I add that you know on the same level as toolbar for example? Well we could but we are also going to reuse the cursor's presence to also render when another user is drawing in real time. So for that it's gonna have to be inside of our SVG element because later this SVG and this G element are gonna have functionality on our scroll wheel to kind of change the camera position.
So that's why we need to put that inside of here. Now let's go ahead and create the cursor's presence. So inside of underscore components, I'm going to create cursors presence.csx like that. And all I'm going to do is mark this as use client and then I'm gonna go ahead and import Memo from React. Since this will often change, let's go ahead and ensure that the throttle is minimal, that the connection is fast so for that we're going to memoize this component and let's go ahead and export const cursors-presence to be memo and return an arrow function which for now can just return a fragment and a paragraph of cursors.
And now we have this error because we have to add display name to this cursor's presence and let's just add that cursor's presence. Like this. Now you can go back inside of canvas and you can import cursor's presence and there we go, like that. And nothing should really be visible right now, I don't think that text that text probably is somewhere but we cannot see it right now. Nevertheless, let's go back inside of the Cursors presence now and I want to go ahead and I want to import UseOthersConnectionIds from LiveBlocksConfig, like that.
And then I'm going to go ahead and create another component here called Cursors and that is very simply going to get the IDs from useOthersConnectionIDs and it's gonna go ahead and return a fragment where we're gonna go ahead and iterate our IDs.mapConnectionID like this and inside we're gonna render an individual cursor element which doesn't exist yet but it is gonna exist in a second and let's pass in the key to be connection id and let's pass in connection id to be connection id like that there we go And now let's actually use these cursors and let's render them right here inside of this fragment. So later inside of this fragment we are also gonna have draft pencil component which is going to track and display to all users what another user is drawing at that time. But for now we're just going to handle the cursor movement. And now we have to create this cursor component. So inside of underscore components create cursor.csx and inside of the cursor let's go ahead and mark it as use client let's import memo here from react let's import Connection ID to color from libutils and let's also import mouse pointer2 from lucidreact so we have this connection ID to color in our libutils if you remember when we created our colors array and then we use the connection ID mod on the colors length to get a random color from the array.
And now we're going to use that again here. First let's actually create an interface. Cursor, cursor crops is simply going to have the connection ID, which is going to be a required string. So now let's write memo and let's go ahead and return this. And let's go ahead and immediately write cursor display name to be cursor and let's go in here and return a paragraph for now just so we get rid of the error and then inside we can assign the prox and extract the connection ID.
Like that. Great. So first let's get the info of the user using the connection ID. So for that I'm going to use useOther from live blocks config. Let me just move that here.
So useOther is going to use the connection ID it's going to get the user and extract user info and let's go ahead and connection ID is a type of number, my apologies, not a type of string and then we're gonna have cursor to be useOther, connection ID user, user presence cursor and then we're gonna go ahead and write const name to be info question mark dot name or teammate let's do that and if there is no cursor we're gonna go ahead and return null And ignore this TypeScript error for now. We're going to go ahead and extend the presence object in a second so we can resolve that. Great, and then what we're going to be able to do is extract the coordinates from our cursor. The problem is right now None of these things are defined right here You can see that cursor can be a type of any and our user presence does not recognize it so we have to go ahead inside of LiveBlocks.config and modify this let's go inside of LiveBlocks.config so this root file and in here we can find just above user meta I believe the type presence. So let's go ahead and we can just uncomment this line or write it if you don't have it.
So cursor can be an object with coordinates or null. And you can see how now we no longer have that issue here and we can safely extract the coordinates here. And let's just do one more thing. And that is we're going to go inside of our reusable component room right here. And we're going to pass in the initial presence.
So we get rid of this error right here because we now added something to the presence so we have to go ahead and pass cursor be null by default and that way we no longer have the TypeScript error here. Great! So that resolves the cursor component and now let's go inside of CursorsPresence and let's just import that so we don't have the error here either. So cursor from dot slash cursor component. There we go.
And now we can safely continue to develop right here. So in order to render the icon inside of an SVG parent, we have to use an element called foreign object. So let's go ahead and add foreign object. And then inside we can render mouse pointer two, which is a self-closing tag. And let's give this a class name of h5 and a width of 5 and let's go ahead and give this a style property because all of these are going to be dynamic so fill is going to use connection id to color and pass in the connection id props and we're going to have the color itself be the exact same thing with the exact same param.
And now we have to find a way to move the foreign object according to these coordinates right here. So let's give this a style prop and pass in the transform, open backdicks and write translate X open parenthesis and open a template literal like this, PX. And then we're going to go ahead and copy this and do the same thing for the Y axis. Like this. And now let's go ahead and pass in the height to be 50 and let's give 50 to the width as well.
And let's give this a class name of relative and drop shadow medium like this. And I believe that this already might be enough for us to showcase a cursor. So go into your other browser. Let's try and refresh this. So we're going to debug if it doesn't work.
So I am moving my other cursor, but I cannot see anything on my screen so it looks like we didn't do something properly. So I'm gonna go ahead and add some console logs, debug and tell you what I found out. So the reason we are not seeing anything is because I completely forgot that in our canvas component we actually need to update our presence. So what I did was I just did a console.log of info and cursor in this component here and then I made sure that in my other browser I refreshed my screen and then I refreshed the screen here as well. So let me just expand this and there we go.
This is what I saw. I saw that I am logged in in another browser I can see the other user but their cursor presence is null which is the initial presence which we've just passed in the common room component. So I remembered yes that is correct actually because I never update the cursor presence once I move. So we need to add an onMouseMove event and update the cursor presence. So we're gonna leave this as it is for now and let's head back inside of our canvas.tsx component right here and let's just collapse all of our imports from live blocks here and let's add use mutation.
So use mutation is a hook which can be used to kind of broadcast or stream some changes to the entire group of people connected inside of a LiveBlocks room. So let's go ahead and create a function, const onPointerMove, beUseMutation, and it's gonna extract a method called set my presence and in the other argument is gonna take the event which is a type of react.pointer event and let's go ahead and open this up let's prevent default in here and let's go ahead and simply do const current to be pointer well we have to actually create a lib for this so let me add for now is going to be x0 y0 and let's just do set my presence and passing the cursor to be the current like that. And then we have to add the dependency array. And I don't think we have to add anything in here for now. This can just stay as it is because we're not actually using anything yet.
Later the on pointer move of course is going to have a lot of options but not for now. Let me just collapse my elements here. All right And now what we have to do is we have to make the current actually get calculated by the our pointer event and the state of the canvas and the state of the camera. So we have a couple of more things to add. So first let's initialize the camera.
That's going to be very simple. It's going to be just a simple set state. So const camera set camera is going to be use state and we're going to pass in the default X of 0 and Y of 0. Basically, what I just did right here. That's gonna be the default camera movement.
And we can already define the type of the camera thanks to our types from canvas. So in here we define the type camera so we can use that camera from types canvas so just make sure that you import this. Great and now besides on pointer move let's also go ahead and implement on wheel So const on wheel is gonna change the camera so we can have infinite space once we use the scroll wheel. So that's gonna be react.wheelEvent. And I believe I need to import useCallback from React.
So let me just import that, make sure that you added useCallback. Let's add an empty dependency array here. And in here, all we're gonna do is set camera. We're gonna get the existing camera, open an immediate object, the new x coordinate is going to be camera.x minus event.deltaX and y is going to be camera.y minus e.deltaY. So basically what we are doing here is panning the camera based on the wheel delta.
And now we can use this on wheel and on pointer move and assign it to this SVG element right here. So let's go ahead and pass in the on wheel here and let's write on wheel and let's add on pointer move to be on pointer move. So we have those two now. But we are still not done. There is just one more thing we have to do we have to create a reusable util which is called pointer event to canvas point so we're going to use the position of the camera the position of our pointer and then we're going to calculate the coordinates on the screen that the user can see and where they are currently scroll that on their canvas because remember thanks to this on wheel and moving the camera they have infinite place to move their camera.
So let's go ahead and go inside of lib utils and here at the bottom I'm going to export function pointer event to canvas point. It will accept two arguments. The event which is react.pointer.event and camera which is the camera event. From is for the camera type, from types camera. So I'm going to import that right here and let's go ahead and open this function up and very simply is going to return an object with an x value which is math.round e.client and this is not an arrow function, my apologies.
So event.clientX minus camera.x and Y is going to be math.round e.clientY minus camera.y Like that. We're going to get the new coordinates for our pointer. And then we can use that right here. So let's go ahead now and let's add the current to be our new function pointer event to canvas point so just make sure you add this import from libutils I'm just going to move that here so pointer event to canvas point is going to accept two arguments the event and the camera state like this there we go So this should now actually control our presence and update our cursor. So let's go ahead and console log the current here.
Make sure that you've added on pointer move to the SVG element here. My apologies, I can't focus the scroll. And let's also add the on wheel console log here. So I'm going to add the console log and I'm going to log x to be e.delta x and y to be e.delta y just to convince ourselves that this is actually moving. So let me go ahead and open my inspect element and when I'm moving the cursor you can see that I have a presence.
So you can see that these values are changing every single time. That's great! And now let's try and do the following. Let's try and scroll. There we go.
You can see how when I scroll to the bottom you can see how my canvas increases right? I also have a vertical scroll so my x value increases so that's how we are going to simulate an infinite board so we need all of that information to properly display it to other users and I think that already if I go into my other application now and refresh I think I should be able to see my other user's cursor without almost any changes. There we go! I have my other user's cursor and I can also see my cursor, well you can't see that on the screen but don't worry later I'm gonna join the screens. But we can see that it is quite tacky, right?
It looks like kind of laggy. But we can improve that by fixing the throttling and well by removing all of these console logs, right? We don't need them. So let's go ahead and do the following. We can now remove the console log here.
And we can remove the console log here as well. And now what we can do is we can go back inside of our liveblogs.config.cs and in here what we can do is we can add the throttle to be 16 like this and this already is going to improve the behavior of our app there we go You can see how now it should be very, very smooth. Great. And now what I want to do is I also want to render the user's name. So you can already see that the color is matching.
This is my other user and their color are matching. So now let's also add the name of that user in the sidebar, in the little box below. For that, we have to head back inside of our cursor component. So that is located in app board board id components cursor and now here's what I want to change I want to dynamically calculate the width of this foreign object because we can't really use flex here and stuff like that. Well, we can, but in the inside elements.
We cannot use it on the foreign object. So in here, we need to use the length of our username to calculate the width of the foreign object. So I'm gonna write name.length times 10 plus an offset of 24 for our padding. And let's go ahead now here and let's render a div element which is going to render the name of our user. And let's give this div a class name of absolute so make sure that your foreign object has the class name relative so we can absolutely position it here left-5, px 1.5, py 0.5, rounded-median, text-extra-small, text-white, and font-semi-bold And we're gonna have a style so we can add the dynamic color, specifically background color to be connection ID to color and pass in the connection ID like that.
So let's try that out now and you can already see my cursor right here. Amazing, amazing job! So there is one more thing that I want to do. So right now, you can't really see it, but in my other browser, if I go outside of my window, what happens is that you can see this cursor right here being stuck here. What I want to happen if a user goes outside of canvas is that their pointer gets reset to null.
I want their pointer to be disappeared. Because this way, if they go to the top and switch their tab they are still positioned as being here, but that's no longer true. They are moving their cursor in another tab. So we can resolve that by going back inside of our canvas and let's add another function called on pointer leave and we simply have to assign that to the SVG element as well. So just below our on pointer move, let's add a constant on pointer leave.
That is also going to be a mutation from where we're going to extract set my presence and all we have to do is call set my presence and pass in an object cursor null like that. So let's go ahead and assign on pointer leave now to our SVG element. I think this is the right handler. It is on pointer leave matches on pointer leave great let me see if I can actually make this like that great so now if I refresh this and if I refresh my other element now what should happen is that there we go I'm here and if I go to the top you can see how I disappear now because I'm now switching tabs in my other browser. Right?
Or if I go and choose a tool, my cursor disappears because that's not what I'm currently doing. Perfect! And you can see how again it matches my color. And now to wrap this up I'm just gonna show you a side-by-side example of two browsers and my cursors matching. Here we go.
So in here you can see that I am a user called please no and here I'm a user called Antonio. So you can see that in here I can see the Antonio cursor and in here I can see the please no cursor. When I go outside of the canvas element, my cursor now disappears. Great, amazing, amazing job. As always, if you're having any trouble, you can always visit my github directly.
I don't know if you know this but I actually make my github commits according to the chapter so all you would have to do is find the chapter under the number 21 and you're going to see the exact code that I have changed for this exact feature. Great great job!