From 8b1c9fa6108b241eb8369f6f717d1b438dedc6d6 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 19 Sep 2026 13:05:10 -0400 Subject: [PATCH] fix(threads): thread panel no longer squeezes the composers (#218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/room/Room.tsx | 9 +++++++- src/app/features/room/RoomInput.tsx | 15 ++++++++++-- src/app/features/room/thread/ThreadPanel.tsx | 1 + src/app/hooks/useMediaQuery.ts | 24 ++++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/app/hooks/useMediaQuery.ts diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx index e91a84bad..2526e5119 100644 --- a/src/app/features/room/Room.tsx +++ b/src/app/features/room/Room.tsx @@ -11,6 +11,7 @@ import { WidgetsPanel } from './widgets/WidgetsPanel'; import { widgetsPanelAtom } from '../../state/widgetsPanel'; import { mobileMembersPanelAtom } from '../../state/mobileMembersPanel'; import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; +import { useMediaQuery } from '../../hooks/useMediaQuery'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels'; @@ -150,10 +151,16 @@ export function Room() { (isDesktop || (!galleryOpen && !widgetsOpen)); // Desktop: the persisted drawer preference. Mobile: a transient panel the // 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 = !callView && (isDesktop - ? isDrawer + ? isDrawer && !(narrowDesktop && contentPanelOpen) : mobileMembersOpen && !activeThreadId && !galleryOpen && !widgetsOpen && !threadsListOpen); // Leaving a room on a phone closes its member panel. diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 667aaf12f..5c9a49d72 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -169,10 +169,21 @@ interface RoomInputProps { // message" handler). Threads pass a distinct name so the main timeline's handler // doesn't fire for the thread composer, and vice-versa. 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( ( - { editor, fileDropContainerRef, roomId, room, threadRootId, editableName = 'RoomInput' }, + { + editor, + fileDropContainerRef, + roomId, + room, + threadRootId, + editableName = 'RoomInput', + compactLayout = false, + }, ref, ) => { const mx = useMatrixClient(); @@ -254,7 +265,7 @@ export const RoomInput = forwardRef( // ten controls inline and clips the Send button behind the editor's // overflow:hidden. Touch-sized (44px) targets stay UA-gated: a narrow // desktop window still has a mouse. - const compact = isMobile || screenSize === ScreenSize.Mobile; + const compact = isMobile || screenSize === ScreenSize.Mobile || compactLayout; const [mobileToolsOpen, setMobileToolsOpen] = useState(false); // Both gates on purpose: the class covers phone-width viewports (matches // VoiceMessageRecorder's idle button, which lives in the same row), the diff --git a/src/app/features/room/thread/ThreadPanel.tsx b/src/app/features/room/thread/ThreadPanel.tsx index 85177a93d..be37f7497 100644 --- a/src/app/features/room/thread/ThreadPanel.tsx +++ b/src/app/features/room/thread/ThreadPanel.tsx @@ -200,6 +200,7 @@ export function ThreadPanel({ room, threadId, requestClose }: ThreadPanelProps) editor={editor} editableName="ThreadInput" fileDropContainerRef={fileDropContainerRef} + compactLayout /> diff --git a/src/app/hooks/useMediaQuery.ts b/src/app/hooks/useMediaQuery.ts new file mode 100644 index 000000000..46cc2ee89 --- /dev/null +++ b/src/app/hooks/useMediaQuery.ts @@ -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(() => 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; +}