From b33557565a85ae247a7150b052e26b68735362b1 Mon Sep 17 00:00:00 2001 From: ragnep Date: Fri, 27 Mar 2026 11:55:23 +0200 Subject: [PATCH 1/3] wip Signed-off-by: ragnep --- .../MemesQuickVoteControls.tsx | 113 +++++++++++++++++- .../MemesQuickVoteDialogSkeleton.tsx | 24 ++-- .../media/DropListItemContentMedia.tsx | 61 ++++++++-- .../media/DropListItemContentMediaImage.tsx | 19 ++- 4 files changed, 184 insertions(+), 33 deletions(-) diff --git a/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteControls.tsx b/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteControls.tsx index 1e87c7bb6f..6c92ca116b 100644 --- a/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteControls.tsx +++ b/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteControls.tsx @@ -2,6 +2,7 @@ import { formatNumberWithCommas } from "@/helpers/Helpers"; import type { ExtendedDrop } from "@/helpers/waves/drop.helpers"; +import { useCallback, useEffect, useRef, useState } from "react"; import MemesQuickVoteActionBar from "./MemesQuickVoteActionBar"; import MemesQuickVoteDropHeader from "./MemesQuickVoteDropHeader"; @@ -27,6 +28,109 @@ interface MemesQuickVoteControlsProps { readonly onVoteAmount: (amount: number) => void; } +function MemesQuickVoteDescription({ + description, +}: { + readonly description: string; +}) { + const [isExpanded, setIsExpanded] = useState(false); + const [isOverflowing, setIsOverflowing] = useState(false); + const visibleDescriptionRef = useRef(null); + const clampClass = isExpanded ? "tw-line-clamp-none" : "tw-line-clamp-4"; + const descriptionClassName = + "tw-mb-0 tw-whitespace-pre-line tw-text-sm tw-font-medium tw-leading-relaxed tw-text-iron-400 md:tw-text-md"; + + const measureOverflow = useCallback(() => { + const visibleDescription = visibleDescriptionRef.current; + + if (!visibleDescription) { + return; + } + + const computedStyles = globalThis.getComputedStyle(visibleDescription); + const lineHeight = Number.parseFloat(computedStyles.lineHeight || "0"); + const collapsedHeight = lineHeight * 4; + + if (!Number.isFinite(collapsedHeight) || collapsedHeight <= 0) { + return; + } + + const previousDisplay = visibleDescription.style.display; + const previousOverflow = visibleDescription.style.overflow; + const previousWebkitLineClamp = visibleDescription.style.webkitLineClamp; + + visibleDescription.style.display = "block"; + visibleDescription.style.overflow = "visible"; + visibleDescription.style.webkitLineClamp = "unset"; + + const fullHeight = visibleDescription.getBoundingClientRect().height; + + visibleDescription.style.display = previousDisplay; + visibleDescription.style.overflow = previousOverflow; + visibleDescription.style.webkitLineClamp = previousWebkitLineClamp; + + const nextIsOverflowing = fullHeight > collapsedHeight + 1; + + setIsOverflowing((current) => + current === nextIsOverflowing ? current : nextIsOverflowing + ); + }, []); + + useEffect(() => { + const frameId = globalThis.requestAnimationFrame(() => { + measureOverflow(); + }); + + if (typeof ResizeObserver === "undefined") { + const handleResize = () => { + measureOverflow(); + }; + + globalThis.addEventListener("resize", handleResize); + return () => { + globalThis.removeEventListener("resize", handleResize); + globalThis.cancelAnimationFrame(frameId); + }; + } + + const observer = new ResizeObserver(() => { + measureOverflow(); + }); + + if (visibleDescriptionRef.current) { + observer.observe(visibleDescriptionRef.current); + } + + return () => { + observer.disconnect(); + globalThis.cancelAnimationFrame(frameId); + }; + }, [measureOverflow]); + + return ( +
+

+ {description} +

+ {isOverflowing && ( + + )} +
+ ); +} + export default function MemesQuickVoteControls({ customValue, drop, @@ -65,7 +169,7 @@ export default function MemesQuickVoteControls({
-
+
@@ -75,9 +179,10 @@ export default function MemesQuickVoteControls({ {description && ( -

- {description} -

+ )}
diff --git a/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialogSkeleton.tsx b/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialogSkeleton.tsx index fd23037866..8cd8872f56 100644 --- a/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialogSkeleton.tsx +++ b/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialogSkeleton.tsx @@ -40,26 +40,26 @@ function MemesQuickVoteCopySkeleton({ function MemesQuickVoteActionBarSkeleton() { return ( -
+
- + -
+
-
- - +
+ +
- - + +
- +
); @@ -80,12 +80,12 @@ export default function MemesQuickVoteDialogSkeleton() {

- +
- +
@@ -121,7 +121,7 @@ export default function MemesQuickVoteDialogSkeleton() { className="tw-hidden tw-shrink-0 tw-flex-col tw-gap-0 md:tw-flex md:tw-h-full md:tw-min-h-0 md:tw-overflow-hidden md:tw-bg-[#0a0a0a]/30" >
- +
diff --git a/components/drops/view/item/content/media/DropListItemContentMedia.tsx b/components/drops/view/item/content/media/DropListItemContentMedia.tsx index c82cc9574e..3d01416c07 100644 --- a/components/drops/view/item/content/media/DropListItemContentMedia.tsx +++ b/components/drops/view/item/content/media/DropListItemContentMedia.tsx @@ -25,6 +25,43 @@ const DropListItemContentMediaGLB = dynamic( } ); +const IMAGE_URL_PATTERN = + /\.(avif|bmp|gif|heic|heif|jpe?g|png|svg|webp)(?:$|[?#])/i; +const VIDEO_URL_PATTERN = /\.(m3u8|m4v|mov|mp4|mpeg|mpg|ogv|webm)(?:$|[?#])/i; +const AUDIO_URL_PATTERN = /\.(aac|flac|m4a|mp3|oga|ogg|wav)(?:$|[?#])/i; +const HTML_URL_PATTERN = /\.(html?)(?:$|[?#])/i; +const GLB_URL_PATTERN = /\.(glb|gltf)(?:$|[?#])/i; + +const resolveMediaTypeFromUrl = (mediaUrl: string): MediaType => { + if (IMAGE_URL_PATTERN.test(mediaUrl)) { + return MediaType.IMAGE; + } + + if (VIDEO_URL_PATTERN.test(mediaUrl)) { + return MediaType.VIDEO; + } + + if (AUDIO_URL_PATTERN.test(mediaUrl)) { + return MediaType.AUDIO; + } + + if (GLB_URL_PATTERN.test(mediaUrl)) { + return MediaType.GLB; + } + + if (HTML_URL_PATTERN.test(mediaUrl)) { + return MediaType.HTML; + } + + return MediaType.UNKNOWN; +}; + +const UnavailableMediaFallback = () => ( +
+ Preview unavailable +
+); + export default function DropListItemContentMedia({ media_mime_type, media_url, @@ -49,27 +86,28 @@ export default function DropListItemContentMedia({ readonly htmlPreviewImageUrl?: string | undefined; }) { const getMediaType = (): MediaType => { - if (media_mime_type.includes("image")) { + const normalizedMimeType = media_mime_type.trim().toLowerCase(); + + if (normalizedMimeType.includes("image")) { return MediaType.IMAGE; } - if (media_mime_type.includes("video")) { + if (normalizedMimeType.includes("video")) { return MediaType.VIDEO; } - if (media_mime_type.includes("audio")) { + if (normalizedMimeType.includes("audio")) { return MediaType.AUDIO; } if ( - media_mime_type === "model/gltf-binary" || - media_mime_type === "model/gltf+json" || - media_url.endsWith(".glb") || - media_url.endsWith(".gltf") + normalizedMimeType === "model/gltf-binary" || + normalizedMimeType === "model/gltf+json" ) { return MediaType.GLB; } - if (media_mime_type === "text/html") { + if (normalizedMimeType === "text/html") { return MediaType.HTML; } - return MediaType.UNKNOWN; + + return resolveMediaTypeFromUrl(media_url); }; const mediaType = getMediaType(); @@ -109,9 +147,8 @@ export default function DropListItemContentMedia({ /> ); case MediaType.UNKNOWN: - return <>; + return ; default: - assertUnreachable(mediaType); - return; + return assertUnreachable(mediaType); } } diff --git a/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx b/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx index 5baee50c1d..a3d1dfd186 100644 --- a/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx +++ b/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx @@ -47,6 +47,7 @@ function DropListItemContentMediaImage({ }) { const [ref, inView] = useInView(); const [loaded, setLoaded] = useState(false); + const [hasFailed, setHasFailed] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [errorCount, setErrorCount] = useState(0); const [retryTick, setRetryTick] = useState(0); @@ -58,11 +59,17 @@ function DropListItemContentMediaImage({ const { isCapacitor } = useCapacitor(); const handleImageLoad = useCallback(() => { + setHasFailed(false); setLoaded(true); }, []); const handleError = useCallback(() => { - if (errorCount >= maxRetries) return; // give up – show fallback + if (errorCount >= maxRetries) { + setHasFailed(true); + setLoaded(false); + return; + } + const delay = 500 * 2 ** errorCount; // 0.5s, 1s, 2s … setTimeout(() => { setErrorCount((n) => n + 1); @@ -72,6 +79,7 @@ function DropListItemContentMediaImage({ const manualRetry = () => { setErrorCount(0); + setHasFailed(false); setLoaded(false); setRetryTick((t) => t + 1); }; @@ -105,7 +113,7 @@ function DropListItemContentMediaImage({ event.stopPropagation(); const fullscreenTarget = modalImageRef.current ?? imgRef.current; if (fullscreenTarget) { - fullscreenTarget.requestFullscreen(); + void fullscreenTarget.requestFullscreen(); } }, [] @@ -162,6 +170,7 @@ function DropListItemContentMediaImage({ wrapperClass="tw-w-full tw-h-full tw-flex tw-items-center tw-justify-center" contentClass="tw-w-full tw-h-full tw-flex tw-items-center tw-justify-center" > + {/* eslint-disable-next-line @next/next/no-img-element */} - {!loaded && errorCount <= maxRetries && ( + {!loaded && !hasFailed && errorCount <= maxRetries && (
)} - {inView && errorCount <= maxRetries && ( + {inView && !hasFailed && errorCount <= maxRetries && ( )} - {errorCount > maxRetries && ( + {hasFailed && (
Couldn’t load image. From afa781be6b7c6476dfa5dcde4461b2713ed0e936 Mon Sep 17 00:00:00 2001 From: ragnep Date: Fri, 27 Mar 2026 12:03:59 +0200 Subject: [PATCH 2/3] wip Signed-off-by: ragnep --- .../media/DropListItemContentMedia.tsx | 58 ++++--------------- .../media/DropListItemContentMediaImage.tsx | 16 ++--- 2 files changed, 14 insertions(+), 60 deletions(-) diff --git a/components/drops/view/item/content/media/DropListItemContentMedia.tsx b/components/drops/view/item/content/media/DropListItemContentMedia.tsx index 3d01416c07..8dff10622c 100644 --- a/components/drops/view/item/content/media/DropListItemContentMedia.tsx +++ b/components/drops/view/item/content/media/DropListItemContentMedia.tsx @@ -25,43 +25,6 @@ const DropListItemContentMediaGLB = dynamic( } ); -const IMAGE_URL_PATTERN = - /\.(avif|bmp|gif|heic|heif|jpe?g|png|svg|webp)(?:$|[?#])/i; -const VIDEO_URL_PATTERN = /\.(m3u8|m4v|mov|mp4|mpeg|mpg|ogv|webm)(?:$|[?#])/i; -const AUDIO_URL_PATTERN = /\.(aac|flac|m4a|mp3|oga|ogg|wav)(?:$|[?#])/i; -const HTML_URL_PATTERN = /\.(html?)(?:$|[?#])/i; -const GLB_URL_PATTERN = /\.(glb|gltf)(?:$|[?#])/i; - -const resolveMediaTypeFromUrl = (mediaUrl: string): MediaType => { - if (IMAGE_URL_PATTERN.test(mediaUrl)) { - return MediaType.IMAGE; - } - - if (VIDEO_URL_PATTERN.test(mediaUrl)) { - return MediaType.VIDEO; - } - - if (AUDIO_URL_PATTERN.test(mediaUrl)) { - return MediaType.AUDIO; - } - - if (GLB_URL_PATTERN.test(mediaUrl)) { - return MediaType.GLB; - } - - if (HTML_URL_PATTERN.test(mediaUrl)) { - return MediaType.HTML; - } - - return MediaType.UNKNOWN; -}; - -const UnavailableMediaFallback = () => ( -
- Preview unavailable -
-); - export default function DropListItemContentMedia({ media_mime_type, media_url, @@ -86,28 +49,27 @@ export default function DropListItemContentMedia({ readonly htmlPreviewImageUrl?: string | undefined; }) { const getMediaType = (): MediaType => { - const normalizedMimeType = media_mime_type.trim().toLowerCase(); - - if (normalizedMimeType.includes("image")) { + if (media_mime_type.includes("image")) { return MediaType.IMAGE; } - if (normalizedMimeType.includes("video")) { + if (media_mime_type.includes("video")) { return MediaType.VIDEO; } - if (normalizedMimeType.includes("audio")) { + if (media_mime_type.includes("audio")) { return MediaType.AUDIO; } if ( - normalizedMimeType === "model/gltf-binary" || - normalizedMimeType === "model/gltf+json" + media_mime_type === "model/gltf-binary" || + media_mime_type === "model/gltf+json" || + media_url.endsWith(".glb") || + media_url.endsWith(".gltf") ) { return MediaType.GLB; } - if (normalizedMimeType === "text/html") { + if (media_mime_type === "text/html") { return MediaType.HTML; } - - return resolveMediaTypeFromUrl(media_url); + return MediaType.UNKNOWN; }; const mediaType = getMediaType(); @@ -147,7 +109,7 @@ export default function DropListItemContentMedia({ /> ); case MediaType.UNKNOWN: - return ; + return <>; default: return assertUnreachable(mediaType); } diff --git a/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx b/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx index a3d1dfd186..ff428df101 100644 --- a/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx +++ b/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx @@ -47,7 +47,6 @@ function DropListItemContentMediaImage({ }) { const [ref, inView] = useInView(); const [loaded, setLoaded] = useState(false); - const [hasFailed, setHasFailed] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [errorCount, setErrorCount] = useState(0); const [retryTick, setRetryTick] = useState(0); @@ -59,17 +58,11 @@ function DropListItemContentMediaImage({ const { isCapacitor } = useCapacitor(); const handleImageLoad = useCallback(() => { - setHasFailed(false); setLoaded(true); }, []); const handleError = useCallback(() => { - if (errorCount >= maxRetries) { - setHasFailed(true); - setLoaded(false); - return; - } - + if (errorCount >= maxRetries) return; const delay = 500 * 2 ** errorCount; // 0.5s, 1s, 2s … setTimeout(() => { setErrorCount((n) => n + 1); @@ -79,7 +72,6 @@ function DropListItemContentMediaImage({ const manualRetry = () => { setErrorCount(0); - setHasFailed(false); setLoaded(false); setRetryTick((t) => t + 1); }; @@ -300,7 +292,7 @@ function DropListItemContentMediaImage({ isCompetitionDrop ? "tw-justify-center" : "" }`} > - {!loaded && !hasFailed && errorCount <= maxRetries && ( + {!loaded && errorCount <= maxRetries && (
)} - {inView && !hasFailed && errorCount <= maxRetries && ( + {inView && errorCount <= maxRetries && ( )} - {hasFailed && ( + {errorCount > maxRetries && (
Couldn’t load image. From 7fc660133a3c56914095d4d46482bfc4b8f3faf6 Mon Sep 17 00:00:00 2001 From: ragnep Date: Fri, 27 Mar 2026 12:14:02 +0200 Subject: [PATCH 3/3] wip Signed-off-by: ragnep --- .../view/item/content/media/DropListItemContentMediaImage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx b/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx index ff428df101..50f270e2fa 100644 --- a/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx +++ b/components/drops/view/item/content/media/DropListItemContentMediaImage.tsx @@ -105,7 +105,7 @@ function DropListItemContentMediaImage({ event.stopPropagation(); const fullscreenTarget = modalImageRef.current ?? imgRef.current; if (fullscreenTarget) { - void fullscreenTarget.requestFullscreen(); + fullscreenTarget.requestFullscreen().catch(() => undefined); } }, []