263 lines
7.3 KiB
TypeScript
263 lines
7.3 KiB
TypeScript
import './discordstatus.css';
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
interface LanyardResponse {
|
|
success: boolean;
|
|
data: LanyardData;
|
|
}
|
|
|
|
interface LanyardData {
|
|
discord_status: 'online' | 'idle' | 'dnd' | 'offline';
|
|
activities: DiscordActivity[];
|
|
discord_user: DiscordUser;
|
|
active_on_discord_web: boolean;
|
|
active_on_discord_desktop: boolean;
|
|
active_on_discord_mobile: boolean;
|
|
active_on_discord_embedded: boolean;
|
|
active_on_discord_vr: boolean;
|
|
listening_to_spotify: boolean;
|
|
spotify: SpotifyData | null;
|
|
kv: Record<string, string>;
|
|
}
|
|
|
|
interface SpotifyData {
|
|
album: string;
|
|
album_art_url: string;
|
|
artist: string;
|
|
song: string;
|
|
track_id: string;
|
|
timestamps: {
|
|
start: number;
|
|
end: number;
|
|
};
|
|
}
|
|
|
|
interface DiscordUser {
|
|
id: string;
|
|
username: string;
|
|
discriminator: string;
|
|
global_name: string;
|
|
display_name: string;
|
|
avatar: string;
|
|
bot: boolean;
|
|
public_flags: number;
|
|
avatar_decoration_data: null | any;
|
|
collectibles?: {
|
|
nameplate?: {
|
|
asset: string;
|
|
expires_at: string | null;
|
|
label: string;
|
|
palette: string;
|
|
sku_id: string;
|
|
};
|
|
};
|
|
display_name_styles?: {
|
|
colors: any[];
|
|
effect_id: number;
|
|
font_id: number;
|
|
};
|
|
primary_guild?: {
|
|
badge: null | any;
|
|
identity_enabled: boolean;
|
|
identity_guild_id: null | string;
|
|
tag: null | string;
|
|
};
|
|
}
|
|
|
|
interface DiscordActivity {
|
|
id: string;
|
|
name: string;
|
|
type: number;
|
|
session_id?: string;
|
|
created_at: number;
|
|
state?: string;
|
|
details?: string;
|
|
timestamps?: {
|
|
start?: number;
|
|
end?: number;
|
|
};
|
|
assets?: {
|
|
large_image?: string;
|
|
large_text?: string;
|
|
small_image?: string;
|
|
small_text?: string;
|
|
};
|
|
emoji?: {
|
|
name: string;
|
|
id?: string;
|
|
animated?: boolean;
|
|
};
|
|
application_id?: string;
|
|
flags?: number;
|
|
platform?: string;
|
|
sync_id?: string;
|
|
content_classification?: {
|
|
data: null | any;
|
|
loaded: boolean;
|
|
};
|
|
}
|
|
|
|
function resolveDiscordAsset(applicationId: string | undefined, image: string | undefined): string {
|
|
if (!image) return "";
|
|
|
|
if (image.startsWith("mp:external/")) {
|
|
const httpsIndex = image.indexOf("/https/");
|
|
if (httpsIndex !== -1) {
|
|
return `https://${image.slice(httpsIndex + "/https/".length)}`;
|
|
}
|
|
}
|
|
|
|
if (image.startsWith("spotify:"))
|
|
return "";
|
|
|
|
|
|
if (applicationId && image)
|
|
return `https://cdn.discordapp.com/app-assets/${applicationId}/${image}.png`;
|
|
|
|
return image;
|
|
}
|
|
|
|
export interface DiscordStatusParams {
|
|
userId: string
|
|
}
|
|
|
|
const STATUS_LABELS: Record<LanyardData['discord_status'], string> = {
|
|
online: 'online',
|
|
idle: 'away',
|
|
dnd: 'busy',
|
|
offline: 'offline'
|
|
};
|
|
|
|
export function DiscordStatus({ userId }: DiscordStatusParams) {
|
|
const [presence, setPresence] = useState<LanyardData | null>(null);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
|
|
useEffect(() => {
|
|
let interval: NodeJS.Timeout | null = null;
|
|
|
|
async function fetchRichPresence() {
|
|
try {
|
|
const response = await fetch(`https://api.lanyard.rest/v1/users/${userId}`);
|
|
const json: LanyardResponse = await response.json();
|
|
if (json.success)
|
|
setPresence(json.data);
|
|
} catch (error) {
|
|
console.error("Failed to fetch Lanyard presence:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
const startPolling = () => {
|
|
if (!interval) {
|
|
fetchRichPresence();
|
|
interval = setInterval(fetchRichPresence, 30000);
|
|
}
|
|
};
|
|
|
|
const stopPolling = () => {
|
|
if (interval) {
|
|
clearInterval(interval);
|
|
interval = null;
|
|
}
|
|
};
|
|
|
|
const handleVisibilityChange = () => {
|
|
if (document.visibilityState === 'visible')
|
|
startPolling();
|
|
else
|
|
stopPolling();
|
|
|
|
};
|
|
|
|
if (document.visibilityState === 'visible')
|
|
startPolling();
|
|
else
|
|
setLoading(false);
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
return () => {
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
stopPolling();
|
|
};
|
|
}, [userId]);
|
|
|
|
if (loading)
|
|
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>loading status...</p>;
|
|
|
|
if (!presence)
|
|
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>offline</p>;
|
|
|
|
const customActivity = presence.activities.find(act => act.id === "custom");
|
|
const customStatusText = customActivity
|
|
? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim()
|
|
: null;
|
|
|
|
const gameActivity = presence.activities
|
|
.filter(act => act.type === 0)
|
|
.sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as DiscordActivity | undefined;
|
|
|
|
const isListeningToSpotify = presence.listening_to_spotify && presence.spotify;
|
|
|
|
let primaryActivity = null;
|
|
let activityText = "";
|
|
let activityImage = "";
|
|
|
|
if (gameActivity) {
|
|
primaryActivity = gameActivity;
|
|
activityText = `playing: ${gameActivity.name.toLowerCase()}`;
|
|
|
|
if (gameActivity.details)
|
|
activityText += ` • ${gameActivity.details}`;
|
|
|
|
if (gameActivity.assets) {
|
|
const targetImage = gameActivity.assets.small_image || gameActivity.assets.large_image;
|
|
activityImage = resolveDiscordAsset(gameActivity.application_id, targetImage);
|
|
}
|
|
}
|
|
else if (isListeningToSpotify && presence.spotify) {
|
|
primaryActivity = { name: "Spotify" } as DiscordActivity;
|
|
activityText = `listening to: ${presence.spotify.song} • ${presence.spotify.artist}`;
|
|
activityImage = presence.spotify.album_art_url || "";
|
|
}
|
|
|
|
return (
|
|
<div className="discord-status-compact">
|
|
<div className="avatar-container">
|
|
<img
|
|
src={`https://api.lanyard.rest/${userId}.png`}
|
|
alt="Discord avatar"
|
|
className="discord-avatar"
|
|
/>
|
|
<span className={`status-dot ${presence.discord_status}`} />
|
|
</div>
|
|
|
|
<div className="status-details">
|
|
<span className="status-text">
|
|
{primaryActivity ? (
|
|
<>{activityText}</>
|
|
) : (
|
|
<>currently: <em>{STATUS_LABELS[presence.discord_status]}</em></>
|
|
)}
|
|
</span>
|
|
|
|
{customStatusText && (
|
|
<span className="status-bubble-inline">
|
|
{customStatusText}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{activityImage && primaryActivity && (
|
|
<img
|
|
src={activityImage}
|
|
alt={primaryActivity.name}
|
|
className="game-icon"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|