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:
@@ -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>
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function MediaRequestForm() {
|
||||
const [albumPlaybackLoadingId, setAlbumPlaybackLoadingId] = useState(null);
|
||||
const [shuffleAlbums, setShuffleAlbums] = useState({});
|
||||
const [audioProgress, setAudioProgress] = useState({ current: 0, duration: 0 });
|
||||
const [diskSpace, setDiskSpace] = useState(null);
|
||||
|
||||
const debounceTimeout = useRef(null);
|
||||
const autoCompleteRef = useRef(null);
|
||||
@@ -64,6 +65,22 @@ export default function MediaRequestForm() {
|
||||
lastUrlRef.current = window.location.pathname + window.location.search + window.location.hash;
|
||||
}, []);
|
||||
|
||||
// Fetch disk space on mount
|
||||
useEffect(() => {
|
||||
const fetchDiskSpace = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/disk-space');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setDiskSpace(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch disk space:', err);
|
||||
}
|
||||
};
|
||||
fetchDiskSpace();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
@@ -999,6 +1016,32 @@ export default function MediaRequestForm() {
|
||||
}
|
||||
`}</style>
|
||||
<BreadcrumbNav currentPage="request" />
|
||||
|
||||
{/* Disk Space Indicator - always visible */}
|
||||
{diskSpace && (
|
||||
<div className="mb-4 flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2 1 3 3 3h10c2 0 3-1 3-3V7c0-2-1-3-3-3H7c-2 0-3 1-3 3z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V5c0-1 .5-2 2-2h4c1.5 0 2 1 2 2v2" />
|
||||
</svg>
|
||||
<span>
|
||||
<span className={`font-medium ${
|
||||
diskSpace.usedPercent > 90 ? 'text-red-500' :
|
||||
diskSpace.usedPercent > 75 ? 'text-yellow-500' : 'text-green-600 dark:text-green-400'
|
||||
}`}>{diskSpace.availableFormatted}</span> available
|
||||
</span>
|
||||
<div className="w-20 h-1.5 bg-neutral-200 dark:bg-neutral-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${diskSpace.usedPercent > 90 ? 'bg-red-500' :
|
||||
diskSpace.usedPercent > 75 ? 'bg-yellow-500' : 'bg-green-500'
|
||||
}`}
|
||||
style={{ width: `${diskSpace.usedPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-neutral-400">({diskSpace.usedPercent}% used)</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight mb-2">New Request</h2>
|
||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm mb-6">Search for an artist to browse and select tracks for download.</p>
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -1283,8 +1326,8 @@ export default function MediaRequestForm() {
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSubmitRequest} color="primary" className="mt-4" disabled={isSubmitting}>
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button onClick={handleSubmitRequest} color="primary" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="animate-spin h-4 w-4 border-2 border-t-2 border-gray-200 border-t-primary rounded-full"></span>
|
||||
|
||||
Reference in New Issue
Block a user