{group.items.map((post) => (
))}
diff --git a/src/components/messageStream/bubbles/AlbumBubble.tsx b/src/components/messageStream/bubbles/AlbumBubble.tsx
index df06503..ee04115 100644
--- a/src/components/messageStream/bubbles/AlbumBubble.tsx
+++ b/src/components/messageStream/bubbles/AlbumBubble.tsx
@@ -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 ? (
) : (
-
+
)}
);
@@ -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 (
-
- {images.map((att, i) => (
-
-
-
-
- ))}
- {text ? (
-
- {autolink(text)}
-
- ) : null}
-
- );
- }
-
const visible = images.slice(0, MAX_VISIBLE);
const extra = images.length - MAX_VISIBLE;
+ const layoutCount = Math.min(images.length, MAX_VISIBLE);
return (
-
-
+
+
{visible.map((att, i) => {
const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0;
return (
{isLastSlot ? (
-
+
+{extra}
) : null}
@@ -222,7 +196,7 @@ export function AlbumBubble({ post }: { post: Post }) {
})}
{text ? (
-
+
{autolink(text)}
) : null}
diff --git a/src/components/messageStream/bubbles/FileDocBubble.tsx b/src/components/messageStream/bubbles/FileDocBubble.tsx
index 7b63ea3..19907d4 100644
--- a/src/components/messageStream/bubbles/FileDocBubble.tsx
+++ b/src/components/messageStream/bubbles/FileDocBubble.tsx
@@ -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 ? (
) : (
-
+
)}
diff --git a/src/components/messageStream/bubbles/ImageBubble.tsx b/src/components/messageStream/bubbles/ImageBubble.tsx
index 314a891..5c7c391 100644
--- a/src/components/messageStream/bubbles/ImageBubble.tsx
+++ b/src/components/messageStream/bubbles/ImageBubble.tsx
@@ -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 (
-
+
diff --git a/src/components/messageStream/bubbles/ImageWithTextBubble.tsx b/src/components/messageStream/bubbles/ImageWithTextBubble.tsx
index 966679a..5c51dcb 100644
--- a/src/components/messageStream/bubbles/ImageWithTextBubble.tsx
+++ b/src/components/messageStream/bubbles/ImageWithTextBubble.tsx
@@ -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 (
-
-
-
+
+
+
+
+
{text ? (
-
-
- {autolink(text)}
-
+
+ {autolink(text)}
) : null}
diff --git a/src/components/messageStream/bubbles/VideoBubble.tsx b/src/components/messageStream/bubbles/VideoBubble.tsx
index 34952b6..c553eee 100644
--- a/src/components/messageStream/bubbles/VideoBubble.tsx
+++ b/src/components/messageStream/bubbles/VideoBubble.tsx
@@ -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 ? (
) : (
-
+
)}
);
@@ -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 (
-
-
+
+
{visible.map((att, i) => {
const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0;
return (
- setListOpen(true)}
- />
+ className={`h-full w-full ${videoItemClass(i, layoutCount)}`}
+ >
+ setListOpen(true)}
+ />
+
);
})}
{text ? (
-
+
{autolink(text)}
) : null}
@@ -342,12 +360,10 @@ export function VideoBubble({ post }: { post: Post }) {
}
return (
-
- {videos.map((att) => (
-
- ))}
+
+
{text ? (
-
+
{autolink(text)}
) : null}
diff --git a/src/i18n.tsx b/src/i18n.tsx
index e115d64..7a95d31 100644
--- a/src/i18n.tsx
+++ b/src/i18n.tsx
@@ -118,6 +118,10 @@ const zhDict: Dict = {
adminSearchQuery: "查询词",
adminSearchTime: "时间",
adminSearchId: "编号",
+ favorites: "我的收藏",
+ favoritesComingSoon: "功能即将推出",
+ favoritesComingSoonDesc: "登入与收藏功能开发中,敬请期待。",
+ backToHome: "返回首页",
};
const enDict: Dict = {
@@ -228,6 +232,11 @@ const enDict: Dict = {
adminSearchQuery: "Query",
adminSearchTime: "Time",
adminSearchId: "ID",
+ favorites: "My Favorites",
+ favoritesComingSoon: "Coming Soon",
+ favoritesComingSoonDesc:
+ "Sign-in and favorites are in development. Stay tuned.",
+ backToHome: "Back to Home",
};
const languageNames: Record
= {
diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx
index ccd90a6..c4dc84b 100644
--- a/src/layouts/PublicLayout.tsx
+++ b/src/layouts/PublicLayout.tsx
@@ -266,6 +266,8 @@ export function PublicLayout() {
const na = (which: PublicNavWhich) =>
navIsActive(pathname, search, hash, which);
+ const footerInContentFlow =
+ pathname === "/browse" || pathname.startsWith("/category/");
const goSearch = () => {
const s = q.trim();
@@ -276,7 +278,7 @@ export function PublicLayout() {
};
return (
-
+
-
+
-