Having someone to oversee a video call meeting is important for numerous reasons, including being able to manage which participants are allowed in the call. With Daily’s Client SDK for JavaScript, participants can optionally join as meeting owners, which gives them special privileges like being able to remove participants from a call.
In some cases, it can be useful for meeting owners to share their administrator responsibilities with other participants, too.
In today’s post, we’ll look at sample code that demonstrates how to let meeting owners promote participants to admins, as well as how to let admins or owners remove other participants.
Daily meeting owners vs. admins
To start, let’s look at how meeting owners and admins differ.
Meeting owner privileges
A meeting owner is a participant with the most privileges related to managing room access.
Participants can join as a meeting owner by using a meeting owner token: a token that has the is_owner
property set to true
.
Being a meeting owner allows the participant to:
- Share media (audio/video/screen) in owner-only broadcast mode, if these features are enabled in the Daily room.
- Start or stop a live stream, if live streaming is enabled in Daily the room.
- Start or stop call transcription.
- Allow participants to join a private room by responding to knocking.
- Update other participants, such as adding or revoking admin permissions and muting their devices.
(See an example of creating a token using in a fetch request in today’s sample app.)
Admins
Meeting admins are similar to meeting owners, with two key differences:
- A meeting owner has to join the call with an owner meeting token. An admin can join with an admin token or be promoted to an admin mid-call.
- Unlike an owner, an admin can be demoted and lose admin privileges.
- An owner has all relevant privileges within the call, whereas an admin can be given more granular permissions. Depending on the specific permissions provided, admins can potentially do any of the actions listed above for meeting owners; however, they cannot remove a meeting owner from a call or remove their meeting owner privileges.
Now, onto the code!
Today’s goals
In this tutorial, we’ll look at sample code for how to build an admin panel to let meeting owners upgrade regular participants to admins or remove them from the call. We’ll use a sample app built with Next.js and Daily Prebuilt.
This tutorial will focus on two features:
- A button that converts a participant to an admin.
- A button that removes the participant (or admin) from the call.
We won’t cover some of the more general Daily-related code like rendering Daily Prebuilt in your app, but we’ll include some related blog posts at the end!
To test the demo app yourself, follow the instructions included in its README.
Project structure
When the main route of our app (e.g., localhost:3000/
) is visited, the component in the page.js
file found at the top-level of the app’s /app
directory is rendered – in this case, the Home
component, which parents all other components in the app.
The DailyContainer
component is the home of our video call feature and has a number of conditionally-rendered components/elements:
- A form (
JoinForm
) to join a call. - The
AdminPanel
component to upgrade regular participants to admins or remove them from the call. - The
containerRef
, adiv
which will contain the Daily Prebuilt UI once theJoinForm
is submitted and the call is created.
Creating a Daily room and joining the call
Let’s start by quickly familiarlizing ourselves with what happens after the JoinForm
is submitted.
The JoinForm
itself is conditionally rendered and shown by default. Once it’s submitted, it’s destroyed and the call-related components are displayed instead.
There are also two conditions to be aware of when the JoinForm
is rendered:
- (Default) The user is creating a new Daily room to join and will join as a meeting owner with an owner meeting token. They can then share a link to that specific room.
- If the participant is using a shared link with a
url
query param (e.g.,http://localhost:3000/?url=https://domain.daily.co/[room-name]
), a “Daily room URL” form input will be rendered to indicate which room is being joined and no new room or meeting token will be created for them.
This means there are two types of participants on join: meeting owners and regular participants. (No one joins as an admin!)
When the JoinForm
is submitted, a few things happen.
If the participant is joining as a meeting owner:
- A new room is created.
- An owner meeting token is created.
- An instance of a call frame (or
DailyIframe
class) is created. (Note: This is calledcallFrame
in the code samples below.) - The call is joined using the meeting token.
If the participant is joining an existing call, the call frame is created and joined without a meeting token.
Now that we know how our participants can join the video call, let’s see how the AdminPanel
component works.
Rendering the AdminPanel
component
The AdminPanel
component will render a list of all call participants. If the local participant has owner or admin privileges, each participant in the list will have buttons to remove that participant or promote them to be an admin. (Admins can’t remove owners, though!)
Before getting into how we remove/promote participants, let’s first look at how we keep track of them in app state. In DailyContainer
, there’s a participants
object saved in the component’s state:
const [participants, setParticipants] = useState({});
Any time someone joins the call, we update the participants
object by adding the participant’s session ID as the key and the participant’s object as the value.
setParticipants((p) => ({
...p,
[e.participant.session_id]: e.participant,
}));
Note: When someone joins as an owner or anyone is promoted to an admin, their status is tracked in DailyContainer
’s state. These values are passed as props to AdminPanel
, too.
If someone leaves the call, we delete their item from the object:
setParticipants((p) => {
const currentParticipants = { ...p };
delete currentParticipants[e.participant.session_id];
return currentParticipants;
});
Using participants
, we can render all call participants as a list. First we render the AdminPanel
component itself, including number of props:
// DailyContainer.js
{callFrame && (
<>
<AdminPanel
participants={participants}
localIsOwner={isOwner}
localIsAdmin={isAdmin}
makeAdmin={makeAdmin}
removeFromCall={removeFromCall}
/>
// … See source code
</>
)}
The AdminPanel
component is rendered for all participants, but only owners and admins will see buttons to update other participants:
A regular participant will instead see only the participant information:
The main functionality of AdminPanel
is actually defined in its child component, ParticipantListItem
:
export default function AdminPanel({
participants,
makeAdmin,
removeFromCall,
localIsOwner,
localIsAdmin,
}) {
return (
<div className='admin-panel'>
// … See source code
<ul>
{Object.values(participants).map((p, i) => {
const handleMakeAdmin = () => makeAdmin(p.session_id);
const handleRemoveFromCall = () => removeFromCall(p.session_id);
return (
<ParticipantListItem
count={i + 1} // for numbered list
key={p.session_id}
p={p}
localIsOwner={localIsOwner}
localIsAdmin={localIsAdmin}
makeAdmin={handleMakeAdmin}
removeFromCall={handleRemoveFromCall}
/>
);
})}
</ul>
</div>
);
}
Using the participants
prop, a ParticipantListItem
component is rendered for each participant in an unordered list (ul
).
ParticipantListItem
is passed most of the props AdminPanel
received and then actually uses them:
const ParticipantListItem = ({
p,
makeAdmin,
removeFromCall,
localIsOwner,
localIsAdmin,
count,
}) => (
<li>
<span>
{`${count}. `}
{p.permissions.canAdmin && <b>{p.owner ? 'Owner | ' : 'Admin | '}</b>}
<b>{p.local && '(You) '}</b>
{p.user_name}: {p.session_id}
</span>{' '}
{!p.local && !p.owner && (localIsAdmin || localIsOwner) && (
<span className='buttons'>
{(!p.permissions.canAdmin || localIsOwner) && (
<button className='red-button-secondary' onClick={removeFromCall}>
Remove from call
</button>
)}
{!p.permissions.canAdmin && (
<button onClick={makeAdmin}>Make admin</button>
)}
</span>
)}
</li>
);
Here, we:
- Add the participant’s number to their line item.
- Indicate if they’re an owner or admin and if it’s the local participant (you!).
- Render their username and session ID to confirm they’re unique participants.
- Conditionally render two buttons to either remove the participant or make them an admin.
Next, let’s focus on the button to make a participant an admin.
Converting non-admins to admins
As we saw above, the ParticipantListItem
component conditionally renders a button to let owners or admins make a regular participant an admin.
The “Make admin” button click handler uses the makeAdmin
prop passed down from DailyContainer
and includes the participant’s session_id
so we known which participant is being upgraded to an admin:
// AdminPanel.js
return (
// … See See source code for full code block
{Object.values(participants).map((p, i) => {
const handleMakeAdmin = () => makeAdmin(p.session_id);
const handleRemoveFromCall = () => removeFromCall(p.session_id);
return (
<ParticipantListItem
makeAdmin={handleMakeAdmin}
//… See source code
On click, the button invokes the makeAdmin()
function declared in DailyContainer
, which will upgrade the participant to an admin via Daily’s updateParticipant()
instance method:
const makeAdmin = useCallback(
(participantId) => {
// https://docs.daily.co/reference/daily-js/instance-methods/update-participant#permissions
callFrame.updateParticipant(participantId, {
updatePermissions: {
canAdmin: true,
},
});
},
[callFrame]
);
updateParticipant()
can be used for various participant updates and will act differently depending on the options passed in the second parameter. In this case, we want to make the participant an admin so we set the canAdmin
property to true
.
Using updateParticipant()
this way will in fact make the participant an admin, but it won’t update our UI. To do this, we need to listen for the ”participant-updated”
Daily event, which is emitted any time a participant is updated in any way.
Updating the participant list UI for new admins
To see how we update the UI for new admins, we need to backtrack for a second.
When the call frame was first created, a series of Daily event listeners were attached to it:
const addDailyEvents = (dailyCallFrame) => {
// https://docs.daily.co/reference/daily-js/instance-methods/on
dailyCallFrame
.on('joined-meeting', handleJoinedMeeting)
.on('participant-joined', handleParticipantJoined)
.on('participant-updated', handleParticipantUpdate)
// … See source code
The handleParticipantUpdate()
method is attached as the handler for ”participant-updated”
:
const handleParticipantUpdate = (e) => {
const { participant } = e;
const id = participant.session_id;
if (!prevParticipants.current[id]) return;
// Only update the participants list if the permission has changed.
if (
prevParticipants.current[id].permissions.canAdmin !==
participant.permissions.canAdmin
) {
setParticipants((p) => ({
...p,
[id]: participant,
}));
if (participant.local) {
setIsAdmin(participant.permissions.canAdmin);
}
}
};
For the purposes of this app, we are only looking for changes to the participant.permissions.canAdmin
status. If the previously saved value is different from the value included in the event’s payload for the participant, we update the participants
list.
Note: prevParticipants
is a ref that keeps a copy of the participants list so that we can compare the current list with possible updates.
Once the participant change is updated in state, the AdminPanel
component will automatically receive the updates through the participants
prop and update how the panel is rendered.
Ejecting a participant from the call
Ejecting (or removing) a participant from the call works almost identically from a code perspective.
In AdminPanel
when the ParticipantListItem
s are rendered, the removeFromCall()
prop is passed down, as well:
// AdminPanel
return (
// … See source code
{Object.values(participants).map((p, i) => {
const handleMakeAdmin = () => makeAdmin(p.session_id);
const handleRemoveFromCall = () => removeFromCall(p.session_id);
return (
<ParticipantListItem
removeFromCall={handleRemoveFromCall}
//… See source code
This is then used by the button
element in ParticipantListItem
to remove a participant.
{(!p.permissions.canAdmin || localIsOwner) && (
<button className='red-button-secondary' onClick={removeFromCall}>
Remove from call
</button>
)}
The actual action it triggers is defined in DailyContainer
, which will invoke the updateParticipant()
instance method:
const removeFromCall = useCallback(
(participantId) => {
// https://docs.daily.co/reference/daily-js/instance-methods/update-participant#setaudio-setvideo-and-eject
callFrame.updateParticipant(participantId, {
eject: true,
});
},
[callFrame]
);
The main difference here is the properties passed to updateParticipant()
; this time we use the eject
property set to true
.
Once they’ve been ejected, the ”participant-left”
event is emitted, another event that we’re already listening for:
const addDailyEvents = (dailyCallFrame) => {
dailyCallFrame
.on('participant-left', handleParticipantLeft)
// … See source code
When this event is received, the participants
object is updated in handleParticipantLeft()
and the UI updates to reflect the change:
const handleParticipantLeft = (e) => {
console.log(e.action);
setParticipants((p) => {
const currentParticipants = { ...p };
delete currentParticipants[e.participant.session_id];
return currentParticipants;
});
};
And, like that, we can promote participants to admins or remove them from the call!
Conclusion
In today’s post we looked at sample code that demonstrates how to use the updateParticipant()
instance method with the canAdmin
property to share admin privileges with other call participants. We also looked at ejecting call participants with the updateParticipant()
instance method.
To learn more about admin privileges and some of the topics we couldn’t cover in detail today, check out these related blog posts: