feat: add discord status class
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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: null | Record<string, any>;
|
||||
kv: Record<string, string>;
|
||||
}
|
||||
|
||||
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?: Record<string, any>;
|
||||
display_name_styles?: Record<string, any>;
|
||||
primary_guild?: Record<string, any>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function resolveDiscordAsset(image: string | undefined): string {
|
||||
if (!image) return "";
|
||||
|
||||
if (image.startsWith("mp:external/")) {
|
||||
const httpsIndex = image.indexOf("/https/");
|
||||
if (httpsIndex !== -1) {
|
||||
const after = image.slice(httpsIndex + "/https/".length);
|
||||
return `https://${after}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(image) || image.startsWith("spotify:")) return "";
|
||||
|
||||
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(() => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fetchRichPresence();
|
||||
const interval = setInterval(fetchRichPresence, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
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 gameImage = gameActivity?.assets
|
||||
? resolveDiscordAsset(gameActivity.assets.small_image || gameActivity.assets.large_image)
|
||||
: "";
|
||||
|
||||
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">
|
||||
{gameActivity ? (
|
||||
<>
|
||||
playing: <em>{gameActivity.name.toLowerCase()}</em>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
currently: <em>{STATUS_LABELS[presence.discord_status]}</em>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{customStatusText && (
|
||||
<span className="status-bubble-inline">
|
||||
{customStatusText}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{gameImage && gameActivity && (
|
||||
<img
|
||||
src={gameImage}
|
||||
alt={gameActivity.name}
|
||||
className="game-icon"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user