From 557793bcf735026a96777c6ba0017e8cebea3913 Mon Sep 17 00:00:00 2001 From: Simo Date: Thu, 29 Jan 2026 09:15:53 +0100 Subject: [PATCH 1/7] wip Signed-off-by: Simo --- .../view/part/dropPartMarkdown/renderers.tsx | 45 +++---- .../boosted/BoostedTweetPreview.module.css | 33 ----- .../home/boosted/BoostedTweetPreview.tsx | 39 +----- .../tweets/ExpandableTweetPreview.module.css | 94 ++++++++++++++ components/tweets/ExpandableTweetPreview.tsx | 116 ++++++++++++++++++ .../tweets/ExpandableTweetPreview.types.ts | 3 + .../ExpandableTweetPreviewCompactContent.tsx | 14 +++ .../ExpandableTweetPreviewCompactControls.tsx | 29 +++++ .../ExpandableTweetPreviewExpandedContent.tsx | 34 +++++ ...ExpandableTweetPreviewMeasurementShell.tsx | 26 ++++ 10 files changed, 339 insertions(+), 94 deletions(-) delete mode 100644 components/home/boosted/BoostedTweetPreview.module.css create mode 100644 components/tweets/ExpandableTweetPreview.module.css create mode 100644 components/tweets/ExpandableTweetPreview.tsx create mode 100644 components/tweets/ExpandableTweetPreview.types.ts create mode 100644 components/tweets/ExpandableTweetPreviewCompactContent.tsx create mode 100644 components/tweets/ExpandableTweetPreviewCompactControls.tsx create mode 100644 components/tweets/ExpandableTweetPreviewExpandedContent.tsx create mode 100644 components/tweets/ExpandableTweetPreviewMeasurementShell.tsx diff --git a/components/drops/view/part/dropPartMarkdown/renderers.tsx b/components/drops/view/part/dropPartMarkdown/renderers.tsx index ace4d1c59e..c9f0fd1546 100644 --- a/components/drops/view/part/dropPartMarkdown/renderers.tsx +++ b/components/drops/view/part/dropPartMarkdown/renderers.tsx @@ -1,6 +1,5 @@ import type { ReactElement } from "react"; import { ErrorBoundary } from "react-error-boundary"; -import type { TweetProps, TwitterComponents } from "react-tweet"; import type { ApiDrop } from "@/generated/models/ApiDrop"; import type { SeizeQuoteLinkInfo } from "@/helpers/SeizeLinkParser"; @@ -8,31 +7,16 @@ import type { SeizeQuoteLinkInfo } from "@/helpers/SeizeLinkParser"; import LinkHandlerFrame from "@/components/waves/LinkHandlerFrame"; import WaveDropQuoteWithDropId from "@/components/waves/drops/WaveDropQuoteWithDropId"; import WaveDropQuoteWithSerialNo from "@/components/waves/drops/WaveDropQuoteWithSerialNo"; -import dynamic from "next/dynamic"; +import ExpandableTweetPreview from "@/components/tweets/ExpandableTweetPreview"; import { ensureTwitterLink } from "./twitter"; -const TweetEmbedSkeleton = () => ( -
- Loading tweet… -
-); - -const TweetEmbed = dynamic( - () => import("react-tweet").then((mod) => mod.Tweet), - { - ssr: false, - loading: TweetEmbedSkeleton, - } -); - const TweetFallback = ({ href }: { href: string }) => ( + className="tw-flex tw-h-full tw-w-full tw-flex-col tw-justify-center tw-gap-y-1 tw-rounded-xl tw-border tw-border-solid tw-border-iron-700 tw-bg-iron-900 tw-p-4 tw-text-left tw-no-underline tw-transition-colors tw-duration-200 hover:tw-border-iron-500 focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-offset-2 focus-visible:tw-outline-primary-400" + > Tweet unavailable @@ -41,15 +25,19 @@ const TweetFallback = ({ href }: { href: string }) => ( ); const renderTweetEmbed = (href: string) => { - const result = ensureTwitterLink(href); - const renderFallback = () => ; - const TweetNotFound: TwitterComponents["TweetNotFound"] = () => - renderFallback(); + const { tweetId, href: normalizedHref } = ensureTwitterLink(href); + const renderFallback = () => ; return ( - -
+ +
renderFallback()}> - + ( + + )} + />
@@ -60,7 +48,7 @@ const renderGifEmbed = (url: string): ReactElement => ( {url} ); @@ -86,7 +74,8 @@ const renderSeizeQuote = ( return ( + relativeHref={`/waves?wave=${waveId}&drop=${dropId}`} + > (data ? enrichTweet(data) : null), [data]); - - if (isLoading) { - return ( -
- -
- ); - } - - if (!tweet) { - return ; - } - return ( -
- - - - -
+ } + /> ); } diff --git a/components/tweets/ExpandableTweetPreview.module.css b/components/tweets/ExpandableTweetPreview.module.css new file mode 100644 index 0000000000..8bb759c348 --- /dev/null +++ b/components/tweets/ExpandableTweetPreview.module.css @@ -0,0 +1,94 @@ +.root { + --tweet-container-margin: 0; + --tweet-header-font-size: 0.75rem; + --tweet-header-line-height: 1rem; + --tweet-body-font-size: 0.9rem; + --tweet-body-line-height: 1.35rem; + --tweet-body-margin: 0; +} + +.root :global(.react-tweet-theme) { + width: 100%; + min-width: 0; + max-width: 100%; + height: 100%; +} + +.root :global(.react-tweet-theme article) { + padding: 0.5rem 0.75rem; + height: 100%; +} + +.compact :global(.react-tweet-theme p) { + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.expanded :global(.react-tweet-theme p) { + display: block; + -webkit-line-clamp: unset; + -webkit-box-orient: unset; + overflow: visible; +} + +.compact :global(.react-tweet-theme [class*="authorFollow"]) { + display: none; +} + +.root :global(.react-tweet-theme [class*="header"]) { + padding-bottom: 0.5rem; +} + +.fade { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 3.5rem; + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0), + var(--tweet-bg-color, rgb(21, 32, 43)) + ); + pointer-events: none; +} + +.toggle { + position: absolute; + right: 0.5rem; + bottom: 0.5rem; + z-index: 2; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + padding: 0.2rem 0.6rem; + border-radius: 9999px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(0, 0, 0, 0.35); + color: var(--tweet-font-color, rgb(247, 249, 249)); + font-size: 0.75rem; + line-height: 1rem; + font-weight: 600; + backdrop-filter: blur(6px); + transition: + border-color 0.2s ease, + background-color 0.2s ease; +} + +.toggle:hover { + background: rgba(0, 0, 0, 0.5); + border-color: rgba(255, 255, 255, 0.2); +} + +.measure { + position: absolute; + visibility: hidden; + pointer-events: none; + height: auto; + width: 100%; + left: 0; + top: 0; +} diff --git a/components/tweets/ExpandableTweetPreview.tsx b/components/tweets/ExpandableTweetPreview.tsx new file mode 100644 index 0000000000..5db3898d05 --- /dev/null +++ b/components/tweets/ExpandableTweetPreview.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { + useLayoutEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; +import { TweetContainer, enrichTweet, useTweet } from "react-tweet"; + +import styles from "./ExpandableTweetPreview.module.css"; +import CompactControls from "./ExpandableTweetPreviewCompactControls"; +import CompactTweetContent from "./ExpandableTweetPreviewCompactContent"; +import ExpandedTweetContent from "./ExpandableTweetPreviewExpandedContent"; +import MeasurementShell from "./ExpandableTweetPreviewMeasurementShell"; +import type { EnrichedTweet } from "./ExpandableTweetPreview.types"; + +interface ExpandableTweetPreviewProps { + readonly href: string; + readonly tweetId: string; + readonly compactHeightClassName?: string | undefined; + readonly renderFallback?: ((href: string) => ReactNode) | undefined; +} + +const COMPACT_PX = 256; +const COMPACT_THRESHOLD_MULTIPLIER = 1.5; +const COMPACT_THRESHOLD_PX = COMPACT_PX * COMPACT_THRESHOLD_MULTIPLIER; + +const LoadingPlaceholder = () => ( +
+ Loading tweet... +
+); + +export default function ExpandableTweetPreview({ + href, + tweetId, + compactHeightClassName = "tw-h-64", + renderFallback, +}: ExpandableTweetPreviewProps) { + const { data, isLoading } = useTweet(tweetId); + const tweet = useMemo( + () => (data ? enrichTweet(data) : null), + [data] + ); + const [isExpanded, setIsExpanded] = useState(false); + const [measuredHeight, setMeasuredHeight] = useState(null); + const measureRef = useRef(null); + + useLayoutEffect(() => { + if (!tweet || measuredHeight !== null) { + return; + } + if (!measureRef.current) { + return; + } + const rect = measureRef.current.getBoundingClientRect(); + if (rect.height > 0) { + setMeasuredHeight(rect.height); + } + }, [tweet, measuredHeight]); + + const shouldCompact = + measuredHeight === null ? true : measuredHeight > COMPACT_THRESHOLD_PX; + + const heightClassName = + !shouldCompact || isExpanded ? "" : compactHeightClassName; + const modeClassName = isExpanded ? styles["expanded"] : styles["compact"]; + const overflowClassName = + isExpanded || !shouldCompact ? "tw-overflow-visible" : "tw-overflow-hidden"; + const rootClassName = `${styles["root"] ?? ""} ${modeClassName ?? ""} ${heightClassName} ${overflowClassName} tw-relative tw-w-full tw-min-w-0 tw-max-w-full`; + const showMeasure = measuredHeight === null; + const showExpanded = isExpanded || !shouldCompact; + const showCompactControls = !isExpanded && shouldCompact; + + if (isLoading) { + return ( +
+ + + +
+ ); + } + + if (!tweet) { + return ( +
+ {renderFallback ? renderFallback(href) : null} +
+ ); + } + + return ( +
+ + + + + {showExpanded ? ( + + ) : ( + + )} + + + setIsExpanded(true)} + /> +
+ ); +} diff --git a/components/tweets/ExpandableTweetPreview.types.ts b/components/tweets/ExpandableTweetPreview.types.ts new file mode 100644 index 0000000000..ec31ff8624 --- /dev/null +++ b/components/tweets/ExpandableTweetPreview.types.ts @@ -0,0 +1,3 @@ +import type { enrichTweet } from "react-tweet"; + +export type EnrichedTweet = NonNullable>; diff --git a/components/tweets/ExpandableTweetPreviewCompactContent.tsx b/components/tweets/ExpandableTweetPreviewCompactContent.tsx new file mode 100644 index 0000000000..83bd781589 --- /dev/null +++ b/components/tweets/ExpandableTweetPreviewCompactContent.tsx @@ -0,0 +1,14 @@ +import { TweetBody, TweetHeader } from "react-tweet"; + +import type { EnrichedTweet } from "./ExpandableTweetPreview.types"; + +export default function CompactTweetContent({ + tweet, +}: Readonly<{ tweet: EnrichedTweet }>) { + return ( + <> + + + + ); +} diff --git a/components/tweets/ExpandableTweetPreviewCompactControls.tsx b/components/tweets/ExpandableTweetPreviewCompactControls.tsx new file mode 100644 index 0000000000..799329cf5f --- /dev/null +++ b/components/tweets/ExpandableTweetPreviewCompactControls.tsx @@ -0,0 +1,29 @@ +import styles from "./ExpandableTweetPreview.module.css"; + +export default function CompactControls({ + showFade, + showButton, + isExpanded, + onExpand, +}: { + readonly showFade: boolean; + readonly showButton: boolean; + readonly isExpanded: boolean; + readonly onExpand: () => void; +}) { + return ( + <> + {showFade ?