feat: 消息气泡长文字支持「展开全部/收起全部」折叠

- 新增 CollapsibleText 组件:超过 8 行文字自动折叠,按行高对齐裁切,底部柔和渐隐遮罩暗示有更多内容
- 折叠按钮左对齐,胶囊样式 + chevron 图标,hover 浅背景高亮,点击 chevron 旋转 180°
- 应用到全部 5 种气泡:Text/ImageWithText/Album/Video/FileDoc
- 动画统一使用 motion 包的 EASE_OUT 缓动,尊重 reducedMotion=user
- i18n 七种语言新增 showMore/showLess 文案
This commit is contained in:
TerryM
2026-05-29 23:49:59 +08:00
parent a7792c117d
commit b283ba74da
7 changed files with 170 additions and 12 deletions

View File

@@ -5,6 +5,7 @@ import { BubbleImage } from "../BubbleImage";
import { useLightbox } from "../overlays/ImageLightbox";
import { autolink } from "../utils/autolink";
import { postDisplayText } from "../utils/postText";
import { CollapsibleText } from "../CollapsibleText";
const MAX_VISIBLE = 4;
@@ -71,9 +72,12 @@ export function AlbumBubble({ post }: { post: Post }) {
})}
</div>
{text ? (
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
<CollapsibleText
wrapperClassName="px-4 pt-3"
className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-6 text-neutral-100"
>
{autolink(text)}
</div>
</CollapsibleText>
) : null}
</div>
);

View File

@@ -11,6 +11,7 @@ import {
} from "../utils/filenameDisplay";
import { formatBytes } from "../utils/formatBytes";
import { postDisplayText } from "../utils/postText";
import { CollapsibleText } from "../CollapsibleText";
import { useToast } from "../../Toast";
function AttachmentRow({ postId, att }: { postId: string; att: Attachment }) {
@@ -97,9 +98,9 @@ export function FileDocBubble({ post }: { post: Post }) {
<AttachmentRow key={att.id} postId={post.id} att={att} />
))}
{text ? (
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[15px] font-medium leading-6 text-neutral-100">
<CollapsibleText className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[15px] font-medium leading-6 text-neutral-100">
{text}
</div>
</CollapsibleText>
) : null}
</div>
);

View File

@@ -5,6 +5,7 @@ import { AttachmentDownloadPill } from "../AttachmentDownloadPill";
import { BubbleImage } from "../BubbleImage";
import { autolink } from "../utils/autolink";
import { postDisplayText } from "../utils/postText";
import { CollapsibleText } from "../CollapsibleText";
export function ImageWithTextBubble({ post }: { post: Post }) {
const { openLightbox } = useLightbox();
@@ -30,9 +31,12 @@ export function ImageWithTextBubble({ post }: { post: Post }) {
<AttachmentDownloadPill postId={post.id} attachment={att} />
</div>
{text ? (
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
<CollapsibleText
wrapperClassName="px-4 pt-3"
className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-6 text-neutral-100"
>
{autolink(text)}
</div>
</CollapsibleText>
) : null}
</div>
);

View File

@@ -2,12 +2,13 @@ import type { Post } from "../../../types/post";
import { useI18n } from "../../../i18n";
import { autolink } from "../utils/autolink";
import { postDisplayText } from "../utils/postText";
import { CollapsibleText } from "../CollapsibleText";
export function TextBubble({ post }: { post: Post }) {
const { lang } = useI18n();
return (
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
<CollapsibleText className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
{autolink(postDisplayText(post, lang))}
</div>
</CollapsibleText>
);
}

View File

@@ -7,6 +7,7 @@ import type { Attachment, Post } from "../../../types/post";
import { AttachmentDownloadPill } from "../AttachmentDownloadPill";
import { useVideoPlayer } from "../overlays/VideoPlayer";
import { autolink } from "../utils/autolink";
import { CollapsibleText } from "../CollapsibleText";
import { downloadAttachment } from "../utils/downloadFile";
import { formatBytes } from "../utils/formatBytes";
import { postDisplayText } from "../utils/postText";
@@ -355,9 +356,12 @@ export function VideoBubble({ post }: { post: Post }) {
})}
</div>
{text ? (
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
<CollapsibleText
wrapperClassName="px-4 pt-3"
className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-6 text-neutral-100"
>
{autolink(text)}
</div>
</CollapsibleText>
) : null}
{listOpen ? (
<VideoListDialog
@@ -378,9 +382,12 @@ export function VideoBubble({ post }: { post: Post }) {
<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 px-4 pt-3 text-[14px] leading-6 text-neutral-100">
<CollapsibleText
wrapperClassName="px-4 pt-3"
className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-6 text-neutral-100"
>
{autolink(text)}
</div>
</CollapsibleText>
) : null}
</div>
);