Add a way to display participants in an activity

Define interfaces for participant and voice state, fetch and subscribe to participant updates, and display participants in a new component within the Activity page.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 4414273a-a40f-4598-8758-a875f7eccc1c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/139vJay
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-13 04:51:25 +00:00
parent 572f15aec0
commit 3feb3d91d9
2 changed files with 166 additions and 1 deletions

View file

@ -13,12 +13,38 @@ interface DiscordUser {
primary_arm: string | null;
}
interface Participant {
id: string;
username: string;
discriminator: string;
avatar: string | null;
bot: boolean;
flags: number;
global_name: string | null;
}
interface VoiceState {
mute: boolean;
deaf: boolean;
self_mute: boolean;
self_deaf: boolean;
suppress: boolean;
}
interface ParticipantWithVoice extends Participant {
voice_state?: VoiceState;
speaking?: boolean;
}
interface DiscordActivityContextType {
isActivity: boolean;
isLoading: boolean;
user: DiscordUser | null;
error: string | null;
discordSdk: any | null;
participants: ParticipantWithVoice[];
channelId: string | null;
guildId: string | null;
openExternalLink: (url: string) => Promise<void>;
}
@ -28,6 +54,9 @@ const DiscordActivityContext = createContext<DiscordActivityContextType>({
user: null,
error: null,
discordSdk: null,
participants: [],
channelId: null,
guildId: null,
openExternalLink: async () => {},
});
@ -54,6 +83,10 @@ export const DiscordActivityProvider: React.FC<
const [error, setError] = useState<string | null>(null);
const [discordSdk, setDiscordSdk] = useState<any>(null);
const [auth, setAuth] = useState<any>(null);
const [participants, setParticipants] = useState<ParticipantWithVoice[]>([]);
const [channelId, setChannelId] = useState<string | null>(null);
const [guildId, setGuildId] = useState<string | null>(null);
const [speakingUsers, setSpeakingUsers] = useState<Set<string>>(new Set());
useEffect(() => {
const initializeActivity = async () => {
@ -208,6 +241,81 @@ export const DiscordActivityProvider: React.FC<
setUser(userData);
setError(null);
console.log("[Discord Activity] User authenticated successfully");
// Store channel and guild info
setChannelId(sdk.channelId);
setGuildId(sdk.guildId);
// Fetch initial participants
try {
console.log("[Discord Activity] Fetching participants...");
const participantsResult = await sdk.commands.getInstanceConnectedParticipants();
if (participantsResult?.participants) {
const participantList = participantsResult.participants.map((p: any) => ({
id: p.id,
username: p.username,
discriminator: p.discriminator || "0",
avatar: p.avatar,
bot: p.bot || false,
flags: p.flags || 0,
global_name: p.global_name,
}));
setParticipants(participantList);
console.log("[Discord Activity] Initial participants:", participantList.length);
}
// Subscribe to participant updates
sdk.subscribe("ACTIVITY_INSTANCE_PARTICIPANTS_UPDATE", (data: any) => {
console.log("[Discord Activity] Participants updated:", data);
if (data?.participants) {
const updatedList = data.participants.map((p: any) => ({
id: p.id,
username: p.username,
discriminator: p.discriminator || "0",
avatar: p.avatar,
bot: p.bot || false,
flags: p.flags || 0,
global_name: p.global_name,
}));
setParticipants(updatedList);
}
});
// Subscribe to speaking updates if in voice channel
if (sdk.channelId) {
try {
sdk.subscribe("SPEAKING_START", (data: any) => {
console.log("[Discord Activity] Speaking start:", data);
if (data?.user_id) {
setSpeakingUsers(prev => new Set(prev).add(data.user_id));
setParticipants(prev => prev.map(p =>
p.id === data.user_id ? { ...p, speaking: true } : p
));
}
}, { channel_id: sdk.channelId });
sdk.subscribe("SPEAKING_STOP", (data: any) => {
console.log("[Discord Activity] Speaking stop:", data);
if (data?.user_id) {
setSpeakingUsers(prev => {
const next = new Set(prev);
next.delete(data.user_id);
return next;
});
setParticipants(prev => prev.map(p =>
p.id === data.user_id ? { ...p, speaking: false } : p
));
}
}, { channel_id: sdk.channelId });
console.log("[Discord Activity] Voice subscriptions active");
} catch (voiceErr) {
console.log("[Discord Activity] Voice subscription not available:", voiceErr);
}
}
} catch (participantErr) {
console.log("[Discord Activity] Could not fetch participants:", participantErr);
}
} catch (err: any) {
console.error("Discord Activity initialization error:", err);
console.error("Error details:", {
@ -255,6 +363,9 @@ export const DiscordActivityProvider: React.FC<
user,
error,
discordSdk,
participants,
channelId,
guildId,
openExternalLink,
}}
>

View file

@ -679,8 +679,59 @@ function BadgesTab({ userId, openExternalLink }: { userId?: string; openExternal
);
}
function ParticipantsBar({ participants, currentUserId }: { participants: any[]; currentUserId?: string }) {
const otherParticipants = participants.filter(p => p.id !== currentUserId);
if (otherParticipants.length === 0) return null;
return (
<div className="flex items-center gap-2 px-4 py-2 bg-[#2b2d31] border-b border-[#1e1f22]">
<Users className="w-4 h-4 text-[#949ba4]" />
<span className="text-xs text-[#949ba4]">{otherParticipants.length} here</span>
<div className="flex -space-x-2 ml-2">
{otherParticipants.slice(0, 8).map((p) => (
<motion.div
key={p.id}
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="relative"
>
{p.avatar ? (
<img
src={`https://cdn.discordapp.com/avatars/${p.id}/${p.avatar}.png?size=32`}
alt={p.global_name || p.username}
className={`w-7 h-7 rounded-full border-2 border-[#2b2d31] ${p.speaking ? 'ring-2 ring-green-400' : ''}`}
title={p.global_name || p.username}
/>
) : (
<div
className={`w-7 h-7 rounded-full bg-[#5865f2] flex items-center justify-center text-white text-xs font-bold border-2 border-[#2b2d31] ${p.speaking ? 'ring-2 ring-green-400' : ''}`}
title={p.global_name || p.username}
>
{(p.global_name || p.username)?.[0]?.toUpperCase() || "?"}
</div>
)}
{p.speaking && (
<motion.div
className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-400 rounded-full border border-[#2b2d31]"
animate={{ scale: [1, 1.2, 1] }}
transition={{ repeat: Infinity, duration: 0.5 }}
/>
)}
</motion.div>
))}
{otherParticipants.length > 8 && (
<div className="w-7 h-7 rounded-full bg-[#4e5058] flex items-center justify-center text-white text-xs font-bold border-2 border-[#2b2d31]">
+{otherParticipants.length - 8}
</div>
)}
</div>
</div>
);
}
export default function Activity() {
const { isActivity, isLoading, user, error, openExternalLink } = useDiscordActivity();
const { isActivity, isLoading, user, error, openExternalLink, participants } = useDiscordActivity();
const [activeTab, setActiveTab] = useState("feed");
const [xpGain, setXpGain] = useState<number | null>(null);
const [showConfetti, setShowConfetti] = useState(false);
@ -818,6 +869,9 @@ export default function Activity() {
</div>
</motion.div>
{/* Participants Bar */}
<ParticipantsBar participants={participants} currentUserId={user?.id} />
{/* Tab Navigation */}
<div className="flex bg-[#2b2d31] border-b border-[#1e1f22] px-2 overflow-x-auto scrollbar-hide flex-shrink-0">
{tabs.map((tab) => {