diff --git a/.changeset/native-comment-attachments.md b/.changeset/native-comment-attachments.md new file mode 100644 index 000000000..9818647ab --- /dev/null +++ b/.changeset/native-comment-attachments.md @@ -0,0 +1,34 @@ +--- +'@baseapp-frontend/components': major +'@baseapp-frontend/design-system': minor +--- + +Comments on mobile can carry file attachments, like on web: attach photos from the library or PDFs +from the document picker, watch them upload above the composer, and see (and open) a comment's +files under it. + +**Breaking:** `FileUploadProgress.file` is now typed `UploadSource` instead of `File`, because a +native upload has no `File`. A web `File` passed in is wrapped automatically and its `name`, `size` +and `type` read the same; code that read `File`-only members off `fileProgress.file` (`slice`, +`lastModified`, passing it to `URL.createObjectURL`) has to keep its own reference to the original +file instead. + +The upload pipeline is now shared across platforms instead of being web-only: + +- **`UploadSource`**: every upload entry point (`uploadFile`, `addFile`, `handleFilesSelected`) + accepts a web `File` or an `UploadSource` (`name`, `size`, `type`, `readChunk(start, end)`, + optional `dispose()`). Chunks are read lazily inside the concurrency slot, so a native upload holds + at most `MAX_CONCURRENT_CHUNKS` chunks in memory, and a retried chunk reuses the bytes it already + read. `dispose` runs when an upload leaves the store. +- A chunk whose upload was paused or removed during a retry backoff no longer starts a new request, + and `clearScope` now aborts the uploads it removes. +- **`filterSelectedFiles`** / **`describeRejectedFiles`**: the size/count/type rules both pickers + apply. The web file input now also drops files whose type is outside `acceptedFileTypes` (the + `accept` attribute alone can be bypassed from the OS picker). +- **`COMMENT_FILE_ATTACHMENT_LIMITS`** (`comments/common`): the comment attachment limits, shared by + web and mobile. +- **`files/native`**: `useNativeFilePicker`, `createNativeUploadSource`, `UploadingFilesList`, + `AttachedFilesList`, `FileChip`. +- Native `SocialInput` takes `SocialUpsertActionsProps`, and `SocialInputDrawer` takes a `Footer` + that the sheet grows to fit (`Placeholder` gets the matching `footerHeight`). +- `@baseapp-frontend/design-system`: native `AttachmentIcon`. diff --git a/packages/components/modules/__shared__/native/SocialInput/index.tsx b/packages/components/modules/__shared__/native/SocialInput/index.tsx index 80c7a4fd1..77f045fe2 100644 --- a/packages/components/modules/__shared__/native/SocialInput/index.tsx +++ b/packages/components/modules/__shared__/native/SocialInput/index.tsx @@ -15,6 +15,7 @@ const SocialInput = forwardRef( SocialTextInput = DefaultSocialTextInput, SocialTextInputProps = {}, SocialUpsertActions = DefaultSocialUpsertActions, + SocialUpsertActionsProps = {}, SubmitActions = DefaultSubmitActions, SubmitActionsProps = {}, form, @@ -41,7 +42,10 @@ const SocialInput = forwardRef( replyVariables={replyVariables} {...SocialTextInputProps} > - + SocialTextInputProps?: Partial - SocialUpsertActions?: FC + SocialUpsertActions?: FC + SocialUpsertActionsProps?: Record SubmitActions?: FC SubmitActionsProps?: Partial form: UseFormReturn diff --git a/packages/components/modules/__shared__/native/SocialInputDrawer/Drawer/index.tsx b/packages/components/modules/__shared__/native/SocialInputDrawer/Drawer/index.tsx index 116cef7a5..db6427644 100644 --- a/packages/components/modules/__shared__/native/SocialInputDrawer/Drawer/index.tsx +++ b/packages/components/modules/__shared__/native/SocialInputDrawer/Drawer/index.tsx @@ -1,10 +1,10 @@ -import { forwardRef, useCallback, useRef } from 'react' +import { forwardRef, useCallback, useEffect, useRef, useState } from 'react' import { View } from '@baseapp-frontend/design-system/components/native/views' import { useTheme } from '@baseapp-frontend/design-system/providers/native' import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet' -import { TextInput as NativeTextInput } from 'react-native' +import { LayoutChangeEvent, TextInput as NativeTextInput } from 'react-native' import DefaultSocialInput from '../../SocialInput' import DefaultDrawerHandle from './DrawerHandle' @@ -17,6 +17,9 @@ const Drawer = forwardRef( DrawerHandle = DefaultDrawerHandle, SocialInput = DefaultSocialInput, SocialInputProps = {}, + Footer, + FooterProps = {}, + onFooterHeightChange, form, isLoading, keyboardHeight = 0, @@ -32,6 +35,22 @@ const Drawer = forwardRef( ) => { const bottomSheetRef = useRef(null) const theme = useTheme() + const [footerHeight, setFooterHeight] = useState(0) + + const handleFooterLayout = useCallback( + ({ nativeEvent }: LayoutChangeEvent) => { + const height = Math.ceil(nativeEvent.layout.height) + setFooterHeight(height) + onFooterHeightChange?.(height) + }, + [onFooterHeightChange], + ) + // A removed footer fires no layout event, so its last height must not linger. + const activeFooterHeight = Footer ? footerHeight : 0 + const hasFooter = !!Footer + useEffect(() => { + if (!hasFooter) onFooterHeightChange?.(0) + }, [hasFooter, onFooterHeightChange]) const handleSheetChange = useCallback( (index: number) => { @@ -66,13 +85,21 @@ const Drawer = forwardRef( + {Footer ? ( + +