Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 70f1d640ed fix(messages): collapsed long messages work with the keyboard and on any background (#343)
CI / Build & Quality Checks (pull_request) Successful in 3m4s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m15s
- Tab into a link in the hidden part used to scroll the overflow:hidden box
  itself (top cut off, and it stayed that way); focus inside now expands
  the message, and collapsing resets the scroll.
- "Read more" / "Show less" is announced as a toggle (aria-expanded,
  aria-controls); the arrows are aria-hidden.
- The fade is a mask on the content instead of a gradient in a fixed
  surface colour, so there's no band in bubble layout, on highlights or
  over chat backgrounds.
- The cap is 20rem instead of 320px, so a larger font shows as many lines.

Verified headless on a 41-line message with a link on the last line:
aria-expanded false → focusing the link expands it (aria-expanded true,
no clipping); bubble layout fades into the bubble with no band.

Fixes #343

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-12 15:31:50 -04:00
jared e57af60466 Merge pull request 'fix(presence): leaving Do Not Disturb restores your status instead of "dnd" (#283)' (#355) from fix/dnd-restore-status into lotus
CI / Build & Quality Checks (push) Successful in 3m28s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 8s
CI / Trigger Desktop Build (push) Successful in 4s
CI / Playwright smoke (e2e) (push) Successful in 13m55s
2026-10-12 13:12:50 -04:00
jared 1f10866b60 Merge pull request 'fix(presence): Invisible, Hide Online Status, Idle and DND stick (#282)' (#354) from fix/presence-sticky into lotus
CI / Build & Quality Checks (push) Successful in 3m8s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 5s
CI / Playwright smoke (e2e) (push) Canceled after 14s
2026-10-12 12:53:55 -04:00
+32 -19
View File
@@ -1,4 +1,4 @@
import React, { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
import React, { CSSProperties, ReactNode, useEffect, useId, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Button, config, Icon, Icons, Text, color, toRem } from 'folds';
import { IContent, MatrixEvent } from 'matrix-js-sdk';
@@ -41,7 +41,10 @@ import { formatAccuracy } from '../../utils/geoShare';
import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment';
import { FileHeader, FileDownloadButton } from './FileHeader';
const COLLAPSE_MAX_HEIGHT = 320; // px ≈ 20 lines
// ≈ 20 lines; in rem so a larger font setting doesn't show fewer lines (Gitea #343).
const COLLAPSE_MAX_HEIGHT_REM = 20;
const collapseMaxHeightPx = (): number =>
COLLAPSE_MAX_HEIGHT_REM * (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16);
type CollapsibleBodyProps = {
eventId?: string;
@@ -79,7 +82,7 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
const el = bodyRef.current;
if (!el) return undefined;
const observer = new ResizeObserver(() => {
setNeedsCollapse(el.scrollHeight > COLLAPSE_MAX_HEIGHT);
setNeedsCollapse(el.scrollHeight > collapseMaxHeightPx());
});
observer.observe(el);
return () => observer.disconnect();
@@ -89,16 +92,36 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
// (Gitea #85); useReducedMotion subscribes to the change event instead.
const prefersReducedMotion = useReducedMotion();
const bodyId = useId();
const isClipped = needsCollapse && collapsed;
// [Gitea #343] The clipped part stays in the DOM, so Tab can reach a link in
// it; the browser would then scroll the overflow:hidden box itself (top cut
// off, and it stayed that way). Expand instead when focus lands inside.
const handleFocus = () => {
if (isClipped) setCollapsed(false);
};
useEffect(() => {
if (isClipped && bodyRef.current) bodyRef.current.scrollTop = 0;
}, [isClipped]);
// Fade the content itself rather than painting a fixed-colour gradient over
// it, so it blends on any background (bubbles, highlights, chat backgrounds).
const fadeMask = 'linear-gradient(#000 calc(100% - 3rem), transparent)';
return (
<div>
<div
ref={bodyRef}
id={bodyId}
onFocus={handleFocus}
style={{
position: 'relative',
...(needsCollapse && collapsed
...(isClipped
? {
maxHeight: `${COLLAPSE_MAX_HEIGHT}px`,
maxHeight: `${COLLAPSE_MAX_HEIGHT_REM}rem`,
overflow: 'hidden',
maskImage: fadeMask,
WebkitMaskImage: fadeMask,
transition: prefersReducedMotion ? undefined : 'max-height 0.2s ease',
}
: {
@@ -107,23 +130,12 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
}}
>
{children}
{needsCollapse && collapsed && (
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '3rem',
background: `linear-gradient(transparent, ${color.Surface.Container})`,
pointerEvents: 'none',
}}
/>
)}
</div>
{needsCollapse && (
<button
type="button"
aria-expanded={!collapsed}
aria-controls={bodyId}
onClick={() => setCollapsed((c) => !c)}
style={{
cursor: 'pointer',
@@ -134,7 +146,8 @@ function CollapsibleBody({ eventId, children }: CollapsibleBodyProps) {
}}
>
<Text as="span" size="T200" style={{ color: color.Primary.Main }}>
{collapsed ? 'Read more ↓' : 'Show less ↑'}
{collapsed ? 'Read more' : 'Show less'}
<span aria-hidden="true">{collapsed ? ' ↓' : ' ↑'}</span>
</Text>
</button>
)}