From 38d690e425cca55f58d56e53fbff9cb6b873d603 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 3 Jun 2026 18:30:43 +0700 Subject: [PATCH] Show attachment actions in a mobile sheet --- packages/app/src/composer/input/input.tsx | 143 +++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/packages/app/src/composer/input/input.tsx b/packages/app/src/composer/input/input.tsx index a565d6085..235520c40 100644 --- a/packages/app/src/composer/input/input.tsx +++ b/packages/app/src/composer/input/input.tsx @@ -2,6 +2,8 @@ import { View, Text, TextInput, + Pressable, + Platform, ActivityIndicator, useWindowDimensions, NativeSyntheticEvent, @@ -46,6 +48,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; +import { useDismissKeyboardOnOpen } from "@/components/ui/keyboard-dismiss"; import { useWebElementScrollbar } from "@/components/use-web-scrollbar"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { useIosHardwareKeyboardSubmit } from "@/hooks/use-ios-hardware-keyboard-submit"; @@ -138,6 +142,8 @@ const MIN_INPUT_HEIGHT_DESKTOP = 46; const DEFAULT_MAX_INPUT_HEIGHT = 160; const MAX_INPUT_VIEWPORT_RATIO = 0.5; const MIN_INPUT_HEIGHT = isWeb ? MIN_INPUT_HEIGHT_DESKTOP : MIN_INPUT_HEIGHT_MOBILE; +const ATTACHMENT_SHEET_HEADER: SheetHeader = { title: "Add attachment" }; +const ATTACHMENT_SHEET_SNAP_POINTS = ["28%", "45%"]; type WebTextInputKeyPressEvent = NativeSyntheticEvent< TextInputKeyPressEventData & { @@ -198,6 +204,55 @@ function AttachmentMenuList({ items }: { items: AttachmentMenuItem[] }) { ); } +function AttachmentSheetItem({ + item, + onSelect, +}: { + item: AttachmentMenuItem; + onSelect: (item: AttachmentMenuItem) => void; +}) { + const handlePress = useCallback(() => { + onSelect(item); + }, [item, onSelect]); + const pressableStyle = useCallback( + ({ pressed }: { pressed: boolean }) => [ + styles.attachmentSheetItem, + pressed && styles.attachmentSheetItemPressed, + item.disabled && styles.buttonDisabled, + ], + [item.disabled], + ); + + return ( + + {item.icon ? {item.icon} : null} + {item.label} + + ); +} + +function AttachmentSheetList({ + items, + onSelect, +}: { + items: AttachmentMenuItem[]; + onSelect: (item: AttachmentMenuItem) => void; +}) { + return ( + + {items.map((item) => ( + + ))} + + ); +} + function AttachmentDropdown({ isConnected, disabled, @@ -211,12 +266,73 @@ function AttachmentDropdown({ renderAttachButtonIcon: (input: { hovered?: boolean }) => React.ReactElement; attachmentMenuItems: AttachmentMenuItem[]; }) { + const isCompact = useIsCompactFormFactor(); + const [isSheetOpen, setIsSheetOpen] = useState(false); + useDismissKeyboardOnOpen(isSheetOpen, isCompact); + + const isButtonDisabled = !isConnected || disabled; + const handleOpenSheet = useCallback(() => { + if (isButtonDisabled) return; + setIsSheetOpen(true); + }, [isButtonDisabled]); + const handleCloseSheet = useCallback(() => { + setIsSheetOpen(false); + }, []); + const handleSheetItemSelect = useCallback((item: AttachmentMenuItem) => { + if (item.disabled) return; + setIsSheetOpen(false); + if (Platform.OS === "ios") { + setTimeout(item.onSelect, 250); + return; + } + item.onSelect(); + }, []); + const mobileAttachButtonStyle = useCallback( + (state: { pressed: boolean; hovered?: boolean }) => { + if (typeof attachButtonStyle === "function") { + return attachButtonStyle({ ...state, hovered: Boolean(state.hovered), open: isSheetOpen }); + } + return attachButtonStyle; + }, + [attachButtonStyle, isSheetOpen], + ); + const renderMobileAttachButtonIcon = useCallback( + ({ hovered }: { hovered?: boolean }) => renderAttachButtonIcon({ hovered }), + [renderAttachButtonIcon], + ); + + if (isCompact) { + return ( + <> + + {renderMobileAttachButtonIcon} + + + + + + ); + } + return ( ({ buttonDisabled: { opacity: 0.5, }, + attachmentSheetList: { + gap: theme.spacing[1], + }, + attachmentSheetItem: { + minHeight: 44, + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[3], + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[2], + borderRadius: theme.borderRadius.xl, + }, + attachmentSheetItemPressed: { + backgroundColor: theme.colors.surface2, + }, + attachmentSheetItemIcon: { + width: 20, + alignItems: "center", + justifyContent: "center", + }, + attachmentSheetItemText: { + color: theme.colors.foreground, + fontSize: theme.fontSize.base, + fontWeight: theme.fontWeight.normal, + }, overlayContainer: { position: "absolute", display: "flex",