diff --git a/src/app/components/VoiceMessageRecorder.tsx b/src/app/components/VoiceMessageRecorder.tsx index 06ca5f74f..737abf9d1 100644 --- a/src/app/components/VoiceMessageRecorder.tsx +++ b/src/app/components/VoiceMessageRecorder.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Box, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds'; import { useSetting } from '../state/hooks/settings'; import { settingsAtom } from '../state/settings'; +import { MobileTouchTarget } from '../styles/mobile.css'; type RecorderState = 'idle' | 'recording' | 'paused' | 'preview'; @@ -239,6 +240,7 @@ export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) { if (state === 'idle') { return ( ( const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar'); const [composerToolbarButtons] = useSetting(settingsAtom, 'composerToolbarButtons'); - const touchTarget = mobileOrTablet() ? { minWidth: '44px', minHeight: '44px' } : undefined; + const isMobile = mobileOrTablet(); + // On phones the composer's secondary action buttons (attach, GIF, poll, + // location, voice, formatting, schedule) collapse behind a "+" toggle so the + // input stays one compact row instead of wrapping into a tall stack. Emoji + + // Send remain inline. Desktop keeps everything inline (isMobile === false). + const [mobileToolsOpen, setMobileToolsOpen] = useState(false); + const touchTarget = isMobile ? { minWidth: '44px', minHeight: '44px' } : undefined; const showFormat = composerToolbarButtons?.showFormat ?? true; const showEmoji = composerToolbarButtons?.showEmoji ?? true; const showSticker = composerToolbarButtons?.showSticker ?? true; @@ -876,6 +882,12 @@ export const RoomInput = forwardRef( ); } + // Mobile "+" overflow: the `after` builder stashes the collapsed secondary + // buttons here and the `bottom` slot renders them when the toggle is open. + // React evaluates JSX props in source order (before → after → bottom), so + // `after` assigns this before `bottom` reads it within the same render. + let composerOverflow: ReactNode = null; + return (
{selectedFiles.length > 0 && ( @@ -1035,16 +1047,31 @@ export const RoomInput = forwardRef( ) } before={ - pickFile('*')} - aria-label="Attach file" - variant="SurfaceVariant" - size="300" - radii="300" - style={touchTarget} - > - - + isMobile ? ( + setMobileToolsOpen((open) => !open)} + aria-label="More actions" + aria-expanded={mobileToolsOpen} + aria-controls="composer-more-actions" + variant="SurfaceVariant" + size="300" + radii="300" + style={touchTarget} + > + + + ) : ( + pickFile('*')} + aria-label="Attach file" + variant="SurfaceVariant" + size="300" + radii="300" + style={touchTarget} + > + + + ) } after={(() => { const formatButton = showFormat ? ( @@ -1306,9 +1333,37 @@ export const RoomInput = forwardRef( } }); + // Mobile: keep only emoji/sticker inline beside Send; the rest move + // into the "+" overflow row (rendered via `bottom`), led by the attach + // button that `before` gives up on mobile. Desktop renders all inline. + const emojiInline = orderedButtons.filter( + (node) => React.isValidElement(node) && node.key === 'showEmojiSticker', + ); + const overflowButtons = orderedButtons.filter( + (node) => !(React.isValidElement(node) && node.key === 'showEmojiSticker'), + ); + if (isMobile) { + composerOverflow = ( + <> + pickFile('*')} + aria-label="Attach file" + variant="SurfaceVariant" + size="300" + radii="300" + style={touchTarget} + > + + + {overflowButtons} + + ); + } + return ( <> - {orderedButtons} + {isMobile ? emojiInline : orderedButtons} {gifError && ( ( ); })()} bottom={ - toolbar && ( -
- - -
- ) + <> + {isMobile && mobileToolsOpen && composerOverflow && ( +
+ + + {composerOverflow} + +
+ )} + {toolbar && ( +
+ + +
+ )} + } /> {pollOpen && setPollOpen(false)} />}