Participants and voice
Model the live audience, channel members, voice state, and speaking events.
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
#Render Activity presence
1import { useActivityPresence } from "ludicord/activity";23export 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.
1const participants = useParticipants();23return 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.
1discord: {2 scopes: ["identify", "guilds", "rpc.voice.read"]3}
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.
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.