Ludicord
Guide 11 · Discord

Participants and voice

Model the live audience, channel members, voice state, and speaking events.

Ludicord 3.1.0Public source 20c4889

Discord exposes several different groups of people. Choose the source that matches the feature instead of treating every list as a guild roster.

#Choose the correct audience

NeedHookMeaning
Players in this Activity instanceuseParticipants()Live Activity participants from Discord
Summary for common UIuseActivityPresence()Participants, count, current participant, voice, speaking, and alone
Known people in the current channeluseChannelMembers()Channel-member data available from Discord context
Current signed-in guild memberuseCurrentGuildMember()User plus nickname, guild avatar, roles, and raw member payload
Full guild rosteruseDiscordGuildMembers()Paginated trusted-server resource requiring bot access

#Render Activity presence

components/audience.tsxtsx
1import { useActivityPresence } from "ludicord/activity";
2
3export function Audience() {
4 const presence = useActivityPresence();
5 return <p>{presence.alone ? "Invite a friend" : `${presence.participantCount} playing`}</p>;
6}

The presence summary is derived from the reactive Discord event store, so it updates when the participant list or voice events change.

#Use participant raw data

Every participant contains stable normalized identity fields plus raw. Do not assume optional display fields are always present.

tsxtsx
1const participants = useParticipants();
2
3return participants.map((participant) => (
4 <li key={participant.id}>
5 {participant.displayName ?? participant.username ?? participant.id}
6 </li>
7));

#Read voice state

Voice hooks require the rpc.voice.read scope.

ludicord.config.mjsjs
1discord: {
2 scopes: ["identify", "guilds", "rpc.voice.read"]
3}
tsxtsx
1const voiceStates = useVoiceState();
2const speakingUserIds = useSpeakingUsers();
3const isSpeaking = useIsSpeaking(user?.id);
4const voice = useParticipantVoiceState(user?.id);

LudicordVoiceState normalizes userId, mute, deaf, self-mute, and self-deaf values and keeps the full event object in raw.

#Subscribe to a raw event

Use useDiscordEvent() when a feature needs the exact event timing or a field that is not part of a normalized hook.

tsxtsx
1useDiscordEvent("SPEAKING_START", (payload) => {
2 analytics.mark("speaker-start", payload);
3}, { replayLatest: false });

Use useDiscordRawEvent("SPEAKING_START") when a component needs the latest payload as state. Raw event values are unknown; narrow their shape before reading fields.

#Handle missing scope or client support

useDiscordDiagnostics() reports capability problems such as a missing rpc.voice.read scope. Discord client versions and platforms can support different SDK capabilities, so voice UI needs an unavailable state.

Do not repeatedly ask for broader OAuth scopes just to remove a warning. Add a scope only when the Activity genuinely uses the capability and explain the consent to the player.

#Keep realtime concepts separate

Discord participant and voice events describe the current Activity context. Ludicord WebSocket rooms are your application transport. A player may be present in Discord before your WebSocket connects, or reconnect to your route while their Discord participant identity remains stable.

#Next step

Open Responsive Activities to adapt controls and layouts to the Discord surface hosting those participants.