diff --git a/src/assets/styles/DiscordLogs.css b/src/assets/styles/DiscordLogs.css index 035174c..1e75a91 100644 --- a/src/assets/styles/DiscordLogs.css +++ b/src/assets/styles/DiscordLogs.css @@ -916,13 +916,26 @@ display: flex; align-items: center; justify-content: center; +} + +.discord-sticker-lottie svg { + width: 100%; + height: 100%; +} + +.discord-sticker-placeholder { + width: 160px; + height: 160px; + display: flex; + align-items: center; + justify-content: center; background: rgba(88, 101, 242, 0.1); border-radius: 8px; font-size: 0.875rem; color: var(--text-muted, #72767d); } -[data-theme="dark"] .discord-sticker-lottie { +[data-theme="dark"] .discord-sticker-placeholder { background: rgba(88, 101, 242, 0.15); color: #b9bbbe; } @@ -1762,6 +1775,35 @@ a.discord-embed-title:hover { font-size: 0.875rem; } +/* Havoc bot disclaimer */ +.discord-havoc-disclaimer { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + margin: 1rem 1rem 0.5rem; + background: rgba(250, 168, 26, 0.1); + border: 1px solid rgba(250, 168, 26, 0.3); + border-radius: 6px; + color: #b5851b; + font-size: 0.8125rem; +} + +.discord-havoc-disclaimer svg { + flex-shrink: 0; + color: #faa81a; +} + +[data-theme="dark"] .discord-havoc-disclaimer { + background: rgba(250, 168, 26, 0.15); + border-color: rgba(250, 168, 26, 0.4); + color: #f0c674; +} + +[data-theme="dark"] .discord-havoc-disclaimer svg { + color: #faa81a; +} + /* Pagination / Load more */ .discord-load-more { display: flex; @@ -1966,6 +2008,11 @@ a.discord-embed-title:hover { font-weight: 600; } +.discord-system-unknown { + font-style: italic; + opacity: 0.7; +} + [data-theme="dark"] .discord-system-message { color: #949ba4; } diff --git a/src/components/DiscordLogs.jsx b/src/components/DiscordLogs.jsx index cdd6f4c..e27f4b1 100644 --- a/src/components/DiscordLogs.jsx +++ b/src/components/DiscordLogs.jsx @@ -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 ( +
+ {name} +
+ ); + } + + return ( +
+ ); +}); + // ============================================================================ // 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 ( + + + + ); + 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 ( + + + + ); + case MESSAGE_TYPE_CHANNEL_PINNED_MESSAGE: + return ( + + + + ); + default: + return ( + + + + ); + } + }; + return (
- - - + {getSystemIcon()}
{displayAuthor?.displayName || displayAuthor?.username || 'Unknown'} {' '} - + {systemText ? ( + + ) : ( + (system message type {type}) + )} {formatTimestamp(displayTimestamp)}
@@ -1352,18 +1523,21 @@ const DiscordMessage = memo(function DiscordMessage({
{stickers.map((sticker) => (
- {sticker.formatType === 3 ? ( - // Lottie stickers - show placeholder or name -
- {sticker.name} -
- ) : ( + {sticker.formatType === 3 && sticker.lottieData ? ( + // Lottie stickers with cached data + + ) : sticker.url ? ( {sticker.name} + ) : ( + // Fallback for missing sticker data +
+ {sticker.name} +
)}
))} @@ -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() {
); })} + {/* Disclaimer when Havoc bot is not in channel */} + {!isHavocInChannel && !loadingMembers && ( +
+ + + + Havoc no longer has access to this channel. New activity may not be visible. +
+ )}
)} diff --git a/src/components/TRip/MediaRequestForm.jsx b/src/components/TRip/MediaRequestForm.jsx index 9ad4ed9..f78c523 100644 --- a/src/components/TRip/MediaRequestForm.jsx +++ b/src/components/TRip/MediaRequestForm.jsx @@ -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() { } `} + + {/* Disk Space Indicator - always visible */} + {diskSpace && ( +
+ + + + + + 90 ? 'text-red-500' : + diskSpace.usedPercent > 75 ? 'text-yellow-500' : 'text-green-600 dark:text-green-400' + }`}>{diskSpace.availableFormatted} available + +
+
90 ? 'bg-red-500' : + diskSpace.usedPercent > 75 ? 'bg-yellow-500' : 'bg-green-500' + }`} + style={{ width: `${diskSpace.usedPercent}%` }} + /> +
+ ({diskSpace.usedPercent}% used) +
+ )} +

New Request

Search for an artist to browse and select tracks for download.

@@ -1283,8 +1326,8 @@ export default function MediaRequestForm() { ); })} -
-