feat: apply figma browse mobile redesign
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { ArrowDownToLine, LoaderCircle } from "lucide-react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { DownloadCloudIcon } from "../icons/DownloadCloudIcon";
|
||||
import { useState, type MouseEvent } from "react";
|
||||
import { useI18n } from "../../i18n";
|
||||
import type { Attachment } from "../../types/post";
|
||||
@@ -35,20 +36,23 @@ export function AttachmentDownloadPill({
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
disabled={isDownloading}
|
||||
className={`group z-10 inline-flex overflow-hidden rounded-full bg-black/45 text-[10px] text-white shadow-lg ring-1 ring-white/15 backdrop-blur-md transition hover:bg-black/60 disabled:cursor-wait ${className}`}
|
||||
className={`group z-10 inline-flex overflow-hidden rounded-full bg-black/80 text-[11px] text-white shadow-lg ring-1 ring-inset ring-white/20 backdrop-blur-md transition hover:bg-black/90 disabled:cursor-wait ${className}`}
|
||||
aria-label={
|
||||
isDownloading ? t("downloading") : `Download ${attachment.filename}`
|
||||
}
|
||||
aria-busy={isDownloading}
|
||||
>
|
||||
<span className="flex h-6 w-6 items-center justify-center bg-white/10 transition group-hover:bg-white/15">
|
||||
<span className="flex h-6 w-6 items-center justify-center bg-[#545454]/50 transition group-hover:bg-[#545454]/70">
|
||||
{isDownloading ? (
|
||||
<LoaderCircle className="h-3 w-3 animate-spin" strokeWidth={2.3} />
|
||||
<LoaderCircle
|
||||
className="h-3.5 w-3.5 animate-spin"
|
||||
strokeWidth={2.3}
|
||||
/>
|
||||
) : (
|
||||
<ArrowDownToLine className="h-3 w-3" strokeWidth={2.3} />
|
||||
<DownloadCloudIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</span>
|
||||
<span className="flex h-6 items-center gap-0.5 px-1.5">
|
||||
<span className="flex h-6 items-center gap-0.5 px-2">
|
||||
{isDownloading ? (
|
||||
t("downloading")
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { SlidersHorizontal } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { typeFilterLabel } from "../../resourceTypeLabels";
|
||||
|
||||
@@ -22,95 +20,37 @@ export type FilterChipsProps = {
|
||||
|
||||
export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
|
||||
const { t } = useI18n();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [hasOverflow, setHasOverflow] = useState(false);
|
||||
|
||||
const labelsKey = useMemo(
|
||||
() => TYPE_FILTERS.map((tp) => typeFilterLabel(t, tp)).join("|"),
|
||||
[t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
const container = containerRef.current;
|
||||
const measure = measureRef.current;
|
||||
if (!container || !measure) return;
|
||||
|
||||
const nextHasOverflow = measure.scrollWidth > container.clientWidth + 1;
|
||||
setHasOverflow(nextHasOverflow);
|
||||
if (!nextHasOverflow) setExpanded(false);
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
const resizeObserver = new ResizeObserver(checkOverflow);
|
||||
if (containerRef.current) resizeObserver.observe(containerRef.current);
|
||||
if (measureRef.current) resizeObserver.observe(measureRef.current);
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [labelsKey]);
|
||||
|
||||
const chipClass = (active: boolean) =>
|
||||
`inline-flex h-8 min-w-[72px] shrink-0 items-center justify-center rounded-full border px-3 text-xs leading-none transition ${
|
||||
const tabClass = (active: boolean) =>
|
||||
[
|
||||
"relative shrink-0 whitespace-nowrap px-1 py-3 text-[15px] leading-none outline-none transition-colors",
|
||||
"border-b-2",
|
||||
active
|
||||
? "border-ark-gold bg-ark-gold/10 text-ark-gold2"
|
||||
: "border-ark-line text-neutral-300 hover:border-ark-gold/50"
|
||||
}`;
|
||||
? "border-ark-gold text-ark-gold font-medium"
|
||||
: "border-transparent text-neutral-400 hover:text-ark-gold/80",
|
||||
].join(" ");
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="sticky top-0 z-10 overflow-hidden border-b border-ark-line bg-ark-bg/90 py-2 backdrop-blur md:rounded-t-xl"
|
||||
>
|
||||
<div className="flex items-start gap-1.5">
|
||||
<div
|
||||
className={`flex min-w-0 flex-1 gap-1.5 ${
|
||||
expanded
|
||||
? "flex-wrap whitespace-normal"
|
||||
: "overflow-hidden whitespace-nowrap"
|
||||
}`}
|
||||
>
|
||||
{TYPE_FILTERS.map((tp) => {
|
||||
const active = type === tp;
|
||||
return (
|
||||
<button
|
||||
key={tp}
|
||||
type="button"
|
||||
onClick={() => onTypeChange(tp)}
|
||||
className={chipClass(active)}
|
||||
>
|
||||
{typeFilterLabel(t, tp)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{hasOverflow ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-ark-line bg-[#1a1b20] text-neutral-200 transition hover:border-ark-gold/60 hover:text-ark-gold2 min-[440px]:w-9 md:w-10"
|
||||
aria-label={expanded ? "Collapse filters" : "Expand filters"}
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
<SlidersHorizontal
|
||||
className="h-3.5 w-3.5 md:h-4 md:w-4"
|
||||
strokeWidth={2.2}
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="sticky top-0 z-10 border-b border-ark-line bg-ark-bg/95 backdrop-blur md:rounded-t-xl">
|
||||
<div
|
||||
ref={measureRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none invisible absolute left-0 top-0 -z-10 flex h-0 max-w-none gap-1.5 overflow-hidden whitespace-nowrap"
|
||||
className="flex items-end gap-5 overflow-x-auto overflow-y-hidden px-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
role="tablist"
|
||||
>
|
||||
{TYPE_FILTERS.map((tp) => (
|
||||
<span key={tp} className={chipClass(type === tp)}>
|
||||
{typeFilterLabel(t, tp)}
|
||||
</span>
|
||||
))}
|
||||
{TYPE_FILTERS.map((tp) => {
|
||||
const active = type === tp;
|
||||
return (
|
||||
<button
|
||||
key={tp}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
onClick={() => onTypeChange(tp)}
|
||||
className={tabClass(active)}
|
||||
>
|
||||
{typeFilterLabel(t, tp)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,29 +26,31 @@ export function pickBubble(post: Post): BubbleComponent {
|
||||
export function MessageBubble({ post }: { post: Post }) {
|
||||
const { lang } = useI18n();
|
||||
const Bubble = pickBubble(post);
|
||||
const isTextOnly = post.attachments.length === 0;
|
||||
const isVisual = post.attachments.some(
|
||||
(a) => a.kind === "image" || a.kind === "video",
|
||||
);
|
||||
const isVisual =
|
||||
Bubble === AlbumBubble ||
|
||||
Bubble === VideoBubble ||
|
||||
Bubble === ImageBubble ||
|
||||
Bubble === ImageWithTextBubble;
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`post-${post.id}`}
|
||||
className="mx-auto w-full max-w-[380px] md:max-w-[680px] lg:max-w-[900px] xl:max-w-[1120px]"
|
||||
className="mx-auto w-full max-w-[358px] md:max-w-[680px] lg:max-w-[900px] xl:max-w-[1120px]"
|
||||
>
|
||||
<article
|
||||
className={`relative rounded-2xl bg-ark-panel text-left shadow-sm ${
|
||||
isVisual ? "w-full" : "w-fit max-w-full"
|
||||
} ${isTextOnly ? "px-3 py-2" : "p-2"}`}
|
||||
className={`relative w-full overflow-hidden rounded-2xl bg-[#272632] text-left shadow-sm ${
|
||||
isVisual ? "p-0" : "px-4 py-3"
|
||||
}`}
|
||||
>
|
||||
<Bubble post={post} />
|
||||
<time
|
||||
dateTime={post.publishedAt}
|
||||
className="ml-2 mt-1 inline-block float-right text-[10.5px] leading-none text-neutral-500"
|
||||
className={`block text-right text-[12px] leading-[19px] text-[#A8A9AE] ${
|
||||
isVisual ? "px-4 pb-3 pt-3" : "mt-3"
|
||||
}`}
|
||||
>
|
||||
{formatDateTime(post.publishedAt, lang)}
|
||||
</time>
|
||||
<span className="block clear-both" />
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -72,12 +72,12 @@ export function MessageStream({ scope }: MessageStreamProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-full px-3 md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
|
||||
<div className="mx-auto max-w-full px-4 md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
|
||||
<FilterChips type={type} onTypeChange={(v) => updateParam("type", v)} />
|
||||
|
||||
<div className="flex flex-col gap-2 pb-10 pt-2">
|
||||
<div className="flex flex-col gap-3 pt-2">
|
||||
{groups.map((group) => (
|
||||
<div key={group.dayKey} className="flex flex-col gap-2">
|
||||
<div key={group.dayKey} className="flex flex-col gap-3">
|
||||
{group.items.map((post) => (
|
||||
<MessageBubble key={post.id} post={post} />
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArrowDownToLine, LoaderCircle, X } from "lucide-react";
|
||||
import { LoaderCircle, X } from "lucide-react";
|
||||
import { DownloadCloudIcon } from "../../icons/DownloadCloudIcon";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useI18n } from "../../../i18n";
|
||||
@@ -12,8 +13,15 @@ import { postDisplayText } from "../utils/postText";
|
||||
|
||||
const MAX_VISIBLE = 4;
|
||||
|
||||
function imageRatio(att: Attachment) {
|
||||
return att.width && att.height ? `${att.width} / ${att.height}` : "4 / 3";
|
||||
function albumGridClass(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 albumItemClass(index: number, count: number) {
|
||||
if (count === 3 && index === 0) return "row-span-2";
|
||||
return "";
|
||||
}
|
||||
|
||||
function ImageListDownloadButton({
|
||||
@@ -51,7 +59,7 @@ function ImageListDownloadButton({
|
||||
{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>
|
||||
);
|
||||
@@ -142,54 +150,22 @@ export function AlbumBubble({ post }: { post: Post }) {
|
||||
const [listOpen, setListOpen] = useState(false);
|
||||
const images = post.attachments;
|
||||
const text = postDisplayText(post, lang);
|
||||
const shouldMerge = images.length > MAX_VISIBLE;
|
||||
|
||||
if (!shouldMerge) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{images.map((att, i) => (
|
||||
<div
|
||||
key={att.id}
|
||||
className="relative max-h-[180px] w-full overflow-hidden rounded-xl min-[440px]:max-h-[200px] md:max-h-[240px] lg:max-h-[280px]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openLightbox(images, i, text, post.id)}
|
||||
className="block w-full"
|
||||
aria-label={att.filename}
|
||||
>
|
||||
<img
|
||||
src={att.url}
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className="h-full w-full object-cover"
|
||||
style={{ aspectRatio: imageRatio(att) }}
|
||||
/>
|
||||
</button>
|
||||
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
||||
</div>
|
||||
))}
|
||||
{text ? (
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
|
||||
{autolink(text)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const visible = images.slice(0, MAX_VISIBLE);
|
||||
const extra = images.length - MAX_VISIBLE;
|
||||
const layoutCount = Math.min(images.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 min-[440px]:h-[250px] md:h-[300px] lg:h-[340px]">
|
||||
<div className="flex flex-col">
|
||||
<div className={`${albumGridClass(layoutCount)} gap-px overflow-hidden`}>
|
||||
{visible.map((att, i) => {
|
||||
const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0;
|
||||
return (
|
||||
<div
|
||||
key={att.id}
|
||||
className="relative h-full w-full overflow-hidden"
|
||||
className={`relative h-full w-full overflow-hidden ${albumItemClass(
|
||||
i,
|
||||
layoutCount,
|
||||
)}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -204,12 +180,10 @@ export function AlbumBubble({ post }: { post: Post }) {
|
||||
src={att.thumbnailUrl ?? att.url}
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className={`h-full w-full object-cover ${
|
||||
isLastSlot ? "blur-sm scale-105" : ""
|
||||
}`}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
{isLastSlot ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/45 text-3xl font-semibold text-white">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/55 text-4xl font-bold text-white">
|
||||
+{extra}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -222,7 +196,7 @@ export function AlbumBubble({ post }: { post: Post }) {
|
||||
})}
|
||||
</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}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArrowDownToLine, LoaderCircle } from "lucide-react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { DownloadCloudIcon } from "../../icons/DownloadCloudIcon";
|
||||
import { useState } from "react";
|
||||
import { useI18n } from "../../../i18n";
|
||||
import type { Attachment, Post } from "../../../types/post";
|
||||
@@ -56,7 +57,7 @@ function AttachmentRow({ postId, att }: { postId: string; att: Attachment }) {
|
||||
{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" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -6,15 +6,12 @@ export function ImageBubble({ post }: { post: Post }) {
|
||||
const { openLightbox } = useLightbox();
|
||||
const att = post.attachments[0];
|
||||
if (!att) return null;
|
||||
const ratio =
|
||||
att.width && att.height ? `${att.width} / ${att.height}` : "4 / 3";
|
||||
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden rounded-xl max-h-[240px] min-[440px]:max-h-[270px] md:max-h-[320px] lg:max-h-[360px]">
|
||||
<div className="relative h-[180px] w-full overflow-hidden bg-black min-[440px]:h-[210px] md:h-[260px] lg:h-[300px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openLightbox([att], 0, undefined, post.id)}
|
||||
className="block w-full"
|
||||
className="block h-full w-full"
|
||||
aria-label={att.filename}
|
||||
>
|
||||
<img
|
||||
@@ -22,7 +19,6 @@ export function ImageBubble({ post }: { post: Post }) {
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className="h-full w-full object-cover"
|
||||
style={{ aspectRatio: ratio }}
|
||||
/>
|
||||
</button>
|
||||
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
||||
|
||||
@@ -11,31 +11,27 @@ export function ImageWithTextBubble({ post }: { post: Post }) {
|
||||
const att = post.attachments[0];
|
||||
const text = postDisplayText(post, lang);
|
||||
if (!att) return null;
|
||||
const ratio =
|
||||
att.width && att.height ? `${att.width} / ${att.height}` : "4 / 3";
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden rounded-xl bg-black/20">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openLightbox([att], 0, text, post.id)}
|
||||
className="block w-full"
|
||||
aria-label={att.filename}
|
||||
>
|
||||
<img
|
||||
src={att.url}
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className="block h-auto w-full"
|
||||
style={{ aspectRatio: ratio }}
|
||||
/>
|
||||
</button>
|
||||
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
||||
<div className="flex flex-col">
|
||||
<div className="relative h-[180px] w-full overflow-hidden bg-black min-[440px]:h-[210px] md:h-[260px] lg:h-[300px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openLightbox([att], 0, text, post.id)}
|
||||
className="block h-full w-full"
|
||||
aria-label={att.filename}
|
||||
>
|
||||
<img
|
||||
src={att.url}
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
||||
</div>
|
||||
{text ? (
|
||||
<div className="bg-gradient-to-b from-ark-panel/90 to-ark-panel px-4 py-3 text-[14px] leading-snug text-neutral-100">
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words">
|
||||
{autolink(text)}
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user