feat: apply figma browse mobile redesign

This commit is contained in:
TerryM
2026-05-28 10:36:38 +08:00
parent 3825c4ec2f
commit 49f61b89f1
26 changed files with 401 additions and 264 deletions

View File

@@ -1,4 +1,5 @@
import { ArrowDownToLine, LoaderCircle, Play, X } from "lucide-react";
import { LoaderCircle, Play, X } from "lucide-react";
import { DownloadCloudIcon } from "../../icons/DownloadCloudIcon";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useI18n } from "../../../i18n";
@@ -12,6 +13,17 @@ import { postDisplayText } from "../utils/postText";
const MAX_VISIBLE = 4;
function videoGridClass(count: number) {
const height = "h-[230px] min-[440px]:h-[250px] md:h-[300px] lg:h-[340px]";
if (count === 2) return `${height} grid grid-cols-1 grid-rows-2`;
return `${height} grid grid-cols-2 grid-rows-2`;
}
function videoItemClass(index: number, count: number) {
if (count === 3 && index === 0) return "row-span-2";
return "";
}
function formatDuration(sec: number | undefined): string {
if (!sec || sec <= 0) return "";
const m = Math.floor(sec / 60);
@@ -54,7 +66,7 @@ function VideoAttachmentCard({
className={`relative w-full overflow-hidden bg-black ${
compact
? "h-full"
: "max-h-[220px] rounded-xl min-[440px]:max-h-[250px] md:max-h-[300px] lg:max-h-[340px]"
: "h-[180px] min-[440px]:h-[210px] md:h-[260px] lg:h-[300px]"
}`}
style={compact ? undefined : { aspectRatio: videoRatio(attachment) }}
onClick={() => {
@@ -180,7 +192,7 @@ function AttachmentListDownloadButton({
{isDownloading ? (
<LoaderCircle className="h-4 w-4 animate-spin" strokeWidth={2.3} />
) : (
<ArrowDownToLine className="h-4 w-4" strokeWidth={2.3} />
<DownloadCloudIcon className="h-4 w-4" />
)}
</button>
);
@@ -297,32 +309,38 @@ export function VideoBubble({ post }: { post: Post }) {
const [listOpen, setListOpen] = useState(false);
const videos = post.attachments.filter(isVideoAttachment);
const text = postDisplayText(post, lang);
const shouldMerge = videos.length > MAX_VISIBLE;
if (!videos.length) return null;
if (shouldMerge) {
if (videos.length >= 2) {
const visible = videos.slice(0, MAX_VISIBLE);
const extra = videos.length - MAX_VISIBLE;
const layoutCount = Math.min(videos.length, MAX_VISIBLE);
return (
<div className="flex flex-col gap-1.5">
<div className="grid h-[220px] grid-cols-2 grid-rows-2 gap-[2px] overflow-hidden rounded-xl bg-black min-[440px]:h-[250px] md:h-[300px] lg:h-[340px]">
<div className="flex flex-col">
<div
className={`${videoGridClass(layoutCount)} gap-px overflow-hidden bg-black`}
>
{visible.map((att, i) => {
const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0;
return (
<VideoAttachmentCard
<div
key={att.id}
postId={post.id}
attachment={att}
compact
overlayCount={isLastSlot ? extra : undefined}
onMoreClick={() => setListOpen(true)}
/>
className={`h-full w-full ${videoItemClass(i, layoutCount)}`}
>
<VideoAttachmentCard
postId={post.id}
attachment={att}
compact
overlayCount={isLastSlot ? extra : undefined}
onMoreClick={() => setListOpen(true)}
/>
</div>
);
})}
</div>
{text ? (
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
{autolink(text)}
</div>
) : null}
@@ -342,12 +360,10 @@ export function VideoBubble({ post }: { post: Post }) {
}
return (
<div className="flex flex-col gap-1.5">
{videos.map((att) => (
<VideoAttachmentCard key={att.id} postId={post.id} attachment={att} />
))}
<div className="flex flex-col">
<VideoAttachmentCard postId={post.id} attachment={videos[0]} />
{text ? (
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
{autolink(text)}
</div>
) : null}