
No Comments Yet
Be the first to share your thoughts and start the conversation.
Be the first to share your thoughts and start the conversation.
By logging in, you'll unlock full access to this and other free tutorials on JSM Pro.
Why? Logging in lets us personalize your learning experience, track your progress, and keep you in the loop with new workshops, coding tips, and platform updates.
You'll also be the first to know about upcoming launches, events, and exclusive discounts.
No spam—just helpful content to level up your skills.
If that sounds fair, go ahead and log in to continue →
Enter your name and email to get instant access
00:00:00Â I'll open up the root documents and then the page under the ID.
00:00:07Â Oh, and silly me, I haven't even put anything in here.
00:00:12Â Remember when we took out the editor from here, we put it in a collaborative room.
00:00:17Â So what we have to do now is finally call that room right here, collaborative room.
00:00:26Â It's a self-closing component, which we can just put right here.
00:00:30Â And we can also make this a main because it's the primary thing on the page.
00:00:35Â and give it a class name of flex w-full flex-col and items-center.
00:00:43Â Now, if I save this and go back, would you look at that?
00:00:48Â Our editor is back.
00:00:50Â And now this is not just a random document.
00:00:53Â You can see that this is an actual document with a special ID, which is tied to a LiveBlocks collaboration room.
00:01:00Â And we can even verify that by going to our dashboard under rooms.
00:01:06Â And you can see that the room with that same ID has been created and that room has storage attached to it.
00:01:13Â And the comments and the metadata and the permissions will look into all of that very soon.
00:01:18Â So to give you just a little glimpse into the collaborative features that we're going to implement later on.
00:01:24Â I'll show you how to display active collaborators on the top right of our screen.
00:01:28Â Right next to your logo, you'll be able to see the logos of other people that are viewing or editing the same document.
00:01:36Â So let's do that next.
00:01:38Â To start adding active collaborators, we'll have to quickly head over to our RoomActions.ts And just for the time being,
00:01:46Â modify the create document.
00:01:48Â Currently, we have user accesses, and only the user that has created the document will have the write access.
00:01:55Â Other people will have no accesses.
00:01:58Â But what we can do for the time being, before we implement the share functionality, just so we can see how other people can access the document,
00:02:06Â we can modify the default accesses to also include Room Write.
00:02:11Â Only for the time being, later on we'll switch this back to no access by default.
00:02:16Â So now that we have done this, we can create a new document by going to the homepage.
00:02:21Â And here you can create a new blank document.
00:02:24Â And you can see now we have a different ID and we can type test right here.
00:02:28Â So now how would we see the other users that are looking at or editing this document?
00:02:33Â Let me show you.
00:02:34Â We'll do that by creating an active collaborators list by going to components and creating a new component called ActiveCollaborators.dsx.
00:02:45Â There we can run RAFCE.
00:02:49Â And we first need to get access to other people viewing the document.
00:02:53Â And that is super simple using LiveBlocks.
00:02:56Â The only thing you have to do is say const others is equal to use others coming from LiveBlocks React Suspense.
00:03:05Â What a simple way to get access to the other collaborators.
00:03:09Â Now we can extract them by saying const collaborators is equal to others.map where we get each other person.
00:03:18Â And for each one, we just want to return the other.info.
00:03:22Â Now that we have them, we can return them in a list by returning a UL, an unordered list, with a class name equal to collaborators list.
00:03:33Â And within it, we can map over the collaborators by using the collaborators.map.
00:03:39Â And for each collaborator, okay, take a shot every time I say collaborator, for each collaborator, you can just return a new list item like this.
00:03:50Â Each list item has to have a key since we're mapping over it.
00:03:54Â And that's going to be equal to, well, I won't be repeating collaborators anymore.
00:03:59Â Rather, I will destructure the values out of it.
00:04:02Â So I will destructure the ID, the avatar, and the name.
00:04:07Â And now the key will simply be the ID of that collaborator.
00:04:14Â And then right below the Li, we can have an image where we can render a source equal to avatar.
00:04:22Â We can render an alt tag equal to name.
00:04:26Â We can render a width of 100, a height of 100, a height of 100. Let's also make sure to import the image, just so we don't forget,
00:04:35Â and give it a class name equal to inline-block, size of eight.
00:04:42Â rounded-full, ring of 2, and ring dark 100. And we can also give it some custom styles by saying border is dynamic, 3 pixel solid,
00:04:57Â and then we can pass the color right here dynamically.
00:05:01Â Great.
00:05:02Â And this is it for the active collaborators.
00:05:05Â But where will we render this?
00:05:07Â Well, let's head back to our collaborative room and go inside of the header.
00:05:13Â Below this existing div, we'll create a new div.
00:05:17Â This one will have a class name equal to flex, w-full, flex-1, justify-end, gap of two, and in small devices, a gap of three.
00:05:30Â And within there, we can render the active collaborators self-closing component.
00:05:37Â just like that.
00:05:38Â We can also put this signed out and signed in thing right here in that second div, since we want to show them at the end of the screen.
00:05:47Â If I save this, we can just see the loading.
00:05:49Â I think this is because we're missing something in the provider to make the collaborative experience with other users work.
00:05:56Â So if we go back right here, we need to navigate over to the provider component.
00:06:02Â And here, we have to pass some additional properties to the LiveBlocks provider.
00:06:07Â we have passed it the live blocks off, but right below it, we also need to provide a way in which it will resolve the users.
00:06:16Â So this will be an async callback function.
00:06:19Â So that's going to look something like this.
00:06:21Â That's going to accept all the user IDs and we're going to get all the users by saying const users is equal to await get clerk users.
00:06:34Â And this is a function which we have to create.
00:06:36Â So let me comment it out for now and let's create it under user actions.
00:06:41Â So I'm going to go to lib actions and then create a new file called user.actions.ds.
00:06:52Â And here, don't forget to add the use server directive at the top and export const a new function called getClarkUsers, which is an async function that
00:07:04Â accepts user IDs.
00:07:06Â of a type, user IDs, a string array, and we can open up a new try and catch block.
00:07:14Â In the catch, we can say something like console log.
00:07:18Â And then say error fetching users.
00:07:22Â And in the try, we can try to fetch all clerk users by saying const.
00:07:27Â We can destructure the data that we get back by calling the await clerk client coming from clerk next.js dot users dot get user list.
00:07:40Â And to it, we need to pass the email addresses of all the users, which we're storing as user IDs.
00:07:47Â That's going to be email address right here.
00:07:50Â Once we get that data, we need to form it into users by saying const users is equal to data.map where we get each individual user and we want to immediately
00:08:02Â return an object for each one of these users.
00:08:05Â And immediate returns means that you wrap the function block into parentheses.
00:08:10Â So that makes it not a function block.
00:08:12Â It just makes it a return of an object.
00:08:15Â So then say that the ID is user.id.
00:08:20Â Name is a template string of user.firstname.
00:08:26Â and user.lastname.
00:08:29Â After that, we have an email, which is equal to user.emailAddresses, zero.emailAddress, and the avatar will be user.imageURL.
00:08:44Â So now we're forming our users, and we want to sort the users by the user ID to make sure the order is consistent.
00:08:53Â We can do that by saying const sortedUsers is equal to user IDs dot map, where for each email, we can run the users dot find,
00:09:08Â and we can compare that user.
00:09:10Â So if user dot email is equal to the email that we're searching for.
00:09:15Â So what we're doing here is once again sorting the users by the order of the user IDs to make sure that the order is consistent with those user IDs.
00:09:25Â Finally, we can return parse stringify coming from utils and then pass in the sorted users.
00:09:32Â Now, going back to the provider, we can uncomment this line and import get clerk users from lib user actions.
00:09:40Â And to it, we need to pass the user IDs that we're getting from LiveBlocks right here.
00:09:44Â Finally, we can return the users.
00:09:48Â Now, if we go back, there's still a loading here.
00:09:51Â So what I can try to do is stop the terminal from running by pressing Control C and then rerunning it once again.
00:09:58Â I get this weird error saying that I have two dots at the signup and it actually should be three.
00:10:05Â So if I go to sign in, you can see dot dot dot.
00:10:08Â And here in the signup, I had two dots.
00:10:10Â Okay.
00:10:10Â Didn't complain before.
00:10:12Â Now it complains.
00:10:12Â So I'll just add an additional dot and rerun my application.
00:10:17Â It's good now.
00:10:17Â And we're back on localhost 3000. So if I reload the same screen, it is still loading.
00:10:24Â If you open up the console, you'll notice this very convenient and very descriptive errors from LiveBlocks saying, you have no access to this room,
00:10:34Â we're closing down this socket for you.
00:10:36Â And that's why we can see the loading.
00:10:38Â So what this means is that we have to provide our user with the access.
00:10:43Â Now, why does our user not have the access already?
00:10:47Â Well, if you go to the collaborative room, you'll notice that we're wrapping this entire document and the editor with the room provider.
00:10:55Â And currently, there's a hard-coded My Room ID right here.
00:11:00Â So we have to find a way to get access to the ID of a specific room belonging to that document.
00:11:06Â And we can do that by going to room.actions.
00:11:10Â And after we create a document, we want to create a new function or action called export const getDocument, which will be an async function accepting the
00:11:24Â room ID and the user ID.
00:11:26Â So the room ID and the user ID, we have to specify their types.
00:11:31Â They're going to be of a type, room ID is a string, and the user ID is a string as well.
00:11:39Â Now moving here, we have to get access to a room by saying const room is equal to await liveblocks.getRoom to which we pass the room ID.
00:11:51Â Next, we have to check if the user has access to the documents room by saying const has access.
00:11:58Â We do that by using the object.keys on the room user's accesses, because it's an array of different objects.
00:12:07Â So we can say room.user accesses, and then we can call the .includes on it to check whether this array includes the ID of the user that is currently trying
00:12:18Â to look at this room.
00:12:21Â we have no access, we can just return null or maybe even throw a new error saying something like you do not have access to this document.
00:12:33Â Else we do have the access so we can return parse stringify room.
00:12:39Â And all of this can be wrapped in a new try and catch block.
00:12:43Â So let's do this.
00:12:44Â I will wrap it in a try and catch.
00:12:47Â And right here in the catch, we can do a console log saying something like error happened while getting a room.
00:12:57Â Now, where will we call this getDocument?
00:13:00Â We can call it on a page where we're trying to fetch the information about that room, which is right here in the document page.
00:13:08Â So this is the one that is under an ID documents.
00:13:13Â And here we have access to the ID of the document we're trying to access, coming straight to us through params.
00:13:20Â So we can say params and then destructure the ID.
00:13:23Â And that is of a type search param props.
00:13:29Â We can also get access to the user ID by saying const Clark user is equal to await and then get the current user from Clark Next.js.
00:13:39Â And of course, since we're using await, we can make this function async and say, if there is no Clark user, in that case,
00:13:47Â we can redirect.
00:13:49Â So let's say redirect using the next navigation to forward slash sign in.
00:13:56Â But if we do have the user, we want to check people's permissions to this room by calling this new function we created.
00:14:03Â const room is equal to await getDocument, which we need to import from room actions, to which we can pass the room ID equal to the ID from params and the
00:14:16Â user ID equal to clerk user.emailAddresses0 In this case, we're using the email address as the ID, since it's unique anyway.
00:14:31Â If there is no room, we want to redirect back to the home.
00:14:35Â And later on, once we play with more permissions, I'm going to add a to-do right here to assess the permission level that the user has.
00:14:45Â But for now, all users have permissions, so we should be good.
00:14:49Â We just need to pass the room information to our collaborative room, such as the room ID equal to ID, room metadata equal to room.metadata,
00:15:00Â and that's it for now.
00:15:02Â So let's go into the collaborative room and let's accept these props.
00:15:07Â I'll get a room ID, as well as room metadata, and say that's of a type collaborative room props.
00:15:16Â So now that we have this room ID, well, we can very easily exchange this fake ID with the real one.
00:15:23Â So if we go back, LiveBlocks will have no issues loading this document up for us.
00:15:29Â Great.
00:15:31Â Now what you can do next is copy this URL and open up a new browser or the same browser in Incognito mode.
00:15:39Â In my case, I'm currently using Arc, so I'll switch to Chrome and I'll sign in with a different account.
00:15:47Â And if I paste it.
00:15:49Â we get redirected back to home.
00:15:52Â I think I know why that is.
00:15:54Â If we go back to Room Actions in the Get Document, right now this Has Access is checking the user by email.
00:16:01Â But for the time being, as we said, everyone can access the document.
00:16:05Â So let's simply remove this Has Access check.
00:16:09Â And I will add myself a To Do to bring this back later on.
00:16:16Â So now, if I paste the URL once again, you can notice that the app breaks, which is great.
00:16:22Â Trust me, this is a good one because it means that everything was supposed to work.
00:16:26Â It was very close to working and it actually did work, but we have to add the source of our profile photo.
00:16:34Â to our next config.
00:16:35Â So let's just copy the host name, img.clerk.com, which is where the image is being hosted.
00:16:41Â Go back to our app and go to next.config.mjs.
00:16:46Â Right here, we'll have to create a new object called images, add remote patterns, which is an array of an object where a protocol has to be HTTPS,
00:17:00Â and then we can add a host.
00:17:03Â which in this case will be img.clerk.com.
00:17:08Â And this was supposed to be host name.
00:17:11Â There we go.
00:17:12Â And I believe we'll have to rerun our application once again.
00:17:15Â Oh, that's good.
00:17:16Â It's letting me know that I have a typo, no colon right here.
00:17:20Â So if I fix it, we should be good.
00:17:22Â I reloaded the app on both my screens.
00:17:25Â And would you look at that?
00:17:27Â Right here next to my logo, we have another user doing something.
00:17:33Â Right now it looks a bit weird because both of my accounts have the same JavaScript mastery logo.
00:17:39Â But if you have different photos, you'll be able to see that you are active on that document and another person is active as well.
00:17:48Â And if I close my Chrome, you'll notice that in real time, that live collaborator is gone.
00:17:56Â Great.
00:17:57Â But just having that live collaborator there isn't doing much.
00:18:00Â We just see that somebody is viewing the document.
00:18:03Â But soon enough, we'll start implementing other collaborative features, such as comments, the ability to edit the document at the same time,
00:18:11Â live cursors, and so much more.
00:18:14Â But before we do just that, let's fix up the document header.
00:18:18Â We'll make this share button look a bit nicer and add the input field so we can change the title of the document.
00:18:25Â To do that, I'll close all of the currently open files and navigate over to our collaborative room.
00:18:34Â Here we have our header and within it, we'll add some of these additional functionalities.
00:18:39Â So let's do that next.