From bd9c8740a57d4ed49772fd17f13bf7f0a47ad6d4 Mon Sep 17 00:00:00 2001 From: Alisson Patricio Date: Fri, 9 Oct 2026 02:30:17 +0000 Subject: [PATCH] feat(comments): file attachments on mobile comments Mobile comments can attach photos (library, multi-select) and PDFs (document picker), show uploads above the composer, and list a comment's files under it. The web upload pipeline is reused rather than reimplemented: an UploadSource seam (name/size/type/readChunk) replaces File in the uploader, read lazily inside the concurrency slot so native never holds more than MAX_CONCURRENT_CHUNKS chunks. Selection rules, attachment limits and upload-item state are shared by both legs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013LkamMd6XMPeYNHbAV1DZ9 --- .changeset/native-comment-attachments.md | 34 +++++ .../__shared__/native/SocialInput/index.tsx | 6 +- .../__shared__/native/SocialInput/types.ts | 3 +- .../native/SocialInputDrawer/Drawer/index.tsx | 33 ++++- .../native/SocialInputDrawer/Drawer/types.ts | 5 + .../SocialInputDrawer/Placeholder/index.tsx | 8 +- .../SocialInputDrawer/Placeholder/types.ts | 2 + .../native/SocialUpsertActions/index.tsx | 5 +- .../native/SocialUpsertActions/types.ts | 3 + .../modules/comments/common/constants.ts | 10 ++ .../comments/native/BaseComments/index.tsx | 26 +++- .../BaseComments/useCommentComposer/index.ts | 25 +++- .../BaseComments/useCommentComposer/types.ts | 10 ++ .../CommentFilesUpsertActions/index.tsx | 61 ++++++++ .../CommentFilesUpsertActions/styles.ts | 13 ++ .../native/CommentFilesUpsertActions/types.ts | 8 ++ .../comments/native/CommentItem/index.tsx | 2 + .../comments/web/CommentCreate/index.tsx | 13 +- .../CommentFilesUpsertActions/index.tsx | 12 +- packages/components/modules/files/README.md | 89 +++++------- .../context/FileUploadProvider/index.tsx | 9 +- .../context/FileUploadProvider/types.ts | 4 +- .../context/__tests__/fileUploadStore.test.ts | 36 +++++ .../__tests__/useChunkedUpload.test.ts | 18 ++- .../common/hooks/useChunkedUpload/index.ts | 38 +++-- .../hooks/useDeferredFileAttachments/index.ts | 3 +- .../hooks/useDeferredFileAttachments/types.ts | 4 +- .../__tests__/useFileSelect.test.ts | 14 ++ .../files/common/hooks/useFileSelect/index.ts | 9 +- .../files/common/hooks/useFileSelect/types.ts | 4 +- .../common/hooks/useFileUploadLogic/index.ts | 3 +- .../common/hooks/useFileUploadLogic/types.ts | 4 +- .../components/modules/files/common/types.ts | 26 +++- .../__tests__/filterSelectedFiles.test.ts | 84 +++++++++++ .../common/utils/__tests__/formatters.test.ts | 12 ++ .../utils/__tests__/uploadChunk.test.ts | 21 +++ .../utils/__tests__/uploadChunks.test.ts | 82 +++++++++++ .../utils/__tests__/uploadItemState.test.ts | 42 ++++++ .../utils/__tests__/uploadSource.test.ts | 33 +++++ .../modules/files/common/utils/chunkFile.ts | 4 + .../files/common/utils/filterSelectedFiles.ts | 91 ++++++++++++ .../modules/files/common/utils/formatters.ts | 8 ++ .../modules/files/common/utils/index.ts | 3 + .../modules/files/common/utils/uploadChunk.ts | 10 +- .../files/common/utils/uploadChunks.ts | 67 ++++++--- .../files/common/utils/uploadItemState.ts | 37 +++++ .../files/common/utils/uploadSource.ts | 20 +++ .../AttachedFileChip/index.tsx | 45 ++++++ .../AttachedFileChip/types.ts | 6 + .../files/native/AttachedFilesList/index.tsx | 45 ++++++ .../files/native/AttachedFilesList/styles.ts | 12 ++ .../files/native/AttachedFilesList/types.ts | 6 + .../modules/files/native/FileChip/index.tsx | 64 +++++++++ .../modules/files/native/FileChip/styles.ts | 44 ++++++ .../modules/files/native/FileChip/types.ts | 14 ++ .../UploadingFileChip/index.tsx | 88 ++++++++++++ .../UploadingFileChip/styles.ts | 25 ++++ .../UploadingFileChip/types.ts | 8 ++ .../files/native/UploadingFilesList/index.tsx | 48 +++++++ .../files/native/UploadingFilesList/styles.ts | 9 ++ .../files/native/UploadingFilesList/types.ts | 7 + .../__tests__/useNativeFilePicker.test.ts | 127 ++++++++++++++++ .../native/hooks/useNativeFilePicker/index.ts | 135 ++++++++++++++++++ .../native/hooks/useNativeFilePicker/types.ts | 16 +++ .../components/modules/files/native/index.ts | 18 ++- .../createNativeUploadSource.test.ts | 119 +++++++++++++++ .../native/utils/createNativeUploadSource.ts | 64 +++++++++ .../files/web/AttachedFileItem/index.tsx | 15 +- .../UploadingFileItem/index.tsx | 28 +--- .../native/icons/AttachmentIcon/index.tsx | 37 +++++ .../components/native/icons/index.ts | 1 + 71 files changed, 1869 insertions(+), 166 deletions(-) create mode 100644 .changeset/native-comment-attachments.md create mode 100644 packages/components/modules/__shared__/native/SocialUpsertActions/types.ts create mode 100644 packages/components/modules/comments/native/CommentFilesUpsertActions/index.tsx create mode 100644 packages/components/modules/comments/native/CommentFilesUpsertActions/styles.ts create mode 100644 packages/components/modules/comments/native/CommentFilesUpsertActions/types.ts create mode 100644 packages/components/modules/files/common/utils/__tests__/filterSelectedFiles.test.ts create mode 100644 packages/components/modules/files/common/utils/__tests__/formatters.test.ts create mode 100644 packages/components/modules/files/common/utils/__tests__/uploadChunk.test.ts create mode 100644 packages/components/modules/files/common/utils/__tests__/uploadItemState.test.ts create mode 100644 packages/components/modules/files/common/utils/__tests__/uploadSource.test.ts create mode 100644 packages/components/modules/files/common/utils/filterSelectedFiles.ts create mode 100644 packages/components/modules/files/common/utils/uploadItemState.ts create mode 100644 packages/components/modules/files/common/utils/uploadSource.ts create mode 100644 packages/components/modules/files/native/AttachedFilesList/AttachedFileChip/index.tsx create mode 100644 packages/components/modules/files/native/AttachedFilesList/AttachedFileChip/types.ts create mode 100644 packages/components/modules/files/native/AttachedFilesList/index.tsx create mode 100644 packages/components/modules/files/native/AttachedFilesList/styles.ts create mode 100644 packages/components/modules/files/native/AttachedFilesList/types.ts create mode 100644 packages/components/modules/files/native/FileChip/index.tsx create mode 100644 packages/components/modules/files/native/FileChip/styles.ts create mode 100644 packages/components/modules/files/native/FileChip/types.ts create mode 100644 packages/components/modules/files/native/UploadingFilesList/UploadingFileChip/index.tsx create mode 100644 packages/components/modules/files/native/UploadingFilesList/UploadingFileChip/styles.ts create mode 100644 packages/components/modules/files/native/UploadingFilesList/UploadingFileChip/types.ts create mode 100644 packages/components/modules/files/native/UploadingFilesList/index.tsx create mode 100644 packages/components/modules/files/native/UploadingFilesList/styles.ts create mode 100644 packages/components/modules/files/native/UploadingFilesList/types.ts create mode 100644 packages/components/modules/files/native/hooks/useNativeFilePicker/__tests__/useNativeFilePicker.test.ts create mode 100644 packages/components/modules/files/native/hooks/useNativeFilePicker/index.ts create mode 100644 packages/components/modules/files/native/hooks/useNativeFilePicker/types.ts create mode 100644 packages/components/modules/files/native/utils/__tests__/createNativeUploadSource.test.ts create mode 100644 packages/components/modules/files/native/utils/createNativeUploadSource.ts create mode 100644 packages/design-system/components/native/icons/AttachmentIcon/index.tsx 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 ? ( + +