Files
neru.rip/src/app/components/discordstatus.tsx
T

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>
);
}