From 5bd1716c9405a90b96771dd477b7afc08051e73d Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Tue, 18 Aug 2026 18:36:49 -0600 Subject: [PATCH] Split the composer's attach button into Photo/video and File (#29) On mobile, a file input with no accept hint (needed to allow arbitrary file attachments) makes some Android browsers fall back to a generic chooser -- Camera, Camera Video, Files -- with no direct Photos/Gallery shortcut, confirmed via a screenshot showing exactly that. Android can't reliably offer both "any file type" and a gallery shortcut from a single input, so the attach button now opens a small menu: "Photo or video" uses a new input with accept="image/*,video/*" (should surface the OS media picker's gallery shortcut), "File" keeps today's unrestricted picker. Upload routing (handleFile) is unchanged either way. Co-Authored-By: Claude Sonnet 5 --- frontend/src/components/Composer.css | 41 +++++++++++ frontend/src/components/Composer.tsx | 105 +++++++++++++++++++++------ 2 files changed, 124 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/Composer.css b/frontend/src/components/Composer.css index cff7804..1683f75 100644 --- a/frontend/src/components/Composer.css +++ b/frontend/src/components/Composer.css @@ -105,6 +105,11 @@ cursor: not-allowed; } +.composer-attach-wrap { + position: relative; + flex: none; +} + .composer-attach { width: 36px; height: 36px; @@ -119,6 +124,42 @@ cursor: pointer; } +.composer-attach-menu-scrim { + position: fixed; + inset: 0; + z-index: 30; +} + +.composer-attach-menu { + position: absolute; + bottom: calc(100% + 8px); + left: 0; + z-index: 31; + background: var(--card-bg); + border: 1px solid var(--ds-border); + border-radius: var(--radius); + padding: var(--sp-2); + min-width: 160px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.composer-attach-menu button[role='menuitem'] { + background: transparent; + border: none; + color: var(--ds-text); + text-align: left; + padding: 8px; + border-radius: 6px; + font-size: 0.86rem; + cursor: pointer; +} + +.composer-attach-menu button[role='menuitem']:hover { + background: var(--ds-surface-2); +} + .composer-attach:hover:not(:disabled) { color: var(--ds-text); border-color: var(--ds-accent); diff --git a/frontend/src/components/Composer.tsx b/frontend/src/components/Composer.tsx index a857621..81c92a5 100644 --- a/frontend/src/components/Composer.tsx +++ b/frontend/src/components/Composer.tsx @@ -8,6 +8,7 @@ import { type FormEvent, type KeyboardEvent, } from 'react' +import { useEscapeKey } from '../hooks/useEscapeKey' import { useOnlineStatus } from '../hooks/useOnlineStatus' import { uploadRoomFile, uploadRoomImage } from '../api/rooms' import { getUploadLimit } from '../api/uploads' @@ -61,6 +62,33 @@ function detectRoomReferenceQuery(text: string, cursor: number): TriggerQuery | return detectTriggerQuery(text, cursor, '#') } +interface AttachMenuProps { + onPickPhoto: () => void + onPickFile: () => void + onClose: () => void +} + +// #29: splits into two explicit choices rather than one unrestricted file +// input -- see photoInputRef's comment on the Composer below for why a +// single input can't reliably offer both "any file type" and a mobile +// gallery shortcut at once. +function AttachMenu({ onPickPhoto, onPickFile, onClose }: AttachMenuProps) { + useEscapeKey(onClose) + return ( + <> +
+
+ + +
+ + ) +} + export function Composer({ roomId, roomName, members, rooms, disabled, onSend }: ComposerProps) { const [value, setValue] = useState('') const [pendingImage, setPendingImage] = useState<{ id: string; previewUrl: string } | null>(null) @@ -76,8 +104,18 @@ export function Composer({ roomId, roomName, members, rooms, disabled, onSend }: const [roomQuery, setRoomQuery] = useState(null) const [roomActiveIndex, setRoomActiveIndex] = useState(0) const [dragActive, setDragActive] = useState(false) + const [attachMenuOpen, setAttachMenuOpen] = useState(false) const textareaRef = useRef(null) const fileInputRef = useRef(null) + // #29: a separate input with an image/video accept hint, so mobile + // browsers offer their media picker (with a direct Photos/Gallery + // shortcut) instead of the generic chooser a fully-unrestricted `accept` + // falls back to (Camera / Camera Video / Files, no gallery). The + // unrestricted `fileInputRef` above still exists for the "File" choice -- + // Android can't reliably offer both a gallery shortcut and "any file + // type" from a single input, so the attach button now opens a small menu + // to pick which one you want first. + const photoInputRef = useRef(null) // Counts nested dragenter/dragleave pairs (the overlay, the composer box, // the textarea are all separate elements a drag passes over) so the // highlight doesn't flicker off every time the pointer crosses a child @@ -362,35 +400,58 @@ export function Composer({ roomId, roomName, members, rooms, disabled, onSend }: )} {uploadError &&
{uploadError}
}
+ - + {attachMenuOpen && ( + { + setAttachMenuOpen(false) + photoInputRef.current?.click() + }} + onPickFile={() => { + setAttachMenuOpen(false) + fileInputRef.current?.click() + }} + onClose={() => setAttachMenuOpen(false)} + /> )} - +