Compare commits
1
Commits
61dfdea9e9
...
470b5217ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470b5217ae |
@@ -248,7 +248,7 @@ export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) {
|
||||
radii="300"
|
||||
title="Record voice message"
|
||||
>
|
||||
<Icon src={Icons.Mic} size="100" />
|
||||
<Icon src={Icons.Mic} />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,21 +16,16 @@ export const EditorOptions = style([
|
||||
DefaultReset,
|
||||
{
|
||||
padding: config.space.S200,
|
||||
'@media': {
|
||||
// On phones the toolbar can hold many 44px buttons; let them wrap to a
|
||||
// second line instead of overflowing horizontally.
|
||||
'(max-width: 750px)': { flexWrap: 'wrap' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// The composer's before | editable | after row. On phones, allow the toolbar
|
||||
// (`after`) to wrap below the input instead of squeezing the editable to zero
|
||||
// and pushing the Send button off-screen.
|
||||
// The composer's before | editable | after row. It must NOT wrap: folds'
|
||||
// Scroll (the editable's wrapper) is `width: 100%`, so a wrapping row always
|
||||
// breaks into three stacked lines (before / editable / after) — the "wonky"
|
||||
// phone composer. Narrow viewports keep one row by collapsing the secondary
|
||||
// buttons behind the "+" overflow instead (RoomInput `compact`).
|
||||
export const EditorInputRow = style({
|
||||
'@media': {
|
||||
'(max-width: 750px)': { flexWrap: 'wrap' },
|
||||
},
|
||||
minWidth: 0,
|
||||
});
|
||||
|
||||
export const EditorTextareaScroll = style({});
|
||||
@@ -41,6 +36,20 @@ export const EditorTextarea = style([
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
padding: `${toRem(13)} ${toRem(1)}`,
|
||||
'@media': {
|
||||
// Phone-width composer rows carry 44px touch targets (MobileTouchTarget),
|
||||
// so the row is 60px instead of 48px; pad the text to keep it level with
|
||||
// the buttons instead of hugging the top of the row. Only when the row
|
||||
// actually has before/after buttons (not the edit-message editor).
|
||||
'(max-width: 750px)': {
|
||||
selectors: {
|
||||
[`${EditorTextareaScroll}:not(:only-child) &`]: {
|
||||
paddingTop: toRem(19),
|
||||
paddingBottom: toRem(19),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
[`${EditorTextareaScroll}:first-child &`]: {
|
||||
paddingLeft: toRem(13),
|
||||
@@ -70,6 +79,13 @@ export const EditorPlaceholderTextVisual = style([
|
||||
display: 'block',
|
||||
paddingTop: toRem(13),
|
||||
paddingLeft: toRem(1),
|
||||
'@media': {
|
||||
'(max-width: 750px)': {
|
||||
selectors: {
|
||||
[`${EditorTextareaScroll}:not(:only-child) &`]: { paddingTop: toRem(19) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -129,6 +129,8 @@ import { useTheme } from '../../hooks/useTheme';
|
||||
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
||||
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||
import { useComposingCheck } from '../../hooks/useComposingCheck';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
import { VoiceMessageRecorder } from '../../components/VoiceMessageRecorder';
|
||||
import { PollCreator } from './PollCreator';
|
||||
import { useRoomUnverifiedDeviceCount } from '../../hooks/useDeviceVerificationStatus';
|
||||
@@ -235,11 +237,20 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar');
|
||||
const [composerToolbarButtons] = useSetting(settingsAtom, 'composerToolbarButtons');
|
||||
const isMobile = mobileOrTablet();
|
||||
// On phones the composer's secondary action buttons (attach, GIF, poll,
|
||||
const screenSize = useScreenSizeContext();
|
||||
// Compact composer: the 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).
|
||||
// input stays ONE row — [ + | input | emoji | send ]. Keyed on the viewport,
|
||||
// not only the user agent: a phone-width window on a desktop UA (iPad in
|
||||
// desktop mode, a split-screen PWA, a docked window) otherwise renders all
|
||||
// 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 [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
|
||||
// inline style covers touch UAs with a wider viewport (tablet landscape).
|
||||
const touchTarget = isMobile ? { minWidth: '44px', minHeight: '44px' } : undefined;
|
||||
const showFormat = composerToolbarButtons?.showFormat ?? true;
|
||||
const showEmoji = composerToolbarButtons?.showEmoji ?? true;
|
||||
@@ -1140,7 +1151,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
)
|
||||
}
|
||||
before={
|
||||
isMobile ? (
|
||||
compact ? (
|
||||
<IconButton
|
||||
onClick={() => setMobileToolsOpen((open) => !open)}
|
||||
aria-label="More actions"
|
||||
@@ -1150,6 +1161,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
size="300"
|
||||
radii="300"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
>
|
||||
<Icon src={mobileToolsOpen ? Icons.Cross : Icons.Plus} />
|
||||
</IconButton>
|
||||
@@ -1161,6 +1173,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
size="300"
|
||||
radii="300"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
>
|
||||
<Icon src={Icons.PlusCircle} />
|
||||
</IconButton>
|
||||
@@ -1174,6 +1187,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
size="300"
|
||||
radii="300"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
aria-label={toolbar ? 'Hide formatting toolbar' : 'Show formatting toolbar'}
|
||||
aria-pressed={toolbar}
|
||||
onClick={() => setToolbar(!toolbar)}
|
||||
@@ -1200,6 +1214,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
size="300"
|
||||
radii="300"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
>
|
||||
<Icon
|
||||
src={Icons.Sticker}
|
||||
@@ -1220,6 +1235,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
size="300"
|
||||
radii="300"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
>
|
||||
<Icon
|
||||
src={Icons.Smile}
|
||||
@@ -1307,22 +1323,32 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
radii="300"
|
||||
disabled={gifUploading}
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
>
|
||||
{gifUploading ? (
|
||||
<Spinner variant="Secondary" size="100" />
|
||||
) : (
|
||||
<Text
|
||||
size="T200"
|
||||
style={{
|
||||
fontWeight: 800,
|
||||
fontSize: '11px',
|
||||
letterSpacing: '0.04em',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
GIF
|
||||
</Text>
|
||||
)}
|
||||
{/* Sized like a default Icon (1.5rem) so the button
|
||||
matches its 32px neighbours instead of a 28×19 stub. */}
|
||||
<Box
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
style={{ width: '1.5rem', height: '1.5rem' }}
|
||||
>
|
||||
{gifUploading ? (
|
||||
<Spinner variant="Secondary" size="100" />
|
||||
) : (
|
||||
<Text
|
||||
as="span"
|
||||
size="T200"
|
||||
style={{
|
||||
fontWeight: 800,
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.04em',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
GIF
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</IconButton>
|
||||
</PopOut>
|
||||
)}
|
||||
@@ -1340,11 +1366,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
radii="300"
|
||||
title="Share location"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
>
|
||||
{locating ? (
|
||||
<Spinner variant="Secondary" size="100" />
|
||||
<Spinner variant="Secondary" size="400" />
|
||||
) : (
|
||||
<Icon src={Icons.SpaceGlobe} size="100" />
|
||||
<Icon src={Icons.SpaceGlobe} />
|
||||
)}
|
||||
</IconButton>
|
||||
) : null;
|
||||
@@ -1359,8 +1386,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
radii="300"
|
||||
title="Create poll"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
>
|
||||
<Icon src={Icons.OrderList} size="100" />
|
||||
<Icon src={Icons.OrderList} />
|
||||
</IconButton>
|
||||
) : null;
|
||||
|
||||
@@ -1383,10 +1411,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
size="300"
|
||||
radii="300"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
aria-label="Schedule message"
|
||||
title="Schedule message"
|
||||
>
|
||||
<Icon src={Icons.Clock} size="100" />
|
||||
<Icon src={Icons.Clock} />
|
||||
</IconButton>
|
||||
) : null;
|
||||
|
||||
@@ -1426,16 +1455,18 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
}
|
||||
});
|
||||
|
||||
// Mobile: keep only emoji/sticker inline beside Send; the rest move
|
||||
// Compact: 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.
|
||||
// button that `before` gives up on, and trailed by the draft label so
|
||||
// the inline row stays [ emoji | count | send ]. Wide viewports render
|
||||
// everything 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) {
|
||||
if (compact) {
|
||||
composerOverflow = (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -1446,17 +1477,19 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
size="300"
|
||||
radii="300"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
>
|
||||
<Icon src={Icons.PlusCircle} />
|
||||
</IconButton>
|
||||
{overflowButtons}
|
||||
<DraftIndicator roomId={draftKey} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMobile ? emojiInline : orderedButtons}
|
||||
{compact ? emojiInline : orderedButtons}
|
||||
{gifError && (
|
||||
<Text
|
||||
size="T200"
|
||||
@@ -1483,7 +1516,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
{locationError}
|
||||
</Text>
|
||||
)}
|
||||
<DraftIndicator roomId={draftKey} />
|
||||
{!compact && <DraftIndicator roomId={draftKey} />}
|
||||
{charCount > 0 && (
|
||||
<Text
|
||||
size="T200"
|
||||
@@ -1505,6 +1538,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
size="300"
|
||||
radii="300"
|
||||
style={touchTarget}
|
||||
className={MobileTouchTarget}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<Icon src={Icons.Send} />
|
||||
@@ -1514,7 +1548,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
})()}
|
||||
bottom={
|
||||
<>
|
||||
{isMobile && mobileToolsOpen && composerOverflow && (
|
||||
{compact && mobileToolsOpen && composerOverflow && (
|
||||
<div>
|
||||
<Line variant="SurfaceVariant" size="300" />
|
||||
<Box
|
||||
|
||||
Reference in New Issue
Block a user