aethex-forge/client/contexts/DiscordActivityContext.tsx
sirpiglr 3feb3d91d9 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
2025-12-13 04:51:25 +00:00

377 lines
12 KiB
TypeScript

import React, { createContext, useContext, useEffect, useState } from "react";
const API_BASE = import.meta.env.VITE_API_BASE || "";
interface DiscordUser {
id: string;
discord_id: string;
full_name: string | null;
username: string | null;
avatar_url: string | null;
bio: string | null;
user_type: string | null;
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>;
}
const DiscordActivityContext = createContext<DiscordActivityContextType>({
isActivity: false,
isLoading: false,
user: null,
error: null,
discordSdk: null,
participants: [],
channelId: null,
guildId: null,
openExternalLink: async () => {},
});
export const useDiscordActivity = () => {
const context = useContext(DiscordActivityContext);
if (!context) {
throw new Error(
"useDiscordActivity must be used within DiscordActivityProvider",
);
}
return context;
};
interface DiscordActivityProviderProps {
children: React.ReactNode;
}
export const DiscordActivityProvider: React.FC<
DiscordActivityProviderProps
> = ({ children }) => {
const [isActivity, setIsActivity] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [user, setUser] = useState<DiscordUser | null>(null);
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 () => {
// Check if we're running inside a Discord Activity
// Discord passes frame_id as a query parameter when launching an Activity
if (typeof window === "undefined") {
return; // Skip on server-side
}
const searchParams = new URLSearchParams(window.location.search);
const frameId = searchParams.get("frame_id");
const isInDiscordActivity = frameId !== null;
console.log("[Discord Activity] Checking for Discord context...", {
frameId,
isInDiscordActivity,
});
// If we're NOT in Discord Activity, exit early - don't load Discord SDK
if (!isInDiscordActivity) {
console.log(
"[Discord Activity] Not in Discord Activity - skipping SDK load",
);
setIsActivity(false);
setIsLoading(false);
return;
}
// Only initialize Discord SDK if we're actually in a Discord Activity
if (isInDiscordActivity) {
try {
setIsActivity(true);
setIsLoading(true);
// Import the Discord SDK dynamically
const { DiscordSDK } = await import("@discord/embedded-app-sdk");
const clientId =
import.meta.env.VITE_DISCORD_CLIENT_ID || "578971245454950421";
console.log(
"[Discord Activity] Creating SDK with clientId:",
clientId,
);
const sdk = new DiscordSDK(clientId);
setDiscordSdk(sdk);
// Wait for SDK to be ready
console.log("[Discord Activity] Waiting for SDK to be ready...");
await sdk.ready();
console.log("[Discord Activity] SDK is ready");
// Authorize the user to get an access token
console.log("[Discord Activity] Authorizing user...");
const { code } = await sdk.commands.authorize({
client_id: clientId,
response_type: "code",
state: "",
scope: ["identify", "guilds"],
prompt: "none",
});
console.log(
"[Discord Activity] Got authorization code, exchanging for token...",
);
// Exchange code for access token via our backend
const tokenResponse = await fetch(`${API_BASE}/api/discord/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ code }),
});
if (!tokenResponse.ok) {
const errorData = await tokenResponse.json();
const errMsg = errorData.error || "Failed to exchange code";
console.error(
"[Discord Activity] Token exchange failed:",
errMsg,
);
setError(errMsg);
setIsLoading(false);
return;
}
const tokenData = await tokenResponse.json();
const access_token = tokenData.access_token;
console.log(
"[Discord Activity] Got access token, authenticating with SDK...",
);
// Authenticate with SDK using the access token
const authResult = await sdk.commands.authenticate({
access_token,
});
if (!authResult) {
console.error("[Discord Activity] SDK authentication failed");
setError("SDK authentication failed");
setIsLoading(false);
return;
}
console.log(
"[Discord Activity] Authenticated with SDK, fetching user profile...",
);
setAuth(authResult);
// Get user info using the access token
const userResponse = await fetch(
"https://discord.com/api/v10/users/@me",
{
headers: {
Authorization: `Bearer ${access_token}`,
},
},
);
if (!userResponse.ok) {
console.error("[Discord Activity] Failed to fetch user profile");
setError("Failed to fetch user profile");
setIsLoading(false);
return;
}
const discordUserData = await userResponse.json();
console.log(
"[Discord Activity] User profile fetched:",
discordUserData.username,
);
// Store the user data
const userData: DiscordUser = {
id: discordUserData.id,
discord_id: discordUserData.id,
full_name:
discordUserData.global_name || discordUserData.username,
username: discordUserData.username,
avatar_url: discordUserData.avatar
? `https://cdn.discordapp.com/avatars/${discordUserData.id}/${discordUserData.avatar}.png`
: null,
bio: null,
user_type: "community_member",
primary_arm: "labs",
};
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:", {
message: err?.message,
code: err?.code,
stack: err?.stack,
});
setError(
`${err?.message || "Failed to initialize Discord Activity"}. Check browser console for details.`,
);
} finally {
setIsLoading(false);
}
} else {
// Not in a Discord iframe
console.log(
"[Discord Activity] Not in Discord Activity context (no frame_id)",
);
setIsActivity(false);
setIsLoading(false);
}
};
initializeActivity();
}, []);
const openExternalLink = async (url: string) => {
if (discordSdk) {
try {
await discordSdk.commands.openExternalLink({ url });
} catch (err) {
console.error("[Discord Activity] Failed to open external link:", err);
window.open(url, "_blank");
}
} else {
window.open(url, "_blank");
}
};
return (
<DiscordActivityContext.Provider
value={{
isActivity,
isLoading,
user,
error,
discordSdk,
participants,
channelId,
guildId,
openExternalLink,
}}
>
{children}
</DiscordActivityContext.Provider>
);
};
export default DiscordActivityContext;