Add support for Lottie stickers and enhance disk space API endpoint

- Introduced Lottie sticker component with placeholder handling in DiscordLogs.
- Expanded Discord message types in DiscordLogs component.
- Implemented disk space fetching in MediaRequestForm with visual indicator.
- Enhanced API for fetching Discord messages to include Lottie data for stickers.
- Added disk space API endpoint with authentication and authorization checks.
This commit is contained in:
2025-12-19 10:26:22 -05:00
parent 2cc07b6cc2
commit 95a59e9395
7 changed files with 509 additions and 55 deletions

View File

@@ -6,9 +6,36 @@ import { authFetch } from '@/utils/authFetch';
// Discord Message Type Constants
// https://discord.com/developers/docs/resources/channel#message-object-message-types
const MESSAGE_TYPE_DEFAULT = 0;
const MESSAGE_TYPE_RECIPIENT_ADD = 1;
const MESSAGE_TYPE_RECIPIENT_REMOVE = 2;
const MESSAGE_TYPE_CALL = 3;
const MESSAGE_TYPE_CHANNEL_NAME_CHANGE = 4;
const MESSAGE_TYPE_CHANNEL_ICON_CHANGE = 5;
const MESSAGE_TYPE_CHANNEL_PINNED_MESSAGE = 6;
const MESSAGE_TYPE_USER_JOIN = 7;
const MESSAGE_TYPE_GUILD_BOOST = 8;
const MESSAGE_TYPE_GUILD_BOOST_TIER_1 = 9;
const MESSAGE_TYPE_GUILD_BOOST_TIER_2 = 10;
const MESSAGE_TYPE_GUILD_BOOST_TIER_3 = 11;
const MESSAGE_TYPE_CHANNEL_FOLLOW_ADD = 12;
const MESSAGE_TYPE_GUILD_DISCOVERY_DISQUALIFIED = 14;
const MESSAGE_TYPE_GUILD_DISCOVERY_REQUALIFIED = 15;
const MESSAGE_TYPE_GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16;
const MESSAGE_TYPE_GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17;
const MESSAGE_TYPE_THREAD_CREATED = 18;
const MESSAGE_TYPE_REPLY = 19;
const MESSAGE_TYPE_CHAT_INPUT_COMMAND = 20;
const MESSAGE_TYPE_THREAD_STARTER_MESSAGE = 21;
const MESSAGE_TYPE_GUILD_INVITE_REMINDER = 22;
const MESSAGE_TYPE_CONTEXT_MENU_COMMAND = 23;
const MESSAGE_TYPE_AUTO_MODERATION_ACTION = 24;
const MESSAGE_TYPE_ROLE_SUBSCRIPTION_PURCHASE = 25;
const MESSAGE_TYPE_INTERACTION_PREMIUM_UPSELL = 26;
const MESSAGE_TYPE_STAGE_START = 27;
const MESSAGE_TYPE_STAGE_END = 28;
const MESSAGE_TYPE_STAGE_SPEAKER = 29;
const MESSAGE_TYPE_STAGE_TOPIC = 31;
const MESSAGE_TYPE_GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32;
const MESSAGE_TYPE_POLL_RESULT = 46;
// Image modal context for child components to trigger modal
@@ -907,6 +934,60 @@ const LinkPreview = memo(function LinkPreview({ url, cachedPreview, onPreviewLoa
);
});
// ============================================================================
// Lottie Sticker Component
// ============================================================================
const LottieSticker = memo(function LottieSticker({ data, name }) {
const containerRef = useRef(null);
const animationRef = useRef(null);
useEffect(() => {
if (!containerRef.current || !data) return;
// Dynamically import lottie-web only when needed
import('lottie-web').then((lottie) => {
// Clean up any existing animation
if (animationRef.current) {
animationRef.current.destroy();
}
animationRef.current = lottie.default.loadAnimation({
container: containerRef.current,
renderer: 'svg',
loop: true,
autoplay: true,
animationData: data,
});
}).catch((err) => {
console.error('Failed to load lottie-web:', err);
});
return () => {
if (animationRef.current) {
animationRef.current.destroy();
animationRef.current = null;
}
};
}, [data]);
if (!data) {
return (
<div className="discord-sticker-placeholder">
<span>{name}</span>
</div>
);
}
return (
<div
ref={containerRef}
className="discord-sticker-lottie"
aria-label={name}
/>
);
});
// ============================================================================
// Attachment Component
// ============================================================================
@@ -1205,19 +1286,109 @@ const DiscordMessage = memo(function DiscordMessage({
);
}
// Default system message rendering
// Generate system message text based on type
const getSystemMessageText = () => {
const username = displayAuthor?.displayName || displayAuthor?.username || 'Someone';
switch (type) {
case MESSAGE_TYPE_USER_JOIN:
// Discord has multiple join messages, pick randomly or use content if provided
const joinMessages = [
'joined the server.',
'just joined the server - glhf!',
'just joined. Everyone, look busy!',
'just arrived.',
'just landed.',
'just slid into the server.',
'hopped into the server.',
'appeared.',
];
return content || joinMessages[0];
case MESSAGE_TYPE_GUILD_BOOST:
return content || 'just boosted the server!';
case MESSAGE_TYPE_GUILD_BOOST_TIER_1:
return content || 'just boosted the server! Server has achieved Level 1!';
case MESSAGE_TYPE_GUILD_BOOST_TIER_2:
return content || 'just boosted the server! Server has achieved Level 2!';
case MESSAGE_TYPE_GUILD_BOOST_TIER_3:
return content || 'just boosted the server! Server has achieved Level 3!';
case MESSAGE_TYPE_CHANNEL_PINNED_MESSAGE:
return content || 'pinned a message to this channel.';
case MESSAGE_TYPE_THREAD_CREATED:
return content || 'started a thread.';
case MESSAGE_TYPE_RECIPIENT_ADD:
return content || 'added someone to the group.';
case MESSAGE_TYPE_RECIPIENT_REMOVE:
return content || 'removed someone from the group.';
case MESSAGE_TYPE_CALL:
return content || 'started a call.';
case MESSAGE_TYPE_CHANNEL_NAME_CHANGE:
return content || 'changed the channel name.';
case MESSAGE_TYPE_CHANNEL_ICON_CHANGE:
return content || 'changed the channel icon.';
case MESSAGE_TYPE_CHANNEL_FOLLOW_ADD:
return content || 'added a channel follow.';
case MESSAGE_TYPE_STAGE_START:
return content || 'started a Stage.';
case MESSAGE_TYPE_STAGE_END:
return content || 'ended the Stage.';
case MESSAGE_TYPE_STAGE_SPEAKER:
return content || 'is now a speaker.';
case MESSAGE_TYPE_STAGE_TOPIC:
return content || 'changed the Stage topic.';
default:
return content || '';
}
};
const systemText = getSystemMessageText();
// Choose icon based on message type
const getSystemIcon = () => {
switch (type) {
case MESSAGE_TYPE_USER_JOIN:
return (
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 3l4.5 4.5-1.41 1.41L12 7.83l-3.09 3.08L7.5 9.5 12 5z" />
</svg>
);
case MESSAGE_TYPE_GUILD_BOOST:
case MESSAGE_TYPE_GUILD_BOOST_TIER_1:
case MESSAGE_TYPE_GUILD_BOOST_TIER_2:
case MESSAGE_TYPE_GUILD_BOOST_TIER_3:
return (
<svg viewBox="0 0 24 24" fill="#ff73fa" width="16" height="16">
<path d="M12.001 2.5c.512 0 .994.228 1.32.624l.088.114 8.5 12c.25.353.32.805.187 1.214l-.055.134-.061.114a1.5 1.5 0 01-1.191.79l-.119.01H3.331a1.5 1.5 0 01-1.398-2.048l.061-.114 8.5-12a1.5 1.5 0 011.408-.838l.099.01.1.014zM12 7.268L5.416 16.5h13.169L12 7.268z" />
</svg>
);
case MESSAGE_TYPE_CHANNEL_PINNED_MESSAGE:
return (
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z" />
</svg>
);
default:
return (
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M18.5 12c0-1.77-.77-3.37-2-4.47V4.5h-2v1.98A5.96 5.96 0 0 0 12 6c-.97 0-1.89.23-2.71.63L7.11 4.45 5.64 5.92l2.18 2.18A5.96 5.96 0 0 0 6 12c0 3.31 2.69 6 6 6s6-2.69 6-6zm-6 4c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z" />
</svg>
);
}
};
return (
<div className="discord-message discord-system-message">
<div className="discord-system-icon">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M18.5 12c0-1.77-.77-3.37-2-4.47V4.5h-2v1.98A5.96 5.96 0 0 0 12 6c-.97 0-1.89.23-2.71.63L7.11 4.45 5.64 5.92l2.18 2.18A5.96 5.96 0 0 0 6 12c0 3.31 2.69 6 6 6s6-2.69 6-6zm-6 4c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z" />
</svg>
{getSystemIcon()}
</div>
<span className="discord-system-content">
<span className="discord-username" style={displayAuthor?.color ? { color: displayAuthor.color } : undefined}>
{displayAuthor?.displayName || displayAuthor?.username || 'Unknown'}
</span>
{' '}
<span dangerouslySetInnerHTML={{ __html: parsedContent }} />
{systemText ? (
<span dangerouslySetInnerHTML={{ __html: parseDiscordMarkdown(systemText, { usersMap, channelMap, emojiCache, rolesMap }) }} />
) : (
<span className="discord-system-unknown">(system message type {type})</span>
)}
</span>
<span className="discord-timestamp">{formatTimestamp(displayTimestamp)}</span>
</div>
@@ -1352,18 +1523,21 @@ const DiscordMessage = memo(function DiscordMessage({
<div className="discord-stickers">
{stickers.map((sticker) => (
<div key={sticker.id} className="discord-sticker" title={sticker.name}>
{sticker.formatType === 3 ? (
// Lottie stickers - show placeholder or name
<div className="discord-sticker-lottie">
<span>{sticker.name}</span>
</div>
) : (
{sticker.formatType === 3 && sticker.lottieData ? (
// Lottie stickers with cached data
<LottieSticker data={sticker.lottieData} name={sticker.name} />
) : sticker.url ? (
<img
src={sticker.url}
alt={sticker.name}
className="discord-sticker-image"
loading="lazy"
/>
) : (
// Fallback for missing sticker data
<div className="discord-sticker-placeholder">
<span>{sticker.name}</span>
</div>
)}
</div>
))}
@@ -2811,6 +2985,18 @@ export default function DiscordLogs() {
// Check if current channel is dds-archive
const isArchiveChannel = selectedChannel?.name === 'dds-archive';
// Check if Havoc bot is in the channel's member list
const HAVOC_BOT_ID = '1219636064608583770';
const isHavocInChannel = useMemo(() => {
if (!members?.groups) return true; // Assume present if members not loaded
for (const group of members.groups) {
if (group.members?.some(m => m.id === HAVOC_BOT_ID)) {
return true;
}
}
return false;
}, [members]);
// Group messages by author and time window (5 minutes)
// Messages are in ASC order (oldest first, newest at bottom), so we group accordingly
// For archive channel, parse messages to get real author/timestamp for grouping
@@ -3184,6 +3370,15 @@ export default function DiscordLogs() {
</div>
);
})}
{/* Disclaimer when Havoc bot is not in channel */}
{!isHavocInChannel && !loadingMembers && (
<div className="discord-havoc-disclaimer">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
</svg>
<span>Havoc no longer has access to this channel. New activity may not be visible.</span>
</div>
)}
</div>
)}
</div>