fix(threads): thread panel no longer squeezes the composers (#218)
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
Two causes. (1) Three right-hand columns don't fit under ~1500 px: the member
drawer now yields to an open content panel (thread / gallery / widgets /
threads list) on desktops ≤ 1500 px and returns when it closes, like Element.
(2) The thread composer rendered the full ten-button toolbar inside a 360 px
panel, leaving 24 px for the input ('S…') at every width — RoomInput gains a
compactLayout prop (the phone one-row '+ | input | emoji | send' layout) and
ThreadPanel uses it.
Measured headless: 1400 px thread open → drawer hidden, composers 328/168 px
(was 422 → 'Send a mes…' + 24 px); 1600 px → drawer stays, thread composer 168 px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -11,6 +11,7 @@ import { WidgetsPanel } from './widgets/WidgetsPanel';
|
|||||||
import { widgetsPanelAtom } from '../../state/widgetsPanel';
|
import { widgetsPanelAtom } from '../../state/widgetsPanel';
|
||||||
import { mobileMembersPanelAtom } from '../../state/mobileMembersPanel';
|
import { mobileMembersPanelAtom } from '../../state/mobileMembersPanel';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
|
import { useMediaQuery } from '../../hooks/useMediaQuery';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels';
|
import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels';
|
||||||
@@ -150,10 +151,16 @@ export function Room() {
|
|||||||
(isDesktop || (!galleryOpen && !widgetsOpen));
|
(isDesktop || (!galleryOpen && !widgetsOpen));
|
||||||
// Desktop: the persisted drawer preference. Mobile: a transient panel the
|
// Desktop: the persisted drawer preference. Mobile: a transient panel the
|
||||||
// user opened from the header menu (never the desktop default).
|
// user opened from the header menu (never the desktop default).
|
||||||
|
// [Gitea #218] Three right-hand columns (timeline + thread/gallery/widgets
|
||||||
|
// panel + member drawer) don't fit under ~1500 px — both composers shrank to
|
||||||
|
// a few characters. Like Element, the member drawer yields to the content
|
||||||
|
// panel on narrower desktops and comes back when it closes.
|
||||||
|
const narrowDesktop = useMediaQuery('(max-width: 1500px)');
|
||||||
|
const contentPanelOpen = showThreadPanel || showGallery || showWidgets || showThreadsList;
|
||||||
const showMembers =
|
const showMembers =
|
||||||
!callView &&
|
!callView &&
|
||||||
(isDesktop
|
(isDesktop
|
||||||
? isDrawer
|
? isDrawer && !(narrowDesktop && contentPanelOpen)
|
||||||
: mobileMembersOpen && !activeThreadId && !galleryOpen && !widgetsOpen && !threadsListOpen);
|
: mobileMembersOpen && !activeThreadId && !galleryOpen && !widgetsOpen && !threadsListOpen);
|
||||||
|
|
||||||
// Leaving a room on a phone closes its member panel.
|
// Leaving a room on a phone closes its member panel.
|
||||||
|
|||||||
@@ -169,10 +169,21 @@ interface RoomInputProps {
|
|||||||
// message" handler). Threads pass a distinct name so the main timeline's handler
|
// message" handler). Threads pass a distinct name so the main timeline's handler
|
||||||
// doesn't fire for the thread composer, and vice-versa.
|
// doesn't fire for the thread composer, and vice-versa.
|
||||||
editableName?: string;
|
editableName?: string;
|
||||||
|
// [Gitea #218] Force the one-row "+ | input | emoji | send" layout regardless
|
||||||
|
// of viewport — the 360 px thread panel can't fit the full toolbar.
|
||||||
|
compactLayout?: boolean;
|
||||||
}
|
}
|
||||||
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
(
|
(
|
||||||
{ editor, fileDropContainerRef, roomId, room, threadRootId, editableName = 'RoomInput' },
|
{
|
||||||
|
editor,
|
||||||
|
fileDropContainerRef,
|
||||||
|
roomId,
|
||||||
|
room,
|
||||||
|
threadRootId,
|
||||||
|
editableName = 'RoomInput',
|
||||||
|
compactLayout = false,
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
@@ -254,7 +265,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||||||
// ten controls inline and clips the Send button behind the editor's
|
// ten controls inline and clips the Send button behind the editor's
|
||||||
// overflow:hidden. Touch-sized (44px) targets stay UA-gated: a narrow
|
// overflow:hidden. Touch-sized (44px) targets stay UA-gated: a narrow
|
||||||
// desktop window still has a mouse.
|
// desktop window still has a mouse.
|
||||||
const compact = isMobile || screenSize === ScreenSize.Mobile;
|
const compact = isMobile || screenSize === ScreenSize.Mobile || compactLayout;
|
||||||
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
|
const [mobileToolsOpen, setMobileToolsOpen] = useState(false);
|
||||||
// Both gates on purpose: the class covers phone-width viewports (matches
|
// Both gates on purpose: the class covers phone-width viewports (matches
|
||||||
// VoiceMessageRecorder's idle button, which lives in the same row), the
|
// VoiceMessageRecorder's idle button, which lives in the same row), the
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ export function ThreadPanel({ room, threadId, requestClose }: ThreadPanelProps)
|
|||||||
editor={editor}
|
editor={editor}
|
||||||
editableName="ThreadInput"
|
editableName="ThreadInput"
|
||||||
fileDropContainerRef={fileDropContainerRef}
|
fileDropContainerRef={fileDropContainerRef}
|
||||||
|
compactLayout
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const read = (query: string): boolean =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
typeof window.matchMedia === 'function' &&
|
||||||
|
window.matchMedia(query).matches;
|
||||||
|
|
||||||
|
/** Reactive `window.matchMedia(query).matches`. SSR-safe (false). */
|
||||||
|
export function useMediaQuery(query: string): boolean {
|
||||||
|
const [matches, setMatches] = useState<boolean>(() => read(query));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const mql = window.matchMedia(query);
|
||||||
|
const onChange = (event: MediaQueryListEvent) => setMatches(event.matches);
|
||||||
|
setMatches(mql.matches);
|
||||||
|
mql.addEventListener('change', onChange);
|
||||||
|
return () => mql.removeEventListener('change', onChange);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user