diff --git a/.changeset/collapse-long-messages.md b/.changeset/collapse-long-messages.md new file mode 100644 index 000000000..8c9a5cc59 --- /dev/null +++ b/.changeset/collapse-long-messages.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Collapse very long messages behind an expand/collapse toggle so they no longer dominate the timeline. diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx index d8da91dd7..c1aa79d20 100644 --- a/src/app/components/RenderMessageContent.tsx +++ b/src/app/components/RenderMessageContent.tsx @@ -34,6 +34,7 @@ import { UploadedSableCssContent, VideoContent, } from './message'; +import { LongMessageCollapse } from './message/LongMessageCollapse'; import { UrlPreviewCard, UrlPreviewHolder, @@ -162,13 +163,15 @@ function RenderMessageContentInternal({ const renderBody = useCallback( (props: Record) => ( - + + + ), [highlightRegex, htmlReactParserOptions, linkifyOpts] ); diff --git a/src/app/components/message/LongMessageCollapse.tsx b/src/app/components/message/LongMessageCollapse.tsx new file mode 100644 index 000000000..4e5aa203b --- /dev/null +++ b/src/app/components/message/LongMessageCollapse.tsx @@ -0,0 +1,61 @@ +import { useLayoutEffect, useRef, useState } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import { Box, Button, Text, config } from 'folds'; + +const LONG_MESSAGE_LINE_LIMIT = 30; + +type LongMessageCollapseProps = { + children: ReactNode; +}; + +export function LongMessageCollapse({ children }: LongMessageCollapseProps) { + const [expanded, setExpanded] = useState(false); + const [overflowing, setOverflowing] = useState(false); + const bodyRef = useRef(null); + + useLayoutEffect(() => { + const body = bodyRef.current; + if (!body) return; + const lineHeight = parseFloat(getComputedStyle(body).lineHeight) || 20; + // In the clamped (-webkit-box) state scrollHeight holds the unclamped + // height; in the inline-block state clientHeight is the full height. + const height = Math.max(body.scrollHeight, body.clientHeight); + setOverflowing(height / lineHeight > LONG_MESSAGE_LINE_LIMIT + 0.5); + }, [children]); + + // A block wrapper would push inline siblings (like the "(edited)" indicator) + // onto a new line, so keep the body inline-block and only switch to the + // line-clamped box once it is known to overflow. + const collapsedStyle: CSSProperties | undefined = + expanded || !overflowing + ? { display: 'inline-block' } + : { + display: '-webkit-box', + WebkitLineClamp: LONG_MESSAGE_LINE_LIMIT, + WebkitBoxOrient: 'vertical', + overflow: 'hidden', + }; + + return ( + <> +
+ {children} +
+ {overflowing && ( + + + + )} + + ); +} diff --git a/src/app/features/room/message/styles.css.ts b/src/app/features/room/message/styles.css.ts index 00b92a31e..3482cbbcb 100644 --- a/src/app/features/room/message/styles.css.ts +++ b/src/app/features/room/message/styles.css.ts @@ -9,6 +9,8 @@ export const MessageBaseBubbleCollapsed = style({ paddingTop: 0, }); + + export const MessageForceHover = style({ backgroundColor: `${color.Surface.ContainerHover} !important`, }); diff --git a/src/app/generated/tauri/commands.ts b/src/app/generated/tauri/commands.ts index a4a1d8bfe..ff06a362c 100644 --- a/src/app/generated/tauri/commands.ts +++ b/src/app/generated/tauri/commands.ts @@ -1,7 +1,7 @@ /** * Auto-generated TypeScript bindings for Tauri commands * Generated by tauri-typegen v0.5.0 - * Generated at: 2026-08-22T11:19:48.664987491+00:00 + * Generated at: 2026-08-26T20:35:48.860009944+00:00 * Generator: none * * Do not edit manually - regenerate using: cargo tauri-typegen generate diff --git a/src/app/generated/tauri/events.ts b/src/app/generated/tauri/events.ts index 2b740560b..1132716a4 100644 --- a/src/app/generated/tauri/events.ts +++ b/src/app/generated/tauri/events.ts @@ -1,7 +1,7 @@ /** * Auto-generated TypeScript bindings for Tauri commands * Generated by tauri-typegen v0.5.0 - * Generated at: 2026-08-21T15:51:24.043135712+00:00 + * Generated at: 2026-08-26T20:35:48.861831873+00:00 * Generator: none * * Do not edit manually - regenerate using: cargo tauri-typegen generate diff --git a/src/app/generated/tauri/index.ts b/src/app/generated/tauri/index.ts index d19ec957f..3d12c2ebc 100644 --- a/src/app/generated/tauri/index.ts +++ b/src/app/generated/tauri/index.ts @@ -1,7 +1,7 @@ /** * Auto-generated TypeScript bindings for Tauri commands * Generated by tauri-typegen v0.5.0 - * Generated at: 2026-08-21T15:51:24.043195492+00:00 + * Generated at: 2026-08-26T20:35:48.862437959+00:00 * Generator: none * * Do not edit manually - regenerate using: cargo tauri-typegen generate diff --git a/src/app/generated/tauri/types.ts b/src/app/generated/tauri/types.ts index 6bd95705b..627254961 100644 --- a/src/app/generated/tauri/types.ts +++ b/src/app/generated/tauri/types.ts @@ -1,7 +1,7 @@ /** * Auto-generated TypeScript bindings for Tauri commands * Generated by tauri-typegen v0.5.0 - * Generated at: 2026-08-21T15:51:24.041153277+00:00 + * Generated at: 2026-08-26T20:35:48.853902144+00:00 * Generator: none * * Do not edit manually - regenerate using: cargo tauri-typegen generate