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:
parent
572f15aec0
commit
3feb3d91d9
2 changed files with 166 additions and 1 deletions
|
|
@ -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,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue