Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/collapse-long-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Collapse very long messages behind an expand/collapse toggle so they no longer dominate the timeline.
17 changes: 10 additions & 7 deletions src/app/components/RenderMessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
UploadedSableCssContent,
VideoContent,
} from './message';
import { LongMessageCollapse } from './message/LongMessageCollapse';
import {
UrlPreviewCard,
UrlPreviewHolder,
Expand Down Expand Up @@ -162,13 +163,15 @@ function RenderMessageContentInternal({

const renderBody = useCallback(
(props: Record<string, unknown>) => (
<RenderBody
{...props}
body={props.body as string}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
<LongMessageCollapse>
<RenderBody
{...props}
body={props.body as string}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
</LongMessageCollapse>
),
[highlightRegex, htmlReactParserOptions, linkifyOpts]
);
Expand Down
61 changes: 61 additions & 0 deletions src/app/components/message/LongMessageCollapse.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<>
<div ref={bodyRef} style={collapsedStyle}>
{children}
</div>
{overflowing && (
<Box style={{ marginTop: config.space.S200 }}>
<Button
as="button"
type="button"
variant="Secondary"
fill="Soft"
size="300"
radii="300"
onClick={() => setExpanded((current) => !current)}
>
<Text size="B300">{expanded ? 'Show less' : 'Show more'}</Text>
</Button>
</Box>
)}
</>
);
}
2 changes: 2 additions & 0 deletions src/app/features/room/message/styles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const MessageBaseBubbleCollapsed = style({
paddingTop: 0,
});



export const MessageForceHover = style({
backgroundColor: `${color.Surface.ContainerHover} !important`,
});
Expand Down
2 changes: 1 addition & 1 deletion src/app/generated/tauri/commands.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/app/generated/tauri/events.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/app/generated/tauri/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/app/generated/tauri/types.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down