A long reply quote stays inside its column #112
5 changed files with 92 additions and 14 deletions
|
|
@ -60,13 +60,13 @@ export const BubbleLayout = as<'div', BubbleLayoutProps>(
|
|||
<Box
|
||||
grow={own ? 'No' : 'Yes'}
|
||||
direction="Column"
|
||||
className={classNames(own && css.BubbleColumnOwn)}
|
||||
className={classNames(css.BubbleColumn, own && css.BubbleColumnOwn)}
|
||||
>
|
||||
{header && <div className={classNames(own && css.BubbleHeaderOwn)}>{header}</div>}
|
||||
{hideBubble ? (
|
||||
children
|
||||
) : (
|
||||
<Box>
|
||||
<Box className={css.BubbleContentRow}>
|
||||
<Box
|
||||
className={classNames(
|
||||
css.BubbleContent,
|
||||
|
|
|
|||
48
src/app/components/message/layout/bubbleBounds.test.ts
Normal file
48
src/app/components/message/layout/bubbleBounds.test.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { readFileSync } from 'fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import zlib from 'zlib';
|
||||
import { compile, processVanillaFile } from '@vanilla-extract/integration';
|
||||
|
||||
// A long reply quote (nowrap) sized the own-message row to 782px on a 390px phone. The row, the column and the bubble
|
||||
// must be able to shrink to their parent, so pin the emitted declarations (as phoneTapTargets.test.ts does).
|
||||
const emitted = async () => {
|
||||
const filePath = `${process.cwd()}/src/app/components/message/layout/layout.css.ts`;
|
||||
const { source } = await compile({ filePath, cwd: process.cwd(), identOption: 'debug' });
|
||||
const code = await processVanillaFile({ source, filePath, identOption: 'debug' });
|
||||
const bytes = Buffer.from(
|
||||
(code.match(/source=#?([A-Za-z0-9+/=]+)/) as RegExpMatchArray)[1],
|
||||
'base64'
|
||||
);
|
||||
return (bytes[0] === 0x1f && bytes[1] === 0x8b ? zlib.gunzipSync(bytes) : bytes).toString();
|
||||
};
|
||||
|
||||
const declarations = (css: string, className: string): string[] => {
|
||||
const rule = new RegExp(`^\\.layout_${className}__[\\w-]+ \\{\\n([^}]*)\\}`, 'm').exec(css);
|
||||
expect(rule, `no .layout_${className} rule emitted`).toBeTruthy();
|
||||
return (rule as RegExpExecArray)[1].split('\n').map((l) => l.trim().replace(/;$/, ''));
|
||||
};
|
||||
|
||||
describe('bubble rows shrink to their column', () => {
|
||||
it.each([
|
||||
['BubbleColumn', ['min-width: 0']],
|
||||
['BubbleContentRow', ['min-width: 0', 'max-width: 100%']],
|
||||
['BubbleContent', ['min-width: 0']],
|
||||
])('%s', async (name, expected) => {
|
||||
const css = await emitted();
|
||||
expect(declarations(css, name)).toEqual(expect.arrayContaining(expected));
|
||||
});
|
||||
|
||||
it('BubbleContent keeps its 800px cap inside the column width', async () => {
|
||||
const css = await emitted();
|
||||
expect(declarations(css, 'BubbleContent').join('\n')).toMatch(/max-width: min\(100%, 50rem\)/);
|
||||
});
|
||||
|
||||
it('Bubble.tsx puts BubbleColumn on the column and BubbleContentRow on the row', () => {
|
||||
const tsx = readFileSync(
|
||||
`${process.cwd()}/src/app/components/message/layout/Bubble.tsx`,
|
||||
'utf8'
|
||||
);
|
||||
expect(tsx).toMatch(/classNames\(css\.BubbleColumn,/);
|
||||
expect(tsx).toMatch(/<Box className=\{css\.BubbleContentRow\}>/);
|
||||
});
|
||||
});
|
||||
|
|
@ -139,8 +139,22 @@ export const BubbleBefore = style({
|
|||
minWidth: toRem(36),
|
||||
});
|
||||
|
||||
// minWidth 0 down the column: a flex item never shrinks below its content (a reply quote's unbroken text),
|
||||
// so without it a long quote pushes the column past the row, and a right-aligned row pushes it off the left edge.
|
||||
export const BubbleColumn = style({
|
||||
minWidth: 0,
|
||||
});
|
||||
|
||||
// The row around a bubble sizes to its content in a right-aligned (own) column, and a quote's nowrap text
|
||||
// makes that content as wide as the quote: cap the row at the column so the bubble can shrink inside it.
|
||||
export const BubbleContentRow = style({
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
});
|
||||
|
||||
export const BubbleContent = style({
|
||||
maxWidth: toRem(800),
|
||||
minWidth: 0,
|
||||
maxWidth: `min(100%, ${toRem(800)})`,
|
||||
padding: config.space.S200,
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ export type DemoLine = {
|
|||
body: string;
|
||||
html?: string;
|
||||
pose?: string;
|
||||
// Index of an earlier line in the same room this one replies to.
|
||||
replyTo?: number;
|
||||
minutesAgo: number;
|
||||
};
|
||||
export type DemoRoom = {
|
||||
|
|
@ -116,6 +118,14 @@ export const ROOMS: DemoRoom[] = [
|
|||
},
|
||||
// A long display name: the name pill has to show all of it on a phone.
|
||||
{ from: user('heimdall'), body: 'Ok.', pose: 'thinking', minutesAgo: 20 },
|
||||
// A long message to reply to: the quote above each reply must stay inside its column on a phone.
|
||||
{
|
||||
from: user('visitor'),
|
||||
body: "The read exists from 2026-11-04. It's research only, nothing is wired up yet, so please do not treat the notes_in_a_single_unbroken_token_that_keeps_going_past_the_edge as a decision.",
|
||||
minutesAgo: 12,
|
||||
},
|
||||
{ from: ME, body: 'Noted, thank you.', replyTo: 8, minutesAgo: 11 },
|
||||
{ from: user('void'), body: 'Same here.', replyTo: 8, minutesAgo: 10 },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -145,21 +145,27 @@ export const createDemoServer = (now: () => number = Date.now): DemoServer => {
|
|||
|
||||
ROOMS.forEach((room) => {
|
||||
stateOf.set(room.id, buildState(room));
|
||||
const lineIds: string[] = [];
|
||||
room.lines.forEach((l) => {
|
||||
const event = mk(
|
||||
'm.room.message',
|
||||
l.from,
|
||||
{
|
||||
msgtype: 'm.text',
|
||||
body: l.body,
|
||||
...(l.html ? { format: 'org.matrix.custom.html', formatted_body: l.html } : {}),
|
||||
...(l.pose ? { [POSE_KEY]: l.pose } : {}),
|
||||
...(l.replyTo === undefined
|
||||
? {}
|
||||
: { 'm.relates_to': { 'm.in_reply_to': { event_id: lineIds[l.replyTo] } } }),
|
||||
},
|
||||
now() - l.minutesAgo * 60 * 1000
|
||||
);
|
||||
lineIds.push(event.event_id);
|
||||
timeline.push({
|
||||
version: 1,
|
||||
roomId: room.id,
|
||||
event: mk(
|
||||
'm.room.message',
|
||||
l.from,
|
||||
{
|
||||
msgtype: 'm.text',
|
||||
body: l.body,
|
||||
...(l.html ? { format: 'org.matrix.custom.html', formatted_body: l.html } : {}),
|
||||
...(l.pose ? { [POSE_KEY]: l.pose } : {}),
|
||||
},
|
||||
now() - l.minutesAgo * 60 * 1000
|
||||
),
|
||||
event,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue