diff --git a/docusaurus/docs/reactnative/assets/guides/native-image-picker/options.png b/docusaurus/docs/reactnative/assets/guides/native-image-picker/options.png new file mode 100644 index 0000000000..4ffb6726ad Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/native-image-picker/options.png differ diff --git a/docusaurus/docs/reactnative/basics/installation.mdx b/docusaurus/docs/reactnative/basics/installation.mdx index 00674dd744..3fab594bdd 100644 --- a/docusaurus/docs/reactnative/basics/installation.mdx +++ b/docusaurus/docs/reactnative/basics/installation.mdx @@ -56,7 +56,7 @@ Stream Chat React Native SDK requires installing some peer dependencies to provi ```bash title="Terminal" -yarn add @react-native-camera-roll/camera-roll @react-native-community/netinfo @stream-io/flat-list-mvcp react-native-fs react-native-gesture-handler react-native-image-crop-picker react-native-image-resizer react-native-reanimated react-native-svg +yarn add @react-native-community/netinfo @stream-io/flat-list-mvcp react-native-fs react-native-gesture-handler react-native-image-resizer react-native-reanimated react-native-svg ``` @@ -64,7 +64,7 @@ yarn add @react-native-camera-roll/camera-roll @react-native-community/netinfo @ ```bash title="Terminal" -npx expo install @stream-io/flat-list-mvcp @react-native-community/netinfo expo-file-system expo-image-manipulator expo-image-picker expo-media-library react-native-gesture-handler react-native-reanimated react-native-svg +npx expo install @stream-io/flat-list-mvcp @react-native-community/netinfo expo-file-system expo-image-manipulator react-native-gesture-handler react-native-reanimated react-native-svg ``` @@ -82,12 +82,10 @@ values={[ > -- [`@react-native-camera-roll/camera-roll`](https://github.com/react-native-cameraroll/react-native-cameraroll) for accessing device gallery. - [`@react-native-community/netinfo`](https://github.com/react-native-netinfo/react-native-netinfo) for accessing device gallery. - [`@stream-io/flat-list-mvcp`](https://github.com/GetStream/flat-list-mvcp) for bi-directional FlatList support. - [`react-native-fs`](https://github.com/itinance/react-native-fs) to perform file operations like save, delete, etc. - [`react-native-gesture-handler`](https://github.com/software-mansion/react-native-gesture-handler) to handle gestures within the SDK. -- [`react-native-image-crop-picker`](https://github.com/ivpusic/react-native-image-crop-picker) to capture images to attach them in the message. - [`react-native-image-resizer`](https://github.com/bamlab/react-native-image-resizer) to compress image uploads. - [`react-native-reanimated`](https://github.com/software-mansion/react-native-reanimated) to compress image uploads. - [`react-native-svg`](https://github.com/react-native-svg/react-native-svg) for SVG support. @@ -95,12 +93,10 @@ values={[ -- [`expo-media-library`](https://docs.expo.dev/versions/latest/sdk/media-library/) for accessing device gallery. - [`@react-native-community/netinfo`](https://github.com/react-native-netinfo/react-native-netinfo) for accessing device gallery. - [`@stream-io/flat-list-mvcp`](https://github.com/GetStream/flat-list-mvcp) for bi-directional FlatList support. - [`expo-file-system`](https://docs.expo.dev/versions/latest/sdk/filesystem/) to perform file operations like save, delete, etc. - [`react-native-gesture-handler`](https://github.com/software-mansion/react-native-gesture-handler) to handle gestures within the SDK. -- [`expo-image-picker`](https://docs.expo.dev/versions/latest/sdk/imagepicker/) to capture images to attach them in the message. - [`expo-image-manipulator`](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/) to compress image uploads. - [`react-native-reanimated`](https://github.com/software-mansion/react-native-reanimated) to compress image uploads. - [`react-native-svg`](https://docs.expo.dev/versions/latest/sdk/svg/) for SVG support. @@ -110,6 +106,10 @@ values={[ ### Optional Dependencies +:::note +Starting from `v5.35.0` the `react-native-image-crop-picker` and `expo-image-picker` is no longer a required dependency. You can use it if you want to capture images to attach them in the message else feel free to uninstall it. +::: + There are a few optional dependencies that can be added to have more features within the SDK. +- [`@react-native-camera-roll/camera-roll`](https://github.com/react-native-cameraroll/react-native-cameraroll) for accessing device gallery. +- [`react-native-image-crop-picker`](https://github.com/ivpusic/react-native-image-crop-picker) to capture images to attach them in the message. - [`react-native-video`](https://github.com/react-native-video/react-native-video) for Video and Audio playback support. - [`react-native-audio-recorder-player`](https://github.com/hyochan/react-native-audio-recorder-player) for Audio recording and async audio messages support. - [`react-native-share`](https://github.com/react-native-share/react-native-share) for Attachment sharing support. @@ -129,11 +131,14 @@ values={[ - [`@react-native-clipboard/clipboard`](https://github.com/react-native-clipboard/clipboard) for Copy message support. - [`react-native-document-picker`](https://github.com/rnmods/react-native-document-picker) to access device media files. - [`react-native-quick-sqlite`](https://github.com/margelo/react-native-quick-sqlite) to enable Offline support in the app. +- [`react-native-image-picker`](https://github.com/react-native-image-picker/react-native-image-picker) to use native photo picker. - [`expo-av`](https://docs.expo.dev/versions/latest/sdk/av/) for Video and Audio playback, recording and async audio messages support. +- [`expo-media-library`](https://docs.expo.dev/versions/latest/sdk/media-library/) for accessing device gallery. +- [`expo-image-picker`](https://docs.expo.dev/versions/latest/sdk/imagepicker/) to capture images to attach them in the message. - [`expo-sharing`](https://docs.expo.dev/versions/latest/sdk/sharing/) for Attachments sharing support. - [`expo-haptics`](https://docs.expo.dev/versions/latest/sdk/haptics/) for user haptics support. - [`expo-clipboard`](https://docs.expo.dev/versions/latest/sdk/clipboard/) for Copy message support. @@ -275,7 +280,7 @@ Please also follow the steps mentioned in the links below for corresponding depe - `react-native` - [additional installation steps](https://reactnative.dev/docs/image#gif-and-webp-support-on-android) - `react-native-image-crop-picker` - [additional installation steps](https://github.com/ivpusic/react-native-image-crop-picker#step-3) -- `react-native-cameraroll` - [additional installation steps](https://github.com/react-native-cameraroll/react-native-cameraroll#permissions) +- `@react-native-camera-roll/camera-roll` - [additional installation steps](https://github.com/react-native-cameraroll/react-native-cameraroll#permissions) Now you should be able to run the app on simulator by running following command: diff --git a/docusaurus/docs/reactnative/basics/overview.mdx b/docusaurus/docs/reactnative/basics/overview.mdx index f9c5d14c51..8153c57128 100644 --- a/docusaurus/docs/reactnative/basics/overview.mdx +++ b/docusaurus/docs/reactnative/basics/overview.mdx @@ -60,7 +60,6 @@ The SDK tries to keep the list of external dependencies to a minimum, these are > -- [`@react-native-camera-roll/camera-roll`](https://github.com/react-native-cameraroll/react-native-cameraroll) for accessing device gallery. - [`@react-native-community/netinfo`](https://github.com/react-native-netinfo/react-native-netinfo) for accessing device gallery. - [`@stream-io/flat-list-mvcp`](https://github.com/GetStream/flat-list-mvcp) for bi-directional FlatList support. - [`react-native-fs`](https://github.com/itinance/react-native-fs) to perform file operations like save, delete, etc. @@ -74,7 +73,6 @@ The SDK tries to keep the list of external dependencies to a minimum, these are -- [`expo-media-library`](https://docs.expo.dev/versions/latest/sdk/media-library/) for accessing device gallery. - [`@react-native-community/netinfo`](https://github.com/react-native-netinfo/react-native-netinfo) for accessing device gallery. - [`@stream-io/flat-list-mvcp`](https://github.com/GetStream/flat-list-mvcp) for bi-directional FlatList support. - [`expo-file-system`](https://docs.expo.dev/versions/latest/sdk/filesystem/) to perform file operations like save, delete, etc. @@ -102,6 +100,7 @@ There are a few optional dependencies that can be added by our users to have mor > +- [`@react-native-camera-roll/camera-roll`](https://github.com/react-native-cameraroll/react-native-cameraroll) for accessing device gallery. - [`react-native-video`](https://github.com/react-native-video/react-native-video) for Video and Audio playback support. - [`react-native-audio-recorder-player`](https://github.com/hyochan/react-native-audio-recorder-player) for Audio recording and async audio messages support. - [`react-native-share`](https://github.com/react-native-share/react-native-share) for Attachment sharing support. @@ -109,11 +108,13 @@ There are a few optional dependencies that can be added by our users to have mor - [`@react-native-clipboard/clipboard`](https://github.com/react-native-clipboard/clipboard) for Copy message support. - [`react-native-document-picker`](https://github.com/rnmods/react-native-document-picker) to access device media files. - [`react-native-quick-sqlite`](https://github.com/margelo/react-native-quick-sqlite) to enable Offline support in the app. +- [`react-native-image-picker`](https://github.com/react-native-image-picker/react-native-image-picker) to use native photo picker. - [`expo-av`](https://docs.expo.dev/versions/latest/sdk/av/) for Video and Audio playback, recording and async audio messages support. +- [`expo-media-library`](https://docs.expo.dev/versions/latest/sdk/media-library/) for accessing device gallery. - [`expo-sharing`](https://docs.expo.dev/versions/latest/sdk/sharing/) for Attachments sharing support. - [`expo-haptics`](https://docs.expo.dev/versions/latest/sdk/haptics/) for user haptics support. - [`expo-clipboard`](https://docs.expo.dev/versions/latest/sdk/clipboard/) for Copy message support. diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/handle_attach_button_press.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/handle_attach_button_press.mdx new file mode 100644 index 0000000000..2ac2c88c07 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/handle_attach_button_press.mdx @@ -0,0 +1,5 @@ +Function to customize the behaviour when the [AttachButton](../../../../ui-components/attach-button.mdx) is pressed in the message input. + +| Type | +| ------------ | +| `() => void` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_camera_picker.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_camera_picker.mdx new file mode 100644 index 0000000000..5896a258d5 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_camera_picker.mdx @@ -0,0 +1,5 @@ +Enable the file picker on the [`MessageInput`](../../../../ui-components/message-input.mdx) component. + +| Type | Default | +| ------- | ------- | +| Boolean | `true` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_file_picker.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_file_picker.mdx index a3b9425550..5896a258d5 100644 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_file_picker.mdx +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_file_picker.mdx @@ -2,4 +2,4 @@ Enable the file picker on the [`MessageInput`](../../../../ui-components/message | Type | Default | | ------- | ------- | -| boolean | true | +| Boolean | `true` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_image_picker.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_image_picker.mdx index e9689e1787..999c8f1ce5 100644 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_image_picker.mdx +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has_image_picker.mdx @@ -2,4 +2,4 @@ Enable the image picker on the [`MessageInput`](../../../../ui-components/messag | Type | Default | | ------- | ------- | -| boolean | true | +| Boolean | `true` | diff --git a/docusaurus/docs/reactnative/contexts/message-context.mdx b/docusaurus/docs/reactnative/contexts/message-context.mdx index 832511b9e7..9a4b509bdc 100644 --- a/docusaurus/docs/reactnative/contexts/message-context.mdx +++ b/docusaurus/docs/reactnative/contexts/message-context.mdx @@ -3,8 +3,6 @@ id: message-context title: MessageContext --- -import Disabled from '../common-content/contexts/channel-context/disabled.mdx'; - import Alignment from '../common-content/contexts/message-context/alignment.mdx'; import Files from '../common-content/contexts/message-context/files.mdx'; import GroupStyles from '../common-content/contexts/message-context/group_styles.mdx'; @@ -80,10 +78,6 @@ True if one of the following condition is true: | ------- | | boolean | -### disabled - - - ### files diff --git a/docusaurus/docs/reactnative/contexts/message-input-context.mdx b/docusaurus/docs/reactnative/contexts/message-input-context.mdx index dba4f6f94d..26101afa8c 100644 --- a/docusaurus/docs/reactnative/contexts/message-input-context.mdx +++ b/docusaurus/docs/reactnative/contexts/message-input-context.mdx @@ -26,6 +26,8 @@ import DoDocUploadRequest from '../common-content/ui-components/channel/props/do import DoImageUploadRequest from '../common-content/ui-components/channel/props/do_image_upload_request.mdx'; import EmojiSearchIndex from '../common-content/ui-components/channel/props/emoji_search_index.mdx'; import FileUploadPreview from '../common-content/ui-components/channel/props/file_upload_preview.mdx'; +import HandleAttachButtonPress from '../common-content/ui-components/channel/props/handle_attach_button_press.mdx'; +import HasCameraPicker from '../common-content/ui-components/channel/props/has_camera_picker.mdx'; import HasCommands from '../common-content/ui-components/channel/props/has_commands.mdx'; import HasFilePicker from '../common-content/ui-components/channel/props/has_file_picker.mdx'; import HasImagePicker from '../common-content/ui-components/channel/props/has_image_picker.mdx'; @@ -182,15 +184,23 @@ const { sendMessage, toggleAttachmentPicker } = useMessageInputContext(); +###
_forwarded from [Channel](../../core-components/channel#handleattachbuttonpress)_ props
`handleAttachButtonPress` {#handleAttachButtonPress} + + + +###
_forwarded from [Channel](../../core-components/channel#hascamerapicker)_ props
`hasCameraPicker` {#hascamerapicker} + + + ###
_forwarded from [Channel](../../core-components/channel#hascommands)_ props
`hasCommands` {#hascommands} -###
_forwarded from [Channel](../../core-components/channel#hasfilepicker)_ props
hasFilePicker {#hasfilepicker} +###
_forwarded from [Channel](../../core-components/channel#hasfilepicker)_ props
`hasFilePicker` {#hasfilepicker} -###
_forwarded from [Channel](../../core-components/channel#hasimagepicker)_ props
hasImagePicker {#hasimagepicker} +###
_forwarded from [Channel](../../core-components/channel#hasimagepicker)_ props
`hasImagePicker` {#hasimagepicker} diff --git a/docusaurus/docs/reactnative/core-components/channel.mdx b/docusaurus/docs/reactnative/core-components/channel.mdx index 9352e0f8f4..2808e97768 100644 --- a/docusaurus/docs/reactnative/core-components/channel.mdx +++ b/docusaurus/docs/reactnative/core-components/channel.mdx @@ -63,6 +63,7 @@ import GetMessagesGroupStyles from '../common-content/ui-components/channel/prop import Giphy from '../common-content/ui-components/channel/props/giphy.mdx'; import GiphyEnabled from '../common-content/ui-components/channel/props/giphy_enabled.mdx'; import GiphyVersion from '../common-content/ui-components/channel/props/giphy_version.mdx'; +import HandleAttachButtonPress from '../common-content/ui-components/channel/props/handle_attach_button_press.mdx'; import HandleBlock from '../common-content/ui-components/channel/props/handle_block.mdx'; import HandleCopy from '../common-content/ui-components/channel/props/handle_copy.mdx'; import HandleDelete from '../common-content/ui-components/channel/props/handle_delete.mdx'; @@ -74,6 +75,7 @@ import HandleQuotedReply from '../common-content/ui-components/channel/props/han import HandleReaction from '../common-content/ui-components/channel/props/handle_reaction.mdx'; import HandleRetry from '../common-content/ui-components/channel/props/handle_retry.mdx'; import HandleThreadReply from '../common-content/ui-components/channel/props/handle_thread_reply.mdx'; +import HasCameraPicker from '../common-content/ui-components/channel/props/has_camera_picker.mdx'; import HasCommands from '../common-content/ui-components/channel/props/has_commands.mdx'; import HasFilePicker from '../common-content/ui-components/channel/props/has_file_picker.mdx'; import HasImagePicker from '../common-content/ui-components/channel/props/has_image_picker.mdx'; @@ -480,6 +482,10 @@ The max allowable is 255, which when reached displays as `255+`. +### `handleAttachButtonPress` + + + ### `handleBlock` @@ -524,6 +530,10 @@ The max allowable is 255, which when reached displays as `255+`. +### `hasCameraPicker` + + + ### `hasCommands` diff --git a/docusaurus/docs/reactnative/customization/native-handlers.mdx b/docusaurus/docs/reactnative/customization/native-handlers.mdx index cb14b253c4..4ecb65cbe3 100644 --- a/docusaurus/docs/reactnative/customization/native-handlers.mdx +++ b/docusaurus/docs/reactnative/customization/native-handlers.mdx @@ -102,6 +102,14 @@ A function to open the document picker and return documents picked from it. | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | [`react-native-document-picker`](https://github.com/rnmods/react-native-document-picker) | [`expo-document-picker`](https://docs.expo.io/versions/latest/sdk/document-picker/) | +### `pickImage` + +A function to open the native image picker and return images picked from it. + +| React Native CLI | Expo | +| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [`react-native-image-picker`](https://github.com/react-native-image-picker/react-native-image-picker) | [`expo-image-picker`](https://docs.expo.io/versions/latest/sdk/imagepicker/) | + ### `saveFile` A function to save a file from a URL to local storage. diff --git a/docusaurus/docs/reactnative/guides/native-image-picker.mdx b/docusaurus/docs/reactnative/guides/native-image-picker.mdx new file mode 100644 index 0000000000..7369c6d476 --- /dev/null +++ b/docusaurus/docs/reactnative/guides/native-image-picker.mdx @@ -0,0 +1,116 @@ +--- +id: native-image-picker +title: Native Image Picker +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::note +This guide can help you to comply with the new Google Play's [android policy for photo and video permissions](https://support.google.com/googleplay/android-developer/answer/14115180?hl=en). +::: + +To enable the native image picker, you need to do the following steps and that would provide a native image picker for both iOS and Android. + +### Step 1: Uninstall the media library + +Uninstall the media library by running the following command(if you have already installed it) else choose not to install it at all as it is a optional dependency. + + + + +```bash title="Terminal" +yarn remove @react-native-camera-roll/camera-roll +``` + + + + + +```bash title="Terminal" +yarn remove expo-media-library +``` + + + + +### Step 2: Install the native image picker + +Install the native image picker by running the following command: + + + + +```bash title="Terminal" +yarn add react-native-image-picker +``` + + + + + +```bash title="Terminal" +npx expo install expo-image-picker +``` + + + + +This shall give you a UI to select images from the gallery using native image picker or take a picture from the camera or alternatively select a file. + +![](../assets/guides/native-image-picker/options.png) + +:::note +Please follow the post installation steps as mentioned in the [react-native-image-picker](https://github.com/react-native-image-picker/react-native-image-picker?tab=readme-ov-file#post-install-steps). +::: + +### Step 3: Add customization(if necessary) + +You can customize what happens on clicking the [`AttachButton`](../ui-components/attach-button.mdx) by passing your own `onPress` function to the `handleAttachButtonPress` of the `Channel` component. + +```jsx +import { useCallback } from 'react'; +import { Channel } from 'stream-chat-react-native'; + +const App = () => { + const handleAttachButtonPress = useCallback(async () => { + // Your custom logic here + }, []); + + return ; +}; +``` + +The other alternative is customizing the `AttachButton` component itself. + +```jsx +import { AttachButton } from 'stream-chat-react-native'; + +const CustomAttachButton = props => { + const { onPress } = props; + + const handlePress = async () => { + // Your custom logic here + }; + + return ; +}; + +const App = () => { + return ; +}; +``` diff --git a/docusaurus/sidebars-react-native.json b/docusaurus/sidebars-react-native.json index fc4f57a716..9ffdd2d636 100644 --- a/docusaurus/sidebars-react-native.json +++ b/docusaurus/sidebars-react-native.json @@ -130,6 +130,7 @@ "Advanced Guides": [ "guides/audio-messages-support", "guides/date-time-formatting", + "guides/native-image-picker", "customization/typescript", "basics/troubleshooting", "basics/stream_chat_with_navigation", diff --git a/examples/ExpoMessaging/app.json b/examples/ExpoMessaging/app.json index ff889af43e..7f021eae43 100644 --- a/examples/ExpoMessaging/app.json +++ b/examples/ExpoMessaging/app.json @@ -31,19 +31,9 @@ "favicon": "./assets/favicon.png", "bundler": "metro" }, - "experiments": { - "turboModules": true - }, "scheme": "ExpoMessaging", "plugins": [ "expo-router", - [ - "expo-media-library", - { - "photosPermission": "$(PRODUCT_NAME) would like access to your photo gallery to share image in a message.", - "savePhotosPermission": "$(PRODUCT_NAME) would like to save photos to your photo gallery after downloading from a message." - } - ], [ "expo-image-picker", { diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index 44f1e09642..b0bb4c754a 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -22,7 +22,6 @@ "expo-image-manipulator": "~12.0.5", "expo-image-picker": "~15.0.5", "expo-linking": "~6.3.1", - "expo-media-library": "~16.0.4", "expo-router": "~3.5.16", "expo-sharing": "~12.0.1", "expo-splash-screen": "~0.27.5", diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock index 879283c873..1ad2ea362a 100644 --- a/examples/ExpoMessaging/yarn.lock +++ b/examples/ExpoMessaging/yarn.lock @@ -3957,11 +3957,6 @@ expo-linking@~6.3.1: expo-constants "~16.0.0" invariant "^2.2.4" -expo-media-library@~16.0.4: - version "16.0.4" - resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-16.0.4.tgz#d6b264a201861a2eb055b8c181368d2e7f525ca4" - integrity sha512-nX9iN8+XAoERDVGPpDdUbhFwvfYdBpkgTAxwDOYL7heASYCOdxfqQtXy/jv1+QZpj0epaR6Owq/LUn1lVP3ykg== - expo-modules-autolinking@1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-1.11.1.tgz#4a867f727d9dfde07de8dde14b333a3cbf82ce3c" @@ -7237,10 +7232,10 @@ stream-buffers@2.2.x, stream-buffers@~2.2.0: version "0.0.0" uid "" -stream-chat-react-native-core@5.33.0: - version "5.33.0" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.33.0.tgz#14f04de90cbc8db011bab8db3fa84abe2dc2eaec" - integrity sha512-V9OJA9MrHzaCw5q16ZRbEktA1HamITbXPOkVZOjpDbb0OBcmedmOnD9C2NFIprc770lhllS/1MKBDr0GdQ9NXQ== +stream-chat-react-native-core@5.33.1: + version "5.33.1" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.33.1.tgz#d9e7847469d3ffb6e7fd35fbb7b720f2e25d172e" + integrity sha512-TCDmChJe07cYyL3sErc6qycRFMA+HbflCKRGrFvVvpU0RdWJljaqiOo3avFSauciSnQxx9WxzTkMism8YsFHcQ== dependencies: "@gorhom/bottom-sheet" "4.4.8" dayjs "1.10.5" diff --git a/examples/SampleApp/Gemfile.lock b/examples/SampleApp/Gemfile.lock index 109f1de694..dacc34db00 100644 --- a/examples/SampleApp/Gemfile.lock +++ b/examples/SampleApp/Gemfile.lock @@ -10,28 +10,28 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.894.0) - aws-sdk-core (3.191.3) + aws-partitions (1.962.0) + aws-sdk-core (3.201.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.77.0) - aws-sdk-core (~> 3, >= 3.191.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.143.0) - aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.157.0) + aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -87,7 +87,7 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.109.0) + excon (0.111.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -109,22 +109,22 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.3.0) - fastlane (2.219.0) + fastimage (2.3.1) + fastlane (2.222.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -145,10 +145,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (>= 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -157,7 +157,7 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-firebase_app_distribution (0.9.0) google-apis-firebaseappdistribution_v1 (~> 0.3.0) google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) @@ -186,12 +186,12 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) + google-cloud-errors (1.4.0) google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -207,42 +207,43 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.6) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.1) - jwt (2.8.0) + json (2.7.2) + jwt (2.8.2) base64 - mini_magick (4.12.0) + mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.22.2) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.4.0) + multipart-post (2.4.1) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) nkf (0.2.0) - optparse (0.4.0) + optparse (0.5.0) os (1.1.4) plist (3.7.1) public_suffix (4.0.7) - rake (13.1.0) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.3.4) + strscan rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) + security (0.1.5) signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -251,6 +252,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -266,13 +268,13 @@ GEM uber (0.1.0) unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.24.0) + xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.2, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index 4beb2a4589..0ecce10855 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -1240,15 +1240,15 @@ PODS: - glog - RCT-Folly (= 2022.05.16.00) - React-Core - - RNImageCropPicker (0.39.0): + - RNImageCropPicker (0.41.2): - React-Core - React-RCTImage - - RNImageCropPicker/QBImagePickerController (= 0.39.0) - - TOCropViewController - - RNImageCropPicker/QBImagePickerController (0.39.0): + - RNImageCropPicker/QBImagePickerController (= 0.41.2) + - TOCropViewController (~> 2.7.4) + - RNImageCropPicker/QBImagePickerController (0.41.2): - React-Core - React-RCTImage - - TOCropViewController + - TOCropViewController (~> 2.7.4) - RNNotifee (7.8.2): - React-Core - RNNotifee/NotifeeCore (= 7.8.2) @@ -1276,7 +1276,7 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.1) - - TOCropViewController (2.6.1) + - TOCropViewController (2.7.4) - Yoga (1.14.0) DEPENDENCIES: @@ -1608,7 +1608,7 @@ SPEC CHECKSUMS: RNFBMessaging: 9b16c72d001787aca05e2fb997e5c979b821dbb4 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 2e3251b41d462552997c61afd680220d019fea65 - RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332 + RNImageCropPicker: 771e2ca319d2cf92e04ebf334ece892ee9a6728f RNNotifee: 8e2d3df3f0e9ce8f5d1fe4c967431138190b6175 RNReactNativeHapticFeedback: afa5bf2794aecbb2dba2525329253da0d66656df RNReanimated: 440ca83ef0a79a3376455663fc4a01300e131240 @@ -1618,7 +1618,7 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 + TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 Yoga: 9e6a04eacbd94f97d94577017e9f23b3ab41cf6c PODFILE CHECKSUM: 751ee2c534898a790da0a7dba7d623f1f21ae757 diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index ab7b96a281..72ccceac25 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -42,7 +42,7 @@ "react-native-fs": "^2.18.0", "react-native-gesture-handler": "^2.14.0", "react-native-haptic-feedback": "2.0.3", - "react-native-image-crop-picker": "0.39.0", + "react-native-image-crop-picker": "^0.41.2", "react-native-image-resizer": "1.4.5", "react-native-markdown-package": "1.8.2", "react-native-quick-sqlite": "8.0.2", diff --git a/examples/SampleApp/src/components/NewDirectMessagingSendButton.tsx b/examples/SampleApp/src/components/NewDirectMessagingSendButton.tsx index c233002ef4..a0c8d716aa 100644 --- a/examples/SampleApp/src/components/NewDirectMessagingSendButton.tsx +++ b/examples/SampleApp/src/components/NewDirectMessagingSendButton.tsx @@ -64,8 +64,8 @@ const SendButtonWithContext = < testID='send-button' > {giphyActive && } - {!giphyActive && disabled && } - {!giphyActive && !disabled && } + {!giphyActive && disabled && } + {!giphyActive && !disabled && } ); }; diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index 11d050a942..0c77c13a81 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -6203,10 +6203,10 @@ react-native-haptic-feedback@2.0.3: resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-2.0.3.tgz#09133b2175503831c04798cb0dc63ae91e3959c1" integrity sha512-7+qvcxXZts/hA+HOOIFyM1x9m9fn/TJVSTgXaoQ8uT4gLc97IMvqHQ559tDmnlth+hHMzd3HRMpmRLWoKPL0DA== -react-native-image-crop-picker@0.39.0: - version "0.39.0" - resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.39.0.tgz#9cb8e8ffb0e8ab06f7b3227cadf077169e225eba" - integrity sha512-4aANbQMrmU6zN/4b0rVBA7SbaZ3aa5JESm3Xk751sINybZMt1yz/9h95LkO7U0pbslHDo3ofXjG75PmQRP6a/w== +react-native-image-crop-picker@^0.41.2: + version "0.41.2" + resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.41.2.tgz#824fa8fee8391fbb3e0b5ae2973221a2dff0cafb" + integrity sha512-GcDu/adXU/1y/MrxsbOfqcGRGWC2pTttt5VGy/jyRJ6GXfoC29fTQf8SG5kGtc5schSR6K+mKYO4uW6eJPljlQ== react-native-image-resizer@1.4.5: version "1.4.5" diff --git a/examples/TypeScriptMessaging/ios/Podfile.lock b/examples/TypeScriptMessaging/ios/Podfile.lock index a425bc4bab..8669527fc7 100644 --- a/examples/TypeScriptMessaging/ios/Podfile.lock +++ b/examples/TypeScriptMessaging/ios/Podfile.lock @@ -944,12 +944,14 @@ PODS: - React-Mapbuffer (0.73.6): - glog - React-debug - - react-native-cameraroll (5.6.0): - - React-Core - react-native-document-picker (9.0.1): - React-Core - react-native-flipper (0.212.0): - React-Core + - react-native-image-picker (7.1.2): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core - react-native-image-resizer (1.4.5): - React-Core - react-native-netinfo (11.3.0): @@ -1148,15 +1150,15 @@ PODS: - glog - RCT-Folly (= 2022.05.16.00) - React-Core - - RNImageCropPicker (0.39.0): + - RNImageCropPicker (0.41.2): - React-Core - React-RCTImage - - RNImageCropPicker/QBImagePickerController (= 0.39.0) - - TOCropViewController - - RNImageCropPicker/QBImagePickerController (0.39.0): + - RNImageCropPicker/QBImagePickerController (= 0.41.2) + - TOCropViewController (~> 2.7.4) + - RNImageCropPicker/QBImagePickerController (0.41.2): - React-Core - React-RCTImage - - TOCropViewController + - TOCropViewController (~> 2.7.4) - RNReactNativeHapticFeedback (2.0.3): - React-Core - RNReanimated (3.7.1): @@ -1173,7 +1175,7 @@ PODS: - RNSVG (14.1.0): - React-Core - SocketRocket (0.6.1) - - TOCropViewController (2.6.1) + - TOCropViewController (2.7.4) - Yoga (1.14.0) DEPENDENCIES: @@ -1229,9 +1231,9 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" - react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-flipper (from `../node_modules/react-native-flipper`) + - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-image-resizer (from `../node_modules/react-native-image-resizer`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-quick-sqlite (from `../node_modules/react-native-quick-sqlite`) @@ -1343,12 +1345,12 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: :path: "../node_modules/react-native/ReactCommon" - react-native-cameraroll: - :path: "../node_modules/@react-native-camera-roll/camera-roll" react-native-document-picker: :path: "../node_modules/react-native-document-picker" react-native-flipper: :path: "../node_modules/react-native-flipper" + react-native-image-picker: + :path: "../node_modules/react-native-image-picker" react-native-image-resizer: :path: "../node_modules/react-native-image-resizer" react-native-netinfo: @@ -1464,9 +1466,9 @@ SPEC CHECKSUMS: React-jsinspector: 85583ef014ce53d731a98c66a0e24496f7a83066 React-logger: 3eb80a977f0d9669468ef641a5e1fabbc50a09ec React-Mapbuffer: 84ea43c6c6232049135b1550b8c60b2faac19fab - react-native-cameraroll: 755bcc628148a90a7c9cf3f817a252be3a601bc5 react-native-document-picker: 2b8f18667caee73a96708a82b284a4f40b30a156 react-native-flipper: 9c1957af24b76493ba74f46d000a5c1d485e7731 + react-native-image-picker: d3db110a3ded6e48c93aef7e8e51afdde8b16ed0 react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f react-native-netinfo: 299dad906cdbf3b67bcc6f693c807f98bdd127cc react-native-quick-sqlite: 2b225dadc63b670f027111e58f6f169773f6d755 @@ -1497,16 +1499,16 @@ SPEC CHECKSUMS: RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 67fb54b3e6ca338a8044e85cd6f340265aa41091 - RNImageCropPicker: 14fe1c29298fb4018f3186f455c475ab107da332 + RNImageCropPicker: 771e2ca319d2cf92e04ebf334ece892ee9a6728f RNReactNativeHapticFeedback: afa5bf2794aecbb2dba2525329253da0d66656df RNReanimated: 15a855719335a6b655a214531e86d806edfd49da RNScreens: 17e2f657f1b09a71ec3c821368a04acbb7ebcb46 RNShare: d82e10f6b7677f4b0048c23709bd04098d5aee6c RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 + TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 Yoga: d17d2cc8105eed528474683b42e2ea310e1daf61 PODFILE CHECKSUM: 90406e1e85c82b37484f5d746afa45c0637bb4b3 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/examples/TypeScriptMessaging/package.json b/examples/TypeScriptMessaging/package.json index 144f1b3f1a..3cc9bf1e68 100644 --- a/examples/TypeScriptMessaging/package.json +++ b/examples/TypeScriptMessaging/package.json @@ -12,7 +12,6 @@ "clean-all": "yarn clean && rm -rf node_modules && rm -rf ios/Pods && rm -rf vendor && bundle install && yarn install && cd ios && bundle exec pod install && cd -" }, "dependencies": { - "@react-native-camera-roll/camera-roll": "^5.3.1", "@react-native-clipboard/clipboard": "^1.10.0", "@react-native-community/masked-view": "0.1.11", "@react-native-community/netinfo": "^11.0.1", @@ -26,7 +25,8 @@ "react-native-fs": "^2.18.0", "react-native-gesture-handler": "^2.14.0", "react-native-haptic-feedback": "^2.0.3", - "react-native-image-crop-picker": "^0.39.0", + "react-native-image-crop-picker": "^0.41.2", + "react-native-image-picker": "^7.1.2", "react-native-image-resizer": "^1.4.5", "react-native-quick-sqlite": "^8.0.2", "react-native-reanimated": "^3.7.0", diff --git a/examples/TypeScriptMessaging/yarn.lock b/examples/TypeScriptMessaging/yarn.lock index 2ee4fc7e48..07c4f2d3da 100644 --- a/examples/TypeScriptMessaging/yarn.lock +++ b/examples/TypeScriptMessaging/yarn.lock @@ -1898,11 +1898,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@react-native-camera-roll/camera-roll@^5.3.1": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.6.0.tgz#385082d57d694f3fd5ae386f8b8ce24b0969c5f9" - integrity sha512-a/GYwnBTxj1yKWB9m/qy8GzjowSocML8NbLT81wdMh0JzZYXCLze51BR2cb8JNDgRPzA9xe7KpD3j9qQOSOjag== - "@react-native-clipboard/clipboard@^1.10.0": version "1.13.2" resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.13.2.tgz#28adcfc43ed2addddf79a59198ec1b25087c115e" @@ -6325,10 +6320,15 @@ react-native-haptic-feedback@^2.0.3: resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-2.0.3.tgz#09133b2175503831c04798cb0dc63ae91e3959c1" integrity sha512-7+qvcxXZts/hA+HOOIFyM1x9m9fn/TJVSTgXaoQ8uT4gLc97IMvqHQ559tDmnlth+hHMzd3HRMpmRLWoKPL0DA== -react-native-image-crop-picker@^0.39.0: - version "0.39.0" - resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.39.0.tgz#9cb8e8ffb0e8ab06f7b3227cadf077169e225eba" - integrity sha512-4aANbQMrmU6zN/4b0rVBA7SbaZ3aa5JESm3Xk751sINybZMt1yz/9h95LkO7U0pbslHDo3ofXjG75PmQRP6a/w== +react-native-image-crop-picker@^0.41.2: + version "0.41.2" + resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.41.2.tgz#824fa8fee8391fbb3e0b5ae2973221a2dff0cafb" + integrity sha512-GcDu/adXU/1y/MrxsbOfqcGRGWC2pTttt5VGy/jyRJ6GXfoC29fTQf8SG5kGtc5schSR6K+mKYO4uW6eJPljlQ== + +react-native-image-picker@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-7.1.2.tgz#383849d1953caf4578874a1f5e5dd11c737bd5cd" + integrity sha512-b5y5nP60RIPxlAXlptn2QwlIuZWCUDWa/YPUVjgHc0Ih60mRiOg1PSzf0IjHSLeOZShCpirpvSPGnDExIpTRUg== react-native-image-resizer@^1.4.5: version "1.4.5" diff --git a/package/expo-package/package.json b/package/expo-package/package.json index 0c1a3ce810..dbc919c7f9 100644 --- a/package/expo-package/package.json +++ b/package/expo-package/package.json @@ -21,7 +21,7 @@ "expo-file-system": "*", "expo-haptics": "*", "expo-image-manipulator": "*", - "expo-image-picker": ">=14.1.0", + "expo-image-picker": "*", "expo-media-library": "*", "expo-sharing": "*" }, @@ -35,6 +35,12 @@ "expo-document-picker": { "optional": true }, + "expo-media-library": { + "optional": true + }, + "expo-image-picker": { + "optional": true + }, "expo-sharing": { "optional": true }, @@ -46,9 +52,7 @@ "@react-native-community/netinfo": "^6.0.0", "expo": "^44.0.0", "expo-file-system": "^11.0.2", - "expo-image-manipulator": "^9.1.0", - "expo-image-picker": "^14.1.1", - "expo-media-library": "~15.2.3" + "expo-image-manipulator": "^9.1.0" }, "scripts": { "prepack": " cp ../../README.md .", diff --git a/package/expo-package/src/handlers/getLocalAssetUri.ts b/package/expo-package/src/handlers/getLocalAssetUri.ts deleted file mode 100644 index ea03bbf660..0000000000 --- a/package/expo-package/src/handlers/getLocalAssetUri.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as MediaLibrary from 'expo-media-library'; - -export const getLocalAssetUri = async (assetId: string): Promise => { - try { - const { localUri } = await MediaLibrary.getAssetInfoAsync(assetId); - return localUri; - } catch { - throw new Error('getLocalAssetUri Error'); - } -}; diff --git a/package/expo-package/src/handlers/getPhotos.ts b/package/expo-package/src/handlers/getPhotos.ts deleted file mode 100644 index 1e5bdb9310..0000000000 --- a/package/expo-package/src/handlers/getPhotos.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as MediaLibrary from 'expo-media-library'; -import type { Asset } from 'stream-chat-react-native-core'; - -type ReturnType = { - assets: Array & { source: 'picker' }>; - endCursor: string | undefined; - hasNextPage: boolean; - iOSLimited: boolean; -}; - -export const getPhotos = async ({ - after, - first, -}: MediaLibrary.AssetsOptions): Promise => { - try { - // NOTE: - // should always check first before requesting permission - // because always requesting permission will cause - // the app to go to background even if it was granted - const { accessPrivileges, status } = await MediaLibrary.getPermissionsAsync(); - if (status !== 'granted') { - const { status: newStatus } = await MediaLibrary.requestPermissionsAsync(); - if (newStatus !== 'granted') { - throw new Error('getPhotos Error'); - } - } - const results = await MediaLibrary.getAssetsAsync({ - after, - first, - mediaType: [MediaLibrary.MediaType.photo, MediaLibrary.MediaType.video], - sortBy: [MediaLibrary.SortBy.modificationTime], - }); - const assets = results.assets.map((asset) => ({ - duration: asset.duration, - height: asset.height, - id: asset.id, - name: asset.filename, - source: 'picker' as const, - type: asset.mediaType, - uri: asset.uri, - width: asset.width, - })); - - const hasNextPage = results.hasNextPage; - const endCursor = results.endCursor; - return { assets, endCursor, hasNextPage, iOSLimited: accessPrivileges === 'limited' }; - } catch { - throw new Error('getPhotos Error'); - } -}; diff --git a/package/expo-package/src/handlers/iOS14RefreshGallerySelection.ts b/package/expo-package/src/handlers/iOS14RefreshGallerySelection.ts deleted file mode 100644 index f724987222..0000000000 --- a/package/expo-package/src/handlers/iOS14RefreshGallerySelection.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Platform } from 'react-native'; - -import * as MediaLibrary from 'expo-media-library'; - -const isAboveIOS14 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 14; - -export const iOS14RefreshGallerySelection = (): Promise => { - if (isAboveIOS14) { - return MediaLibrary.presentPermissionsPickerAsync(); - } - return Promise.resolve(); -}; diff --git a/package/expo-package/src/handlers/index.ts b/package/expo-package/src/handlers/index.ts index 2518c330db..48e39b9266 100644 --- a/package/expo-package/src/handlers/index.ts +++ b/package/expo-package/src/handlers/index.ts @@ -1,12 +1,7 @@ export * from './Audio'; export * from './compressImage'; export * from './deleteFile'; -export * from './getLocalAssetUri'; -export * from './getPhotos'; -export * from './iOS14RefreshGallerySelection'; export * from './NetInfo'; -export * from './oniOS14GalleryLibrarySelectionChange'; export * from './saveFile'; export * from './Sound'; -export * from './takePhoto'; export * from './Video'; diff --git a/package/expo-package/src/handlers/oniOS14GalleryLibrarySelectionChange.ts b/package/expo-package/src/handlers/oniOS14GalleryLibrarySelectionChange.ts deleted file mode 100644 index f41a57d28f..0000000000 --- a/package/expo-package/src/handlers/oniOS14GalleryLibrarySelectionChange.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Platform } from 'react-native'; - -import * as MediaLibrary from 'expo-media-library'; - -const isAboveIOS14 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 14; - -export function oniOS14GalleryLibrarySelectionChange(callback: () => void): { - unsubscribe: () => void; -} { - if (isAboveIOS14) { - const subscription = MediaLibrary.addListener(callback); - return { - unsubscribe: () => { - subscription.remove(); - }, - }; - } - return { - // eslint-disable-next-line @typescript-eslint/no-empty-function - unsubscribe: () => {}, - }; -} diff --git a/package/expo-package/src/handlers/takePhoto.ts b/package/expo-package/src/handlers/takePhoto.ts deleted file mode 100644 index b30d351ef5..0000000000 --- a/package/expo-package/src/handlers/takePhoto.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Image, Platform } from 'react-native'; - -import * as ImagePicker from 'expo-image-picker'; - -type Size = { - height?: number; - width?: number; -}; - -export const takePhoto = async ({ compressImageQuality = 1 }) => { - try { - const permissionCheck = await ImagePicker.getCameraPermissionsAsync(); - const canRequest = permissionCheck.canAskAgain; - let permissionGranted = permissionCheck.granted; - if (!permissionGranted) { - if (canRequest) { - const response = await ImagePicker.requestCameraPermissionsAsync(); - permissionGranted = response.granted; - } else { - return { askToOpenSettings: true, cancelled: true }; - } - } - - if (permissionGranted) { - const imagePickerSuccessResult = await ImagePicker.launchCameraAsync({ - quality: Math.min(Math.max(0, compressImageQuality), 1), - }); - const canceled = imagePickerSuccessResult.canceled; - const assets = imagePickerSuccessResult.assets; - // since we only support single photo upload for now we will only be focusing on 0'th element. - const photo = assets && assets[0]; - - if (canceled === false && photo && photo.height && photo.width && photo.uri) { - let size: Size = {}; - if (Platform.OS === 'android') { - // Height and width returned by ImagePicker are incorrect on Android. - // The issue is described in following github issue: - // https://github.com/ivpusic/react-native-image-crop-picker/issues/901 - // This we can't rely on them as it is, and we need to use Image.getSize - // to get accurate size. - const getSize = (): Promise => - new Promise((resolve) => { - Image.getSize(photo.uri, (width, height) => { - resolve({ height, width }); - }); - }); - - try { - const { height, width } = await getSize(); - size.height = height; - size.width = width; - } catch (e) { - console.warn('Error get image size of picture caputred from camera ', e); - } - } else { - size = { - height: photo.height, - width: photo.width, - }; - } - - return { - cancelled: false, - source: 'camera', - uri: photo.uri, - ...size, - }; - } - } - } catch (error) { - console.log(error); - } - return { cancelled: true }; -}; diff --git a/package/expo-package/src/index.js b/package/expo-package/src/index.js index 818c0a53a6..1c9bbe0a65 100644 --- a/package/expo-package/src/index.js +++ b/package/expo-package/src/index.js @@ -6,21 +6,22 @@ import { Audio, compressImage, deleteFile, - getLocalAssetUri, - getPhotos, - iOS14RefreshGallerySelection, NetInfo, - oniOS14GalleryLibrarySelectionChange, saveFile, Sound, - takePhoto, Video, } from './handlers'; import { + getLocalAssetUri, + getPhotos, + iOS14RefreshGallerySelection, + oniOS14GalleryLibrarySelectionChange, pickDocument, + pickImage, setClipboardString, shareImage, + takePhoto, triggerHaptic, } from './optionalDependencies'; @@ -35,6 +36,7 @@ registerNativeHandlers({ NetInfo, oniOS14GalleryLibrarySelectionChange, pickDocument, + pickImage, saveFile, SDK: 'stream-chat-expo', setClipboardString, diff --git a/package/expo-package/src/optionalDependencies/getLocalAssetUri.ts b/package/expo-package/src/optionalDependencies/getLocalAssetUri.ts new file mode 100644 index 0000000000..3d79743c9b --- /dev/null +++ b/package/expo-package/src/optionalDependencies/getLocalAssetUri.ts @@ -0,0 +1,22 @@ +let MediaLibrary; + +try { + MediaLibrary = require('expo-media-library'); +} catch (e) { + // do nothing +} + +if (!MediaLibrary) { + console.log( + 'expo-media-library is not installed. Please install it or you can choose to install expo-image-picker for native image picker.', + ); +} + +export const getLocalAssetUri = async (assetId: string): Promise => { + try { + const { localUri } = await MediaLibrary.getAssetInfoAsync(assetId); + return localUri; + } catch { + throw new Error('getLocalAssetUri Error'); + } +}; diff --git a/package/expo-package/src/optionalDependencies/getPhotos.ts b/package/expo-package/src/optionalDependencies/getPhotos.ts new file mode 100644 index 0000000000..b40c74513a --- /dev/null +++ b/package/expo-package/src/optionalDependencies/getPhotos.ts @@ -0,0 +1,61 @@ +let MediaLibrary; + +try { + MediaLibrary = require('expo-media-library'); +} catch (e) { + // do nothing +} + +if (!MediaLibrary) { + console.log( + 'expo-media-library is not installed. Please install it or you can choose to install expo-image-picker for native image picker.', + ); +} +import type { Asset } from 'stream-chat-react-native-core'; + +type ReturnType = { + assets: Array & { source: 'picker' }>; + endCursor: string | undefined; + hasNextPage: boolean; + iOSLimited: boolean; +}; + +export const getPhotos = MediaLibrary + ? async ({ after, first }): Promise => { + try { + // NOTE: + // should always check first before requesting permission + // because always requesting permission will cause + // the app to go to background even if it was granted + const { accessPrivileges, status } = await MediaLibrary.getPermissionsAsync(); + if (status !== 'granted') { + const { status: newStatus } = await MediaLibrary.requestPermissionsAsync(); + if (newStatus !== 'granted') { + throw new Error('getPhotos Error'); + } + } + const results = await MediaLibrary.getAssetsAsync({ + after, + first, + mediaType: [MediaLibrary.MediaType.photo, MediaLibrary.MediaType.video], + sortBy: [MediaLibrary.SortBy.modificationTime], + }); + const assets = results.assets.map((asset) => ({ + duration: asset.duration * 1000, + height: asset.height, + id: asset.id, + name: asset.filename, + source: 'picker' as const, + type: asset.mediaType, + uri: asset.uri, + width: asset.width, + })); + + const hasNextPage = results.hasNextPage; + const endCursor = results.endCursor; + return { assets, endCursor, hasNextPage, iOSLimited: accessPrivileges === 'limited' }; + } catch { + throw new Error('getPhotos Error'); + } + } + : null; diff --git a/package/expo-package/src/optionalDependencies/iOS14RefreshGallerySelection.ts b/package/expo-package/src/optionalDependencies/iOS14RefreshGallerySelection.ts new file mode 100644 index 0000000000..49bfe503cc --- /dev/null +++ b/package/expo-package/src/optionalDependencies/iOS14RefreshGallerySelection.ts @@ -0,0 +1,26 @@ +import { Platform } from 'react-native'; + +let MediaLibrary; + +try { + MediaLibrary = require('expo-media-library'); +} catch (e) { + // do nothing +} + +if (!MediaLibrary) { + console.log( + 'expo-media-library is not installed. Please install it or you can choose to install expo-image-picker for native image picker.', + ); +} + +const isAboveIOS14 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 14; + +export const iOS14RefreshGallerySelection = MediaLibrary + ? (): Promise => { + if (isAboveIOS14) { + return MediaLibrary.presentPermissionsPickerAsync(); + } + return Promise.resolve(); + } + : null; diff --git a/package/expo-package/src/optionalDependencies/index.ts b/package/expo-package/src/optionalDependencies/index.ts index 1dcd987add..ff1fd69741 100644 --- a/package/expo-package/src/optionalDependencies/index.ts +++ b/package/expo-package/src/optionalDependencies/index.ts @@ -3,3 +3,9 @@ export * from './shareImage'; export * from './pickDocument'; export * from './triggerHaptic'; export * from './Video'; +export * from './oniOS14GalleryLibrarySelectionChange'; +export * from './iOS14RefreshGallerySelection'; +export * from './getLocalAssetUri'; +export * from './getPhotos'; +export * from './pickImage'; +export * from './takePhoto'; diff --git a/package/expo-package/src/optionalDependencies/oniOS14GalleryLibrarySelectionChange.ts b/package/expo-package/src/optionalDependencies/oniOS14GalleryLibrarySelectionChange.ts new file mode 100644 index 0000000000..89721ac6a3 --- /dev/null +++ b/package/expo-package/src/optionalDependencies/oniOS14GalleryLibrarySelectionChange.ts @@ -0,0 +1,37 @@ +import { Platform } from 'react-native'; + +let MediaLibrary; + +try { + MediaLibrary = require('expo-media-library'); +} catch (e) { + // do nothing +} + +if (!MediaLibrary) { + console.log( + 'expo-media-library is not installed. Please install it or you can choose to install expo-image-picker for native image picker.', + ); +} + +const isAboveIOS14 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 14; + +export const oniOS14GalleryLibrarySelectionChange = MediaLibrary + ? ( + callback: () => void, + ): { + unsubscribe: () => void; + } => { + if (isAboveIOS14) { + const subscription = MediaLibrary.addListener(callback); + return { + unsubscribe: () => { + subscription.remove(); + }, + }; + } + return { + unsubscribe: () => {}, + }; + } + : null; diff --git a/package/expo-package/src/optionalDependencies/pickImage.ts b/package/expo-package/src/optionalDependencies/pickImage.ts new file mode 100644 index 0000000000..f2801a0395 --- /dev/null +++ b/package/expo-package/src/optionalDependencies/pickImage.ts @@ -0,0 +1,56 @@ +let ImagePicker; + +try { + ImagePicker = require('expo-image-picker'); +} catch (e) { + // do nothing +} + +if (!ImagePicker) { + console.log( + 'expo-image-picker is not installed. Installing this package will enable selecting photos through the native image picker, and thereby send it.', + ); +} + +export const pickImage = ImagePicker + ? async () => { + try { + const permissionCheck = await ImagePicker.getMediaLibraryPermissionsAsync(); + const canRequest = permissionCheck.canAskAgain; + let permissionGranted = permissionCheck.granted; + if (!permissionGranted) { + if (canRequest) { + const response = await ImagePicker.requestMediaLibraryPermissionsAsync(); + permissionGranted = response.granted; + } else { + return { askToOpenSettings: true, cancelled: true }; + } + } + if (permissionGranted) { + const result = await ImagePicker.launchImageLibraryAsync({ + allowsMultipleSelection: true, + mediaTypes: ImagePicker.MediaTypeOptions.All, + }); + + const canceled = result.canceled; + + if (!canceled) { + const assets = result.assets.map((asset) => ({ + ...asset, + duration: asset.duration, + name: asset.fileName, + size: asset.fileSize, + source: 'picker', + type: asset.mimeType, + uri: asset.uri, + })); + return { assets, cancelled: false, source: 'picker' }; + } else { + return { cancelled: true }; + } + } + } catch (error) { + console.log('Error while picking image', error); + } + } + : null; diff --git a/package/expo-package/src/optionalDependencies/takePhoto.ts b/package/expo-package/src/optionalDependencies/takePhoto.ts new file mode 100644 index 0000000000..2fed2c85fa --- /dev/null +++ b/package/expo-package/src/optionalDependencies/takePhoto.ts @@ -0,0 +1,88 @@ +import { Image, Platform } from 'react-native'; + +let ImagePicker; + +try { + ImagePicker = require('expo-image-picker'); +} catch (e) { + // do nothing +} + +if (!ImagePicker) { + console.log( + 'expo-image-picker is not installed. Installing this package will enable campturing photos through the app, and thereby send it.', + ); +} + +type Size = { + height?: number; + width?: number; +}; + +export const takePhoto = ImagePicker + ? async ({ compressImageQuality = 1 }) => { + try { + const permissionCheck = await ImagePicker.getCameraPermissionsAsync(); + const canRequest = permissionCheck.canAskAgain; + let permissionGranted = permissionCheck.granted; + if (!permissionGranted) { + if (canRequest) { + const response = await ImagePicker.requestCameraPermissionsAsync(); + permissionGranted = response.granted; + } else { + return { askToOpenSettings: true, cancelled: true }; + } + } + + if (permissionGranted) { + const imagePickerSuccessResult = await ImagePicker.launchCameraAsync({ + quality: Math.min(Math.max(0, compressImageQuality), 1), + }); + const canceled = imagePickerSuccessResult.canceled; + const assets = imagePickerSuccessResult.assets; + // since we only support single photo upload for now we will only be focusing on 0'th element. + const photo = assets && assets[0]; + + if (canceled === false && photo && photo.height && photo.width && photo.uri) { + let size: Size = {}; + if (Platform.OS === 'android') { + // Height and width returned by ImagePicker are incorrect on Android. + // The issue is described in following github issue: + // https://github.com/ivpusic/react-native-image-crop-picker/issues/901 + // This we can't rely on them as it is, and we need to use Image.getSize + // to get accurate size. + const getSize = (): Promise => + new Promise((resolve) => { + Image.getSize(photo.uri, (width, height) => { + resolve({ height, width }); + }); + }); + + try { + const { height, width } = await getSize(); + size.height = height; + size.width = width; + } catch (e) { + console.warn('Error get image size of picture caputred from camera ', e); + } + } else { + size = { + height: photo.height, + width: photo.width, + }; + } + + return { + cancelled: false, + source: 'camera', + uri: photo.uri, + ...size, + }; + } + } + } catch (error) { + console.log(error); + } + return { cancelled: true }; + } + : null; diff --git a/package/expo-package/yarn.lock b/package/expo-package/yarn.lock index 2a0e705521..1b47be9c03 100644 --- a/package/expo-package/yarn.lock +++ b/package/expo-package/yarn.lock @@ -1895,11 +1895,6 @@ expo-font@~10.0.5: dependencies: fontfaceobserver "^2.1.0" -expo-image-loader@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-4.1.1.tgz#efadbb17de1861106864820194900f336dd641b6" - integrity sha512-ciEHVokU0f6w0eTxdRxLCio6tskMsjxWIoV92+/ZD37qePUJYMfEphPhu1sruyvMBNR8/j5iyOvPFVGTfO8oxA== - expo-image-manipulator@^9.1.0: version "9.2.2" resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-9.2.2.tgz#0fc7d2032972961c0a5fb49511d230fe0788fa6f" @@ -1907,23 +1902,11 @@ expo-image-manipulator@^9.1.0: dependencies: expo-modules-core "~0.2.0" -expo-image-picker@^14.1.1: - version "14.1.1" - resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-14.1.1.tgz#181f1348ba6a43df7b87cee4a601d45c79b7c2d7" - integrity sha512-SvWtnkLW7jp5Ntvk3lVcRQmhFYja8psmiR7O6P/+7S6f4llt3vaFwb4I3+pUXqJxxpi7BHc2+95qOLf0SFOIag== - dependencies: - expo-image-loader "~4.1.0" - expo-keep-awake@~10.0.2: version "10.0.2" resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-10.0.2.tgz#706bda839782bb3e8ad4cbe43bde471a56368813" integrity sha512-Ro1lgyKldbFs4mxhWM+goX9sg0S2SRR8FiJJeOvaRzf8xNhrZfWA00Zpr+/3ocCoWQ3eEL+X9UF4PXXHf0KoOg== -expo-media-library@~15.2.3: - version "15.2.3" - resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-15.2.3.tgz#188f3c77f58b354f0ea6250f6756ac1e1a226291" - integrity sha512-Oz8b8Xsvfj7YcutUBtI84NUIqSnt7iCM5HZ5DyKoWKKiDK/+aUuj3RXNQELG8jUw6pQPgEwgbZ1+J8SdH/y9jw== - expo-modules-autolinking@0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-0.5.5.tgz#6bcc42072dcbdfca79d207b7f549f1fdb54a2b74" diff --git a/package/native-package/package.json b/package/native-package/package.json index 6a1b0fa591..a471c110cb 100644 --- a/package/native-package/package.json +++ b/package/native-package/package.json @@ -15,20 +15,24 @@ }, "peerDependencies": { "@react-native-camera-roll/camera-roll": ">=5.0.0", - "@react-native-community/netinfo": ">=2.0.7", "@react-native-clipboard/clipboard": "^1.11.1", + "@react-native-community/netinfo": ">=2.0.7", "@stream-io/flat-list-mvcp": "^0.10.3", "react-native": ">=0.60.0", + "react-native-audio-recorder-player": ">=3.6.4", "react-native-document-picker": ">=9.0.1", "react-native-fs": ">=2.16.6", "react-native-haptic-feedback": ">=1.11.0", + "react-native-image-picker": ">=7.1.2", "react-native-image-crop-picker": ">=0.33.2", "react-native-image-resizer": ">=1.4.2", "react-native-share": ">=4.1.0", - "react-native-audio-recorder-player": ">=3.6.4", "react-native-video": ">=6.4.2" }, "peerDependenciesMeta": { + "@react-native-camera-roll/camera-roll": { + "optional": true + }, "@react-native-clipboard/clipboard": { "optional": true }, @@ -41,6 +45,12 @@ "react-native-haptic-feedback": { "optional": true }, + "react-native-image-picker": { + "optional": true + }, + "react-native-image-crop-picker": { + "optional": true + }, "react-native-audio-recorder-player": { "optional": true }, @@ -53,12 +63,10 @@ "postpack": "rm README.md" }, "devDependencies": { - "@react-native-camera-roll/camera-roll": "^5.0.2", "@react-native-community/netinfo": ">=2.0.7", "@stream-io/flat-list-mvcp": "0.10.3", "react-native": ">=0.60.0", "react-native-fs": ">=2.16.6", - "react-native-image-crop-picker": "^0.38.0", "react-native-image-resizer": ">=1.4.2" } } diff --git a/package/native-package/src/handlers/getLocalAssetUri.ts b/package/native-package/src/handlers/getLocalAssetUri.ts deleted file mode 100644 index d8e28820a5..0000000000 --- a/package/native-package/src/handlers/getLocalAssetUri.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { CameraRoll } from '@react-native-camera-roll/camera-roll'; - -export const getLocalAssetUri = async (remoteUri: string) => { - try { - const localUri = await CameraRoll.save(remoteUri); - return localUri; - } catch { - throw new Error('getLocalAssetUri Error'); - } -}; diff --git a/package/native-package/src/handlers/iOS14RefreshGallerySelection.ts b/package/native-package/src/handlers/iOS14RefreshGallerySelection.ts deleted file mode 100644 index 7fc833fa47..0000000000 --- a/package/native-package/src/handlers/iOS14RefreshGallerySelection.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Platform } from 'react-native'; - -import { iosRefreshGallerySelection } from '@react-native-camera-roll/camera-roll'; - -const isAboveIOS14 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 14; - -export const iOS14RefreshGallerySelection = (): Promise => { - if (isAboveIOS14) { - return iosRefreshGallerySelection().then(() => { - //do nothing - }); - } - return Promise.resolve(); -}; diff --git a/package/native-package/src/handlers/index.ts b/package/native-package/src/handlers/index.ts index 9c0e249db7..2b68f2d033 100644 --- a/package/native-package/src/handlers/index.ts +++ b/package/native-package/src/handlers/index.ts @@ -1,11 +1,6 @@ export * from './deleteFile'; export * from './compressImage'; -export * from './getLocalAssetUri'; -export * from './getPhotos'; export * from './NetInfo'; export * from './saveFile'; -export * from './takePhoto'; export * from './Sound'; export * from './Video'; -export * from './oniOS14GalleryLibrarySelectionChange'; -export * from './iOS14RefreshGallerySelection'; diff --git a/package/native-package/src/handlers/oniOS14GalleryLibrarySelectionChange.ts b/package/native-package/src/handlers/oniOS14GalleryLibrarySelectionChange.ts deleted file mode 100644 index 3931a24277..0000000000 --- a/package/native-package/src/handlers/oniOS14GalleryLibrarySelectionChange.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Platform } from 'react-native'; - -import { cameraRollEventEmitter } from '@react-native-camera-roll/camera-roll'; - -const isAboveIOS14 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 14; - -export function oniOS14GalleryLibrarySelectionChange(callback: () => void): { - unsubscribe: () => void; -} { - if (isAboveIOS14) { - const subscription = cameraRollEventEmitter.addListener('onLibrarySelectionChange', callback); - return { - unsubscribe: () => { - subscription.remove(); - }, - }; - } - return { - // eslint-disable-next-line @typescript-eslint/no-empty-function - unsubscribe: () => {}, - }; -} diff --git a/package/native-package/src/handlers/takePhoto.ts b/package/native-package/src/handlers/takePhoto.ts deleted file mode 100644 index 344b215291..0000000000 --- a/package/native-package/src/handlers/takePhoto.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { AppState, Image, PermissionsAndroid, Platform } from 'react-native'; -import ImagePicker from 'react-native-image-crop-picker'; - -export const takePhoto = async ({ compressImageQuality = Platform.OS === 'ios' ? 0.8 : 1 }) => { - if (Platform.OS === 'android') { - const cameraPermissions = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.CAMERA); - if (!cameraPermissions) { - const androidPermissionStatus = await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.CAMERA, - ); - if (androidPermissionStatus === PermissionsAndroid.RESULTS.DENIED) { - return { cancelled: true }; - } else if (androidPermissionStatus === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) { - return { askToOpenSettings: true, cancelled: true }; - } - } - } - try { - const photo = await ImagePicker.openCamera({ - compressImageQuality: Math.min(Math.max(0, compressImageQuality), 1), - }); - if (photo.height && photo.width && photo.path) { - let size: { height?: number; width?: number } = {}; - if (Platform.OS === 'android') { - // Height and width returned by ImagePicker are incorrect on Android. - // The issue is described in following github issue: - // https://github.com/ivpusic/react-native-image-crop-picker/issues/901 - // This we can't rely on them as it is, and we need to use Image.getSize - // to get accurate size. - const getSize = (): Promise<{ height: number; width: number }> => - new Promise((resolve) => { - Image.getSize(photo.path, (width, height) => { - resolve({ height, width }); - }); - }); - - try { - const { height, width } = await getSize(); - size.height = height; - size.width = width; - } catch (e) { - // do nothing - console.warn('Error get image size of picture caputred from camera ', e); - } - } else { - size = { - height: photo.height, - width: photo.width, - }; - } - return { - cancelled: false, - source: 'camera', - uri: photo.path, - ...size, - }; - } - } catch (e: unknown) { - if (e instanceof Error) { - // on iOS: if it was in inactive state, then the user had just denied the permissions - if (Platform.OS === 'ios' && AppState.currentState === 'active') { - const cameraPermissionDeniedMsg = 'User did not grant camera permission.'; - // Open settings when the user did not allow camera permissions - if (e.message === cameraPermissionDeniedMsg) { - return { askToOpenSettings: true, cancelled: true }; - } - } - } - } - - return { cancelled: true }; -}; diff --git a/package/native-package/src/index.js b/package/native-package/src/index.js index 1c10124c3e..a2f7ba1437 100644 --- a/package/native-package/src/index.js +++ b/package/native-package/src/index.js @@ -3,25 +3,19 @@ import { Platform } from 'react-native'; import { FlatList } from '@stream-io/flat-list-mvcp'; import { registerNativeHandlers } from 'stream-chat-react-native-core'; +import { compressImage, deleteFile, NetInfo, saveFile, Sound, Video } from './handlers'; + import { - compressImage, - deleteFile, + Audio, getLocalAssetUri, getPhotos, iOS14RefreshGallerySelection, - NetInfo, oniOS14GalleryLibrarySelectionChange, - saveFile, - Sound, - takePhoto, - Video, -} from './handlers'; - -import { - Audio, pickDocument, + pickImage, setClipboardString, shareImage, + takePhoto, triggerHaptic, } from './optionalDependencies'; @@ -36,6 +30,7 @@ registerNativeHandlers({ NetInfo, oniOS14GalleryLibrarySelectionChange, pickDocument, + pickImage, saveFile, SDK: 'stream-chat-react-native', setClipboardString, diff --git a/package/native-package/src/optionalDependencies/getLocalAssetUri.ts b/package/native-package/src/optionalDependencies/getLocalAssetUri.ts new file mode 100644 index 0000000000..bf7f1f627a --- /dev/null +++ b/package/native-package/src/optionalDependencies/getLocalAssetUri.ts @@ -0,0 +1,21 @@ +let CameraRollDependency; + +try { + CameraRollDependency = require('@react-native-camera-roll/camera-roll'); +} catch (e) { + // do nothing + console.log( + '@react-native-camera-roll/camera-roll is not installed. Please install it or you can choose to install react-native-image-crop-picker for native image picker.', + ); +} + +export const getLocalAssetUri = CameraRollDependency + ? async (remoteUri: string) => { + try { + const localUri = await CameraRollDependency.CameraRoll.save(remoteUri); + return localUri; + } catch { + throw new Error('getLocalAssetUri Error'); + } + } + : null; diff --git a/package/native-package/src/handlers/getPhotos.ts b/package/native-package/src/optionalDependencies/getPhotos.ts similarity index 60% rename from package/native-package/src/handlers/getPhotos.ts rename to package/native-package/src/optionalDependencies/getPhotos.ts index 650cfd59ff..6dfa883518 100644 --- a/package/native-package/src/handlers/getPhotos.ts +++ b/package/native-package/src/optionalDependencies/getPhotos.ts @@ -1,6 +1,16 @@ import { PermissionsAndroid, Platform } from 'react-native'; -import { CameraRoll, GetPhotosParams } from '@react-native-camera-roll/camera-roll'; +let CameraRollDependency; + +try { + CameraRollDependency = require('@react-native-camera-roll/camera-roll'); +} catch (e) { + // do nothing + console.log( + '@react-native-camera-roll/camera-roll is not installed. Please install it or you can choose to install react-native-image-crop-picker for native image picker.', + ); +} + import type { Asset } from 'stream-chat-react-native-core'; type ReturnType = { @@ -57,36 +67,35 @@ const verifyAndroidPermissions = async () => { return true; }; -export const getPhotos = async ({ - after, - first, -}: Pick): Promise => { - try { - if (Platform.OS === 'android') { - const granted = await verifyAndroidPermissions(); - if (!granted) { +export const getPhotos = CameraRollDependency + ? async ({ after, first }): Promise => { + try { + if (Platform.OS === 'android') { + const granted = await verifyAndroidPermissions(); + if (!granted) { + throw new Error('getPhotos Error'); + } + } + const results = await CameraRollDependency.CameraRoll.getPhotos({ + after, + assetType: 'All', + first, + include: ['fileSize', 'filename', 'imageSize', 'playableDuration'], + }); + const assets = results.edges.map((edge) => ({ + ...edge.node.image, + duration: edge.node.image.playableDuration * 1000, + // since we include filename, fileSize in the query, we can safely assume it will be defined + name: edge.node.image.filename as string, + size: edge.node.image.fileSize as number, + source: 'picker' as const, + type: edge.node.type, + })); + const hasNextPage = results.page_info.has_next_page; + const endCursor = results.page_info.end_cursor; + return { assets, endCursor, hasNextPage, iOSLimited: !!results.limited }; + } catch (_error) { throw new Error('getPhotos Error'); } } - const results = await CameraRoll.getPhotos({ - after, - assetType: 'All', - first, - include: ['fileSize', 'filename', 'imageSize', 'playableDuration'], - }); - const assets = results.edges.map((edge) => ({ - ...edge.node.image, - duration: edge.node.image.playableDuration, - // since we include filename, fileSize in the query, we can safely assume it will be defined - name: edge.node.image.filename as string, - size: edge.node.image.fileSize as number, - source: 'picker' as const, - type: edge.node.type, - })); - const hasNextPage = results.page_info.has_next_page; - const endCursor = results.page_info.end_cursor; - return { assets, endCursor, hasNextPage, iOSLimited: !!results.limited }; - } catch (_error) { - throw new Error('getPhotos Error'); - } -}; + : null; diff --git a/package/native-package/src/optionalDependencies/iOS14RefreshGallerySelection.ts b/package/native-package/src/optionalDependencies/iOS14RefreshGallerySelection.ts new file mode 100644 index 0000000000..2610048bfa --- /dev/null +++ b/package/native-package/src/optionalDependencies/iOS14RefreshGallerySelection.ts @@ -0,0 +1,25 @@ +import { Platform } from 'react-native'; + +let CameraRollDependency; + +try { + CameraRollDependency = require('@react-native-camera-roll/camera-roll'); +} catch (e) { + // do nothing + console.log( + '@react-native-camera-roll/camera-roll is not installed. Please install it or you can choose to install react-native-image-crop-picker for native image picker.', + ); +} + +const isAboveIOS14 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 14; + +export const iOS14RefreshGallerySelection = CameraRollDependency + ? (): Promise => { + if (isAboveIOS14) { + return CameraRollDependency.iosRefreshGallerySelection().then(() => { + //do nothing + }); + } + return Promise.resolve(); + } + : null; diff --git a/package/native-package/src/optionalDependencies/index.ts b/package/native-package/src/optionalDependencies/index.ts index fbb2228b1f..6250c971aa 100644 --- a/package/native-package/src/optionalDependencies/index.ts +++ b/package/native-package/src/optionalDependencies/index.ts @@ -4,3 +4,9 @@ export * from './Video'; export * from './triggerHaptic'; export * from './setClipboardString'; export * from './pickDocument'; +export * from './getLocalAssetUri'; +export * from './iOS14RefreshGallerySelection'; +export * from './oniOS14GalleryLibrarySelectionChange'; +export * from './getPhotos'; +export * from './pickImage'; +export * from './takePhoto'; diff --git a/package/native-package/src/optionalDependencies/oniOS14GalleryLibrarySelectionChange.ts b/package/native-package/src/optionalDependencies/oniOS14GalleryLibrarySelectionChange.ts new file mode 100644 index 0000000000..546960cec6 --- /dev/null +++ b/package/native-package/src/optionalDependencies/oniOS14GalleryLibrarySelectionChange.ts @@ -0,0 +1,37 @@ +import { Platform } from 'react-native'; + +let CameraRollDependency; + +try { + CameraRollDependency = require('@react-native-camera-roll/camera-roll'); +} catch (e) { + // do nothing + console.log( + '@react-native-camera-roll/camera-roll is not installed. Please install it or you can choose to install react-native-image-crop-picker for native image picker.', + ); +} + +const isAboveIOS14 = Platform.OS === 'ios' && parseInt(Platform.Version as string, 10) >= 14; + +export const oniOS14GalleryLibrarySelectionChange = CameraRollDependency + ? ( + callback: () => void, + ): { + unsubscribe: () => void; + } => { + if (isAboveIOS14) { + const subscription = CameraRollDependency.cameraRollEventEmitter.addListener( + 'onLibrarySelectionChange', + callback, + ); + return { + unsubscribe: () => { + subscription.remove(); + }, + }; + } + return { + unsubscribe: () => {}, + }; + } + : null; diff --git a/package/native-package/src/optionalDependencies/pickImage.ts b/package/native-package/src/optionalDependencies/pickImage.ts new file mode 100644 index 0000000000..4d4cb29c3e --- /dev/null +++ b/package/native-package/src/optionalDependencies/pickImage.ts @@ -0,0 +1,38 @@ +import { Platform } from 'react-native'; +let ImagePicker; + +try { + ImagePicker = require('react-native-image-picker'); +} catch (e) { + console.log('react-native-image-picker is not installed'); +} + +export const pickImage = ImagePicker + ? async () => { + try { + const result = await ImagePicker.launchImageLibrary({ mediaType: 'mixed' }); + const canceled = result.didCancel; + const errorCode = result.errorCode; + + if (Platform.OS === 'ios' && errorCode === 'permission') { + return { askToOpenSettings: true, cancelled: true }; + } + if (!canceled) { + const assets = result.assets.map((asset) => ({ + ...asset, + duration: asset.duration * 1000, // in milliseconds + name: asset.fileName, + size: asset.fileSize, + source: 'picker', + type: asset.type, + uri: asset.uri, + })); + return { assets, cancelled: false, source: 'picker' }; + } else { + return { cancelled: true }; + } + } catch (error) { + console.log('Error picking image: ', error); + } + } + : null; diff --git a/package/native-package/src/optionalDependencies/takePhoto.ts b/package/native-package/src/optionalDependencies/takePhoto.ts new file mode 100644 index 0000000000..33b2e694b8 --- /dev/null +++ b/package/native-package/src/optionalDependencies/takePhoto.ts @@ -0,0 +1,83 @@ +import { AppState, Image, PermissionsAndroid, Platform } from 'react-native'; + +let ImagePicker; + +try { + ImagePicker = require('react-native-image-crop-picker').default; +} catch (e) { + console.log('react-native-image-crop-picker is not installed'); +} + +export const takePhoto = ImagePicker + ? async ({ compressImageQuality = Platform.OS === 'ios' ? 0.8 : 1 }) => { + if (Platform.OS === 'android') { + const cameraPermissions = await PermissionsAndroid.check( + PermissionsAndroid.PERMISSIONS.CAMERA, + ); + if (!cameraPermissions) { + const androidPermissionStatus = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.CAMERA, + ); + if (androidPermissionStatus === PermissionsAndroid.RESULTS.DENIED) { + return { cancelled: true }; + } else if (androidPermissionStatus === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) { + return { askToOpenSettings: true, cancelled: true }; + } + } + } + try { + const photo = await ImagePicker.openCamera({ + compressImageQuality: Math.min(Math.max(0, compressImageQuality), 1), + }); + if (photo.height && photo.width && photo.path) { + let size: { height?: number; width?: number } = {}; + if (Platform.OS === 'android') { + // Height and width returned by ImagePicker are incorrect on Android. + // The issue is described in following github issue: + // https://github.com/ivpusic/react-native-image-crop-picker/issues/901 + // This we can't rely on them as it is, and we need to use Image.getSize + // to get accurate size. + const getSize = (): Promise<{ height: number; width: number }> => + new Promise((resolve) => { + Image.getSize(photo.path, (width, height) => { + resolve({ height, width }); + }); + }); + + try { + const { height, width } = await getSize(); + size.height = height; + size.width = width; + } catch (e) { + // do nothing + console.warn('Error get image size of picture caputred from camera ', e); + } + } else { + size = { + height: photo.height, + width: photo.width, + }; + } + return { + cancelled: false, + source: 'camera', + uri: photo.path, + ...size, + }; + } + } catch (e: unknown) { + if (e instanceof Error) { + // on iOS: if it was in inactive state, then the user had just denied the permissions + if (Platform.OS === 'ios' && AppState.currentState === 'active') { + const cameraPermissionDeniedMsg = 'User did not grant camera permission.'; + // Open settings when the user did not allow camera permissions + if (e.message === cameraPermissionDeniedMsg) { + return { askToOpenSettings: true, cancelled: true }; + } + } + } + } + + return { cancelled: true }; + } + : null; diff --git a/package/native-package/yarn.lock b/package/native-package/yarn.lock index 7dd2c23696..38395ed266 100644 --- a/package/native-package/yarn.lock +++ b/package/native-package/yarn.lock @@ -851,11 +851,6 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@react-native-camera-roll/camera-roll@^5.0.2": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.6.0.tgz#385082d57d694f3fd5ae386f8b8ce24b0969c5f9" - integrity sha512-a/GYwnBTxj1yKWB9m/qy8GzjowSocML8NbLT81wdMh0JzZYXCLze51BR2cb8JNDgRPzA9xe7KpD3j9qQOSOjag== - "@react-native-community/cli-clean@^10.1.1": version "10.1.1" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-10.1.1.tgz#4c73ce93a63a24d70c0089d4025daac8184ff504" @@ -3743,11 +3738,6 @@ react-native-gradle-plugin@^0.71.18: resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.18.tgz#20ef199bc85be32e45bb6cc069ec2e7dcb1a74a6" integrity sha512-7F6bD7B8Xsn3JllxcwHhFcsl9aHIig47+3eN4IHFNqfLhZr++3ElDrcqfMzugM+niWbaMi7bJ0kAkAL8eCpdWg== -react-native-image-crop-picker@^0.38.0: - version "0.38.1" - resolved "https://registry.yarnpkg.com/react-native-image-crop-picker/-/react-native-image-crop-picker-0.38.1.tgz#5973b4a8b55835b987e6be2064de411e849ac005" - integrity sha512-cF5UQnWplzHCeiCO+aiGS/0VomWaLmFf3nSsgTMPfY+8+99h8N/eHQvVdSF7RsGw50B8394wGeGyqHjjp8YRWw== - react-native-image-resizer@>=1.4.2: version "1.4.5" resolved "https://registry.yarnpkg.com/react-native-image-resizer/-/react-native-image-resizer-1.4.5.tgz#5a520aa8baa07638b1894a1d87d4d9a0945c8d58" diff --git a/package/src/components/Attachment/Attachment.tsx b/package/src/components/Attachment/Attachment.tsx index e7ace05600..cf743e483c 100644 --- a/package/src/components/Attachment/Attachment.tsx +++ b/package/src/components/Attachment/Attachment.tsx @@ -13,7 +13,7 @@ import { } from '../../contexts/messagesContext/MessagesContext'; import { isVideoPackageAvailable } from '../../native'; -import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../types/types'; export type ActionHandler = (name: string, value: string) => void; @@ -55,7 +55,7 @@ const AttachmentWithContext = < const hasAttachmentActions = !!attachment.actions?.length; - if (attachment.type === 'giphy' || attachment.type === 'imgur') { + if (attachment.type === FileTypes.Giphy || attachment.type === FileTypes.Imgur) { return ; } @@ -63,7 +63,7 @@ const AttachmentWithContext = < return ; } - if (attachment.type === 'image') { + if (attachment.type === FileTypes.Image) { return ( <> @@ -74,7 +74,7 @@ const AttachmentWithContext = < ); } - if (attachment.type === 'video' && !attachment.og_scrape_url) { + if (attachment.type === FileTypes.Video && !attachment.og_scrape_url) { return isVideoPackageAvailable() ? ( <> @@ -88,9 +88,9 @@ const AttachmentWithContext = < } if ( - attachment.type === 'file' || - attachment.type === 'audio' || - attachment.type === 'voiceRecording' + attachment.type === FileTypes.File || + attachment.type === FileTypes.Audio || + attachment.type === FileTypes.VoiceRecording ) { return ; } diff --git a/package/src/components/Attachment/Card.tsx b/package/src/components/Attachment/Card.tsx index 3ac60f76fa..9bfaa1e0a6 100644 --- a/package/src/components/Attachment/Card.tsx +++ b/package/src/components/Attachment/Card.tsx @@ -27,7 +27,7 @@ import { } from '../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { Play } from '../../icons/Play'; -import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../types/types'; import { makeImageCompatibleUrl } from '../../utils/utils'; import { ImageBackground } from '../ImageBackground'; @@ -160,7 +160,7 @@ const CardWithContext = < const defaultOnPress = () => openUrlSafely(og_scrape_url || uri); - const isVideoCard = type === 'video' && og_scrape_url; + const isVideoCard = type === FileTypes.Video && og_scrape_url; return ( - {(file.type === 'audio' || file.type === 'voiceRecording') && + {(file.type === FileTypes.Audio || file.type === FileTypes.VoiceRecording) && isAudioPackageAvailable() ? ( { // If the url is defined then only try to open the file. if (thumbnail.url) { - if (thumbnail.type === 'video' && !isVideoPackageAvailable()) { + if (thumbnail.type === FileTypes.Video && !isVideoPackageAvailable()) { // This condition is kinda unreachable, since we render videos as file attachment if the video // library is not installed. But doesn't hurt to have extra safeguard, in case of some customizations. openUrlSafely(thumbnail.url); @@ -363,7 +363,7 @@ const GalleryThumbnail = < testID={`gallery-${invertedDirections ? 'row' : 'column'}-${colIndex}-item-${rowIndex}`} {...additionalTouchableProps} > - {thumbnail.type === 'video' ? ( + {thumbnail.type === FileTypes.Video ? ( - + {type?.toUpperCase()} diff --git a/package/src/components/Attachment/utils/getAspectRatio.ts b/package/src/components/Attachment/utils/getAspectRatio.ts index 1adbe15003..f6fb60e886 100644 --- a/package/src/components/Attachment/utils/getAspectRatio.ts +++ b/package/src/components/Attachment/utils/getAspectRatio.ts @@ -1,6 +1,6 @@ import type { Attachment } from 'stream-chat'; -import type { DefaultStreamChatGenerics } from '../../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../../types/types'; /** * Returns the aspect ratio of an image attachment. @@ -11,7 +11,7 @@ import type { DefaultStreamChatGenerics } from '../../../types/types'; export function getAspectRatio< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >(attachment: Attachment) { - if (!(attachment.type === 'image' || attachment.type === 'video')) { + if (!(attachment.type === FileTypes.Image || attachment.type === FileTypes.Video)) { throw new Error( 'getAspectRatio() can only be called on an image attachment or video thumbnail', ); diff --git a/package/src/components/AttachmentPicker/AttachmentPicker.tsx b/package/src/components/AttachmentPicker/AttachmentPicker.tsx index a4026c9224..55244e2568 100644 --- a/package/src/components/AttachmentPicker/AttachmentPicker.tsx +++ b/package/src/components/AttachmentPicker/AttachmentPicker.tsx @@ -118,6 +118,10 @@ export const AttachmentPicker = React.forwardRef( setLoadingPhotos(true); const endCursor = endCursorRef.current; try { + if (!getPhotos) { + setPhotos([]); + setIosLimited(false); + } const results = await getPhotos({ after: endCursor, first: numberOfAttachmentImagesToLoadPerCall ?? 60, @@ -141,6 +145,8 @@ export const AttachmentPicker = React.forwardRef( useEffect(() => { if (selectedPicker !== 'images') return; + + if (!oniOS14GalleryLibrarySelectionChange) return; // ios 14 library selection change event is fired when user reselects the images that are permitted to be readable by the app const { unsubscribe } = oniOS14GalleryLibrarySelectionChange(() => { // we reset the cursor and has next page to true to facilitate fetching of the first page of photos again diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx index f47a11cba9..3e3f9beabe 100644 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx +++ b/package/src/components/AttachmentPicker/components/AttachmentPickerError.tsx @@ -16,8 +16,8 @@ const styles = StyleSheet.create({ errorContainer: { alignItems: 'center', bottom: 0, + justifyContent: 'center', left: 0, - paddingTop: 16, position: 'absolute', right: 0, }, @@ -70,7 +70,7 @@ export const AttachmentPickerError = (props: AttachmentPickerErrorProps) => { styles.errorContainer, { backgroundColor: white_smoke, - height: attachmentPickerBottomSheetHeight ?? 308, + height: attachmentPickerBottomSheetHeight, }, errorContainer, ]} diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx index 3c2088ec07..adbcf666e8 100644 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx +++ b/package/src/components/AttachmentPicker/components/AttachmentPickerIOSSelectMorePhotos.tsx @@ -12,6 +12,9 @@ export const AttachmentPickerIOSSelectMorePhotos = () => { colors: { accent_blue, white }, }, } = useTheme(); + + if (!iOS14RefreshGallerySelection) return null; + return ( { const { duration: videoDuration, uri } = asset; - const ONE_HOUR_IN_SECONDS = 3600; - - let durationLabel = '00:00'; - - if (videoDuration) { - const isDurationLongerThanHour = videoDuration / ONE_HOUR_IN_SECONDS >= 1; - const formattedDurationParam = isDurationLongerThanHour ? 'HH:mm:ss' : 'mm:ss'; - const formattedVideoDuration = dayjs - .duration(videoDuration, 'second') - .format(formattedDurationParam); - durationLabel = formattedVideoDuration; - } + const durationLabel = getDurationLabelFromDuration(videoDuration); const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; /* Patches video files with uri and mimetype */ const patchVideoFile = async (files: File[]) => { // For the case of Expo CLI where you need to fetch the file uri from file id. Here it is only done for iOS since for android the file.uri is fine. - const localAssetURI = Platform.OS === 'ios' && asset.id && (await getLocalAssetUri(asset.id)); + const localAssetURI = + Platform.OS === 'ios' && asset.id && getLocalAssetUri && (await getLocalAssetUri(asset.id)); const uri = localAssetURI || asset.uri || ''; // We need a mime-type to upload a video file. const mimeType = lookup(asset.name) || 'multipart/form-data'; @@ -126,7 +116,7 @@ const AttachmentVideo = (props: AttachmentVideoProps) => { {videoDuration ? ( - + {durationLabel} ) : null} @@ -162,7 +152,8 @@ const AttachmentImage = (props: AttachmentImageProps) => { /* Patches image files with uri */ const patchImageFile = async (images: Asset[]) => { // For the case of Expo CLI where you need to fetch the file uri from file id. Here it is only done for iOS since for android the file.uri is fine. - const localAssetURI = Platform.OS === 'ios' && asset.id && (await getLocalAssetUri(asset.id)); + const localAssetURI = + Platform.OS === 'ios' && asset.id && getLocalAssetUri && (await getLocalAssetUri(asset.id)); const uri = localAssetURI || asset.uri || ''; return [ ...images, diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx index 12419744ff..881d366906 100644 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx +++ b/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx @@ -1,12 +1,9 @@ import React from 'react'; -import { Alert, Linking, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; import { useAttachmentPickerContext } from '../../../contexts/attachmentPickerContext/AttachmentPickerContext'; import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; - -import { takePhoto } from '../../../native'; const styles = StyleSheet.create({ container: { @@ -27,12 +24,11 @@ export const AttachmentPickerSelectionBar = () => { FileSelectorIcon, ImageSelectorIcon, selectedPicker, - setSelectedImages, setSelectedPicker, } = useAttachmentPickerContext(); - const { t } = useTranslationContext(); - const { compressImageQuality, hasFilePicker, imageUploads, pickFile } = useMessageInputContext(); + const { hasCameraPicker, hasFilePicker, imageUploads, pickFile, takeAndUploadImage } = + useMessageInputContext(); const { theme: { @@ -40,12 +36,12 @@ export const AttachmentPickerSelectionBar = () => { }, } = useTheme(); - const setPicker = (selection: 'images') => { - if (selectedPicker === selection) { + const setImagePicker = () => { + if (selectedPicker === 'images') { setSelectedPicker(undefined); closePicker(); } else { - setSelectedPicker(selection); + setSelectedPicker('images'); } }; @@ -55,30 +51,11 @@ export const AttachmentPickerSelectionBar = () => { pickFile(); }; - const takeAndUploadImage = async () => { - setSelectedPicker(undefined); - closePicker(); - const photo = await takePhoto({ compressImageQuality }); - if (photo.askToOpenSettings) { - Alert.alert( - t('Allow camera access in device settings'), - t('Device camera is used to take photos or videos.'), - [ - { style: 'cancel', text: t('Cancel') }, - { onPress: () => Linking.openSettings(), style: 'default', text: t('Open Settings') }, - ], - ); - } - if (!photo.cancelled) { - setSelectedImages((images) => [...images, photo]); - } - }; - return ( setPicker('images')} + onPress={setImagePicker} testID='upload-photo-touchable' > @@ -88,7 +65,7 @@ export const AttachmentPickerSelectionBar = () => { /> - {hasFilePicker && ( + {hasFilePicker ? ( { /> - )} - - - - - + ) : null} + {hasCameraPicker ? ( + + + + + + ) : null} ); }; diff --git a/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx b/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx index 4c6204d63f..12dede272e 100644 --- a/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx +++ b/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx @@ -35,7 +35,7 @@ import type { Trigger } from '../../utils/utils'; const styles = StyleSheet.create({ inputBox: { flex: 1, - fontSize: 14, + fontSize: 16, includeFontPadding: false, // for android vertical text centering padding: 0, // removal of default text input padding on android paddingTop: 0, // removal of iOS top padding for weird centering diff --git a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx index 9a8a579b6b..fbd1dabf17 100644 --- a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx +++ b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionCommandIcon.tsx @@ -80,7 +80,7 @@ export const AutoCompleteSuggestionCommandIcon = < default: return ( - + ); } diff --git a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionHeader.tsx b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionHeader.tsx index 4d77f773af..2fd38d9a6f 100644 --- a/package/src/components/AutoCompleteInput/AutoCompleteSuggestionHeader.tsx +++ b/package/src/components/AutoCompleteInput/AutoCompleteSuggestionHeader.tsx @@ -46,7 +46,7 @@ const AutoCompleteSuggestionHeaderWithContext = < if (triggerType === 'command') { return ( - + {t('Instant Commands')} diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index e8bebdb225..ed0f2879f3 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -75,9 +75,9 @@ import { ThumbsUpReaction, WutReaction, } from '../../icons'; -import { FlatList as FlatListDefault, pickDocument } from '../../native'; +import { FlatList as FlatListDefault, isImagePickerAvailable, pickDocument } from '../../native'; import * as dbApi from '../../store/apis'; -import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../types/types'; import { addReactionToLocalState } from '../../utils/addReactionToLocalState'; import { compressedImageURI } from '../../utils/compressImage'; import { DBSyncManager } from '../../utils/DBSyncManager'; @@ -480,6 +480,7 @@ const ChannelWithContext = < Giphy = GiphyDefault, giphyEnabled, giphyVersion = 'fixed_height', + handleAttachButtonPress, handleBlock, handleCopy, handleDelete, @@ -491,6 +492,7 @@ const ChannelWithContext = < handleReaction, handleRetry, handleThreadReply, + hasCameraPicker = isImagePickerAvailable(), hasCommands = true, // If pickDocument isn't available, default to hiding the file picker hasFilePicker = pickDocument !== null, @@ -1570,7 +1572,7 @@ const ChannelWithContext = < const file = attachment.originalFile; // check if image_url is not a remote url if ( - attachment.type === 'image' && + attachment.type === FileTypes.Image && image?.uri && attachment.image_url && isLocalUrl(attachment.image_url) @@ -1598,10 +1600,10 @@ const ChannelWithContext = < } if ( - (attachment.type === 'file' || - attachment.type === 'audio' || - attachment.type === 'voiceRecording' || - attachment.type === 'video') && + (attachment.type === FileTypes.File || + attachment.type === FileTypes.Audio || + attachment.type === FileTypes.VoiceRecording || + attachment.type === FileTypes.Video) && attachment.asset_url && isLocalUrl(attachment.asset_url) && file?.uri @@ -2219,13 +2221,14 @@ const ChannelWithContext = < CommandsButton, compressImageQuality, CooldownTimer, - disabled: disabledValue, doDocUploadRequest, doImageUploadRequest, editing, editMessage, emojiSearchIndex, FileUploadPreview, + handleAttachButtonPress, + hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, diff --git a/package/src/components/Channel/__tests__/ownCapabilities.test.js b/package/src/components/Channel/__tests__/ownCapabilities.test.js index f0d7c68300..9e7ca51fd4 100644 --- a/package/src/components/Channel/__tests__/ownCapabilities.test.js +++ b/package/src/components/Channel/__tests__/ownCapabilities.test.js @@ -314,24 +314,6 @@ describe('Own capabilities', () => { }); }); - describe(`${allOwnCapabilities.sendMessage} capability`, () => { - it(`should not render SendMessageDisallowedIndicator when "${allOwnCapabilities.sendMessage}" capability is enabled`, async () => { - await generateChannelWithCapabilities([allOwnCapabilities.sendMessage]); - const { queryByTestId } = render(getComponent()); - - await waitFor(() => expect(!!queryByTestId('send-message-disallowed-indicator')).toBeFalsy()); - }); - - it(`should render SendMessageDisallowedIndicator when "${allOwnCapabilities.sendMessage}" capability is disabled`, async () => { - await generateChannelWithCapabilities(); - const { queryByTestId } = render(getComponent()); - - await waitFor(() => - expect(!!queryByTestId('send-message-disallowed-indicator')).toBeTruthy(), - ); - }); - }); - describe(`${allOwnCapabilities.sendLinks} capability`, () => { it(`should not allow sending links when "${allOwnCapabilities.sendLinks}" capability is disabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.sendMessage]); diff --git a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts index 73722b706d..ca353f915a 100644 --- a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts +++ b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts @@ -27,13 +27,14 @@ export const useCreateInputMessageInputContext = < CommandsButton, compressImageQuality, CooldownTimer, - disabled, doDocUploadRequest, doImageUploadRequest, editing, editMessage, emojiSearchIndex, FileUploadPreview, + handleAttachButtonPress, + hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, @@ -96,13 +97,14 @@ export const useCreateInputMessageInputContext = < CommandsButton, compressImageQuality, CooldownTimer, - disabled, doDocUploadRequest, doImageUploadRequest, editing, editMessage, emojiSearchIndex, FileUploadPreview, + handleAttachButtonPress, + hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, @@ -131,15 +133,7 @@ export const useCreateInputMessageInputContext = < StartAudioRecordingButton, UploadProgressIndicator, }), - [ - compressImageQuality, - channelId, - disabled, - editingDep, - initialValue, - maxMessageLength, - quotedMessageId, - ], + [compressImageQuality, channelId, editingDep, initialValue, maxMessageLength, quotedMessageId], ); return inputMessageInputContext; diff --git a/package/src/components/ImageGallery/ImageGallery.tsx b/package/src/components/ImageGallery/ImageGallery.tsx index ec5e610d26..9a0c95946a 100644 --- a/package/src/components/ImageGallery/ImageGallery.tsx +++ b/package/src/components/ImageGallery/ImageGallery.tsx @@ -48,7 +48,7 @@ import { import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useViewport } from '../../hooks/useViewport'; import { isVideoPackageAvailable, VideoType } from '../../native'; -import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../types/types'; import { getResizedImageUrl } from '../../utils/getResizedImageUrl'; import { getUrlOfImageAttachment } from '../../utils/getUrlOfImageAttachment'; import { getGiphyMimeType } from '../Attachment/utils/getGiphyMimeType'; @@ -239,15 +239,15 @@ export const ImageGallery = < cur.attachments ?.filter( (attachment) => - (attachment.type === 'giphy' && + (attachment.type === FileTypes.Giphy && (attachment.giphy?.[giphyVersion]?.url || attachment.thumb_url || attachment.image_url)) || - (attachment.type === 'image' && + (attachment.type === FileTypes.Image && !attachment.title_link && !attachment.og_scrape_url && getUrlOfImageAttachment(attachment)) || - (isVideoPackageAvailable() && attachment.type === 'video'), + (isVideoPackageAvailable() && attachment.type === FileTypes.Video), ) .reverse() || []; @@ -344,7 +344,7 @@ export const ImageGallery = < const imageHeight = Math.floor(height * (fullWindowWidth / width)); setCurrentImageHeight(imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight); } else if (photo?.uri) { - if (photo.type === 'image') { + if (photo.type === FileTypes.Image) { Image.getSize(photo.uri, (width, height) => { const imageHeight = Math.floor(height * (fullWindowWidth / width)); setCurrentImageHeight(imageHeight > fullWindowHeight ? fullWindowHeight : imageHeight); @@ -553,7 +553,7 @@ export const ImageGallery = < {imageGalleryAttachments.map((photo, i) => - photo.type === 'video' ? ( + photo.type === FileTypes.Video ? ( { const View = require('react-native/Libraries/Components/View/View'); return { + isImageMediaLibraryAvailable: jest.fn(() => true), isVideoPackageAvailable: jest.fn(() => true), NetInfo: { addEventListener: jest.fn(), @@ -82,7 +83,7 @@ describe('ImageGallery', () => { videoItemComponent, 'handleLoad', `photoId-${message.id}-${attachment.asset_url}`, - 10, + 10 * 1000, ); }); @@ -143,7 +144,7 @@ describe('ImageGallery', () => { videoItemComponent, 'handleProgress', `photoId-${message.id}-${attachment.asset_url}`, - 0.3, + 0.3 * 1000, ); }); @@ -169,7 +170,7 @@ describe('ImageGallery', () => { act(() => { fireEvent(videoItemComponent, 'handleLoad', { - duration: 10, + duration: 10 * 1000, }); fireEvent(videoItemComponent, 'handleProgress', { currentTime: undefined, @@ -202,7 +203,7 @@ describe('ImageGallery', () => { videoItemComponent, 'handleLoad', `photoId-${message.id}-${attachment.asset_url}`, - 10, + 10 * 1000, ); fireEvent(videoItemComponent, 'handleEnd'); }); diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx index 1f998c8111..0068c7dbeb 100644 --- a/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx +++ b/package/src/components/ImageGallery/__tests__/ImageGalleryFooter.test.tsx @@ -28,6 +28,7 @@ jest.mock('../../../native.ts', () => { const View = require('react-native/Libraries/Components/View/View'); return { deleteFile: jest.fn(), + isImageMediaLibraryAvailable: jest.fn(() => true), isVideoPackageAvailable: jest.fn(() => true), NetInfo: { addEventListener: jest.fn(), diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryHeader.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryHeader.test.tsx index 97c115887e..394c4204a6 100644 --- a/package/src/components/ImageGallery/__tests__/ImageGalleryHeader.test.tsx +++ b/package/src/components/ImageGallery/__tests__/ImageGalleryHeader.test.tsx @@ -30,6 +30,7 @@ import { ImageGallery, ImageGalleryCustomComponents } from '../ImageGallery'; jest.mock('../../../native.ts', () => { const View = require('react-native/Libraries/Components/View/View'); return { + isImageMediaLibraryAvailable: jest.fn(() => true), isVideoPackageAvailable: jest.fn(() => true), NetInfo: { addEventListener: jest.fn(), diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryVideoControl.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryVideoControl.test.tsx index 794df3538f..8abf44d5b3 100644 --- a/package/src/components/ImageGallery/__tests__/ImageGalleryVideoControl.test.tsx +++ b/package/src/components/ImageGallery/__tests__/ImageGalleryVideoControl.test.tsx @@ -54,7 +54,7 @@ describe('ImageGalleryOverlay', () => { render( getComponent({ - duration: 3600, + duration: 3600 * 1000, progress: 1, }), ); @@ -73,7 +73,7 @@ describe('ImageGalleryOverlay', () => { render( getComponent({ - duration: 60, + duration: 60 * 1000, progress: 0.5, }), ); diff --git a/package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx b/package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx index f7c553b64f..439cc2ed6d 100644 --- a/package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx +++ b/package/src/components/ImageGallery/components/AnimatedGalleryImage.tsx @@ -51,7 +51,7 @@ export const AnimatedGalleryImage = React.memo( * image as it is scaled. If the scale is less than one they stay in * place as to not come into the screen when the image shrinks. */ - const AnimatedGalleryImageStyle = useAnimatedStyle(() => { + const animatedGalleryImageStyle = useAnimatedStyle(() => { const xScaleOffset = -7 * screenWidth * (0.5 + index); const yScaleOffset = -screenHeight * 3.5; return { @@ -91,8 +91,7 @@ export const AnimatedGalleryImage = React.memo( resizeMode={'contain'} source={{ uri: photo.uri }} style={[ - style, - AnimatedGalleryImageStyle, + animatedGalleryImageStyle, { transform: [ { scaleX: -1 }, @@ -103,6 +102,7 @@ export const AnimatedGalleryImage = React.memo( { scale: oneEighth }, ], }, + style, ]} /> ); diff --git a/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx b/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx index 0768a5286b..7f669b3ec2 100644 --- a/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx +++ b/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx @@ -83,7 +83,8 @@ export const AnimatedGalleryVideo = React.memo( const onLoad = (payload: VideoPayloadData) => { setOpacity(0); - handleLoad(attachmentId, payload.duration); + // Duration is in seconds so we convert to milliseconds. + handleLoad(attachmentId, payload.duration * 1000); }; const onEnd = () => { @@ -109,12 +110,12 @@ export const AnimatedGalleryVideo = React.memo( } else { // Update your UI for the loaded state setOpacity(0); - handleLoad(attachmentId, playbackStatus.durationMillis / 1000); + handleLoad(attachmentId, playbackStatus.durationMillis); if (playbackStatus.isPlaying) { // Update your UI for the playing state handleProgress( attachmentId, - playbackStatus.positionMillis / 1000 / (playbackStatus.durationMillis / 1000), + playbackStatus.positionMillis / playbackStatus.durationMillis, ); } @@ -173,7 +174,6 @@ export const AnimatedGalleryVideo = React.memo( {isVideoPackageAvailable() && ( diff --git a/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx b/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx index 226b074897..aa3567d900 100644 --- a/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx +++ b/package/src/components/ImageGallery/components/ImageGalleryFooter.tsx @@ -9,7 +9,7 @@ import { useTranslationContext } from '../../../contexts/translationContext/Tran import { Grid as GridIconDefault, Share as ShareIconDefault } from '../../../icons'; import { deleteFile, saveFile, shareImage, VideoType } from '../../../native'; -import type { DefaultStreamChatGenerics } from '../../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../../types/types'; import type { Photo } from '../ImageGallery'; const ReanimatedSafeAreaView = Animated.createAnimatedComponent @@ -181,8 +181,8 @@ export const ImageGalleryFooterWithContext = < pointerEvents={'box-none'} style={styles.wrapper} > - - {photo.type === 'video' ? ( + + {photo.type === FileTypes.Video ? ( videoControlElement ? ( videoControlElement({ duration, onPlayPause, paused, progress, videoRef }) ) : ( @@ -195,7 +195,7 @@ export const ImageGalleryFooterWithContext = < /> ) ) : null} - + {leftElement ? ( leftElement({ openGridView, photo, share, shareMenuOpen }) ) : ( diff --git a/package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx b/package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx index 35c66aa389..3a94ccbce5 100644 --- a/package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx +++ b/package/src/components/ImageGallery/components/ImageGalleryVideoControl.tsx @@ -1,13 +1,12 @@ import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import dayjs from 'dayjs'; - import type { ImageGalleryFooterVideoControlProps } from './ImageGalleryFooter'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { Pause, Play } from '../../../icons'; +import { getDurationLabelFromDuration } from '../../../utils/utils'; import { ProgressControl } from '../../ProgressControl/ProgressControl'; const styles = StyleSheet.create({ @@ -37,19 +36,11 @@ export const ImageGalleryVideoControl = React.memo( (props: ImageGalleryFooterVideoControlProps) => { const { duration, onPlayPause, paused, progress, videoRef } = props; - const videoDuration = duration - ? duration / 3600 >= 1 - ? dayjs.duration(duration, 'second').format('HH:mm:ss') - : dayjs.duration(duration, 'second').format('mm:ss') - : null; + const videoDuration = getDurationLabelFromDuration(duration); const progressValueInSeconds = progress * duration; - const progressDuration = progressValueInSeconds - ? progressValueInSeconds / 3600 >= 1 - ? dayjs.duration(progressValueInSeconds, 'second').format('HH:mm:ss') - : dayjs.duration(progressValueInSeconds, 'second').format('mm:ss') - : null; + const progressDuration = getDurationLabelFromDuration(progressValueInSeconds); const { theme: { @@ -73,7 +64,7 @@ export const ImageGalleryVideoControl = React.memo( return ( - + {paused ? ( ) : ( @@ -83,9 +74,9 @@ export const ImageGalleryVideoControl = React.memo( - {progressDuration ? progressDuration : '00:00'} + {progressDuration} - {videoDuration ? videoDuration : '00:00'} + {videoDuration} ); diff --git a/package/src/components/ImageGallery/components/ImageGrid.tsx b/package/src/components/ImageGallery/components/ImageGrid.tsx index 24536406da..0faaf96716 100644 --- a/package/src/components/ImageGallery/components/ImageGrid.tsx +++ b/package/src/components/ImageGallery/components/ImageGrid.tsx @@ -6,7 +6,7 @@ import { BottomSheetFlatList, TouchableOpacity } from '@gorhom/bottom-sheet'; import { VideoThumbnail } from '../../../components/Attachment/VideoThumbnail'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { useViewport } from '../../../hooks/useViewport'; -import type { DefaultStreamChatGenerics } from '../../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../../types/types'; import type { Photo } from '../ImageGallery'; @@ -84,7 +84,7 @@ const GridImage = < return ( - {type === 'video' ? ( + {type === FileTypes.Video ? ( diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 5d4f6b95f3..d47767d4cf 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -40,7 +40,7 @@ import { } from '../../contexts/translationContext/TranslationContext'; import { isVideoPackageAvailable, triggerHaptic } from '../../native'; -import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../types/types'; import { hasOnlyEmojis, isBlockedMessage, @@ -132,10 +132,7 @@ export type MessageActionHandlers< export type MessagePropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - ChannelContextValue, - 'channel' | 'disabled' | 'enforceUniqueReaction' | 'members' -> & +> = Pick, 'channel' | 'enforceUniqueReaction' | 'members'> & Pick & Partial, 'groupStyles' | 'message'>> & Pick, 'groupStyles' | 'message'> & @@ -245,7 +242,6 @@ const MessageWithContext = < chatContext, deleteMessage: deleteMessageFromContext, deleteReaction, - disabled, dismissKeyboard, dismissKeyboardOnMessageTouch, enableLongPress = true, @@ -381,22 +377,26 @@ const MessageWithContext = < !isMessageTypeDeleted && Array.isArray(message.attachments) ? message.attachments.reduce( (acc, cur) => { - if (cur.type === 'file') { + if (cur.type === FileTypes.File) { acc.files.push(cur); acc.other = []; // remove other attachments if a file exists - } else if (cur.type === 'video' && !cur.og_scrape_url && isVideoPackageAvailable()) { + } else if ( + cur.type === FileTypes.Video && + !cur.og_scrape_url && + isVideoPackageAvailable() + ) { acc.videos.push({ image_url: cur.asset_url, thumb_url: cur.thumb_url, - type: 'video', + type: FileTypes.Video, }); acc.other = []; - } else if (cur.type === 'video' && !cur.og_scrape_url) { + } else if (cur.type === FileTypes.Video && !cur.og_scrape_url) { acc.files.push(cur); acc.other = []; // remove other attachments if a file exists - } else if (cur.type === 'audio' || cur.type === 'voiceRecording') { + } else if (cur.type === FileTypes.Audio || cur.type === FileTypes.VoiceRecording) { acc.files.push(cur); - } else if (cur.type === 'image' && !cur.title_link && !cur.og_scrape_url) { + } else if (cur.type === FileTypes.Image && !cur.title_link && !cur.og_scrape_url) { /** * this next if is not combined with the above one for cases where we have * an image with no url links at all falling back to being an attachment @@ -627,7 +627,7 @@ const MessageWithContext = < }; const onLongPressMessage = - disabled || hasAttachmentActions || isBlockedMessage(message) + hasAttachmentActions || isBlockedMessage(message) ? () => null : onLongPressMessageProp ? (payload?: TouchableHandlerPayload) => @@ -662,7 +662,6 @@ const MessageWithContext = < actionsEnabled, alignment, channel, - disabled, files: attachments.files, goToMessage, groupStyles, @@ -780,7 +779,6 @@ const areEqual = { const { chatContext: { mutedUsers: prevMutedUsers }, - disabled: prevDisabled, goToMessage: prevGoToMessage, groupStyles: prevGroupStyles, isAttachmentEqual, @@ -794,7 +792,6 @@ const areEqual = { const attachmentKeysEqual = - attachment.type === 'image' + attachment.type === FileTypes.Image ? attachment.image_url === nextMessageAttachments[index].image_url && attachment.thumb_url === nextMessageAttachments[index].thumb_url : attachment.type === nextMessageAttachments[index].type; @@ -935,8 +929,7 @@ export const Message = < >( props: MessageProps, ) => { - const { channel, disabled, enforceUniqueReaction, members } = - useChannelContext(); + const { channel, enforceUniqueReaction, members } = useChannelContext(); const chatContext = useChatContext(); const { dismissKeyboard } = useKeyboardContext(); const { setData } = useMessageOverlayContext(); @@ -951,7 +944,6 @@ export const Message = < {...{ channel, chatContext, - disabled, dismissKeyboard, enforceUniqueReaction, members, diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx index 3b8231ee8c..a2e243f79e 100644 --- a/package/src/components/Message/MessageSimple/MessageContent.tsx +++ b/package/src/components/Message/MessageSimple/MessageContent.tsx @@ -63,7 +63,6 @@ export type MessageContentPropsWithContext< > = Pick< MessageContextValue, | 'alignment' - | 'disabled' | 'isEditedMessageOpen' | 'goToMessage' | 'groupStyles' @@ -116,7 +115,6 @@ const MessageContentWithContext = < additionalTouchableProps, alignment, Attachment, - disabled, FileAttachmentGroup, Gallery, groupStyles, @@ -272,7 +270,7 @@ const MessageContentWithContext = < return ( { if (onLongPress) { onLongPress({ @@ -402,7 +400,6 @@ const areEqual = , ) => { const { - disabled: prevDisabled, goToMessage: prevGoToMessage, groupStyles: prevGroupStyles, hasReactions: prevHasReactions, @@ -418,7 +415,6 @@ const areEqual = { const { alignment, - disabled, goToMessage, groupStyles, hasReactions, @@ -598,7 +590,6 @@ export const MessageContent = < additionalTouchableProps, alignment, Attachment, - disabled, FileAttachmentGroup, Gallery, goToMessage, diff --git a/package/src/components/Message/MessageSimple/MessageSimple.tsx b/package/src/components/Message/MessageSimple/MessageSimple.tsx index da8d630512..4bd5bd0b95 100644 --- a/package/src/components/Message/MessageSimple/MessageSimple.tsx +++ b/package/src/components/Message/MessageSimple/MessageSimple.tsx @@ -31,13 +31,7 @@ export type MessageSimplePropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick< MessageContextValue, - | 'alignment' - | 'channel' - | 'disabled' - | 'isEditedMessageOpen' - | 'groupStyles' - | 'hasReactions' - | 'message' + 'alignment' | 'channel' | 'isEditedMessageOpen' | 'groupStyles' | 'hasReactions' | 'message' > & Pick< MessagesContextValue, @@ -125,7 +119,6 @@ const areEqual = { const { channel: prevChannel, - disabled: prevDisabled, groupStyles: prevGroupStyles, hasReactions: prevHasReactions, isEditedMessageOpen: prevIsEditedMessageOpen, @@ -134,7 +127,6 @@ const areEqual = ( props: MessageSimpleProps, ) => { - const { alignment, channel, disabled, groupStyles, hasReactions, isEditedMessageOpen, message } = + const { alignment, channel, groupStyles, hasReactions, isEditedMessageOpen, message } = useMessageContext(); const { enableMessageGroupingByUser, @@ -246,7 +235,6 @@ export const MessageSimple = < {...{ alignment, channel, - disabled, enableMessageGroupingByUser, groupStyles, hasReactions, diff --git a/package/src/components/Message/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts index 44bdca51a4..6d01442882 100644 --- a/package/src/components/Message/hooks/useCreateMessageContext.ts +++ b/package/src/components/Message/hooks/useCreateMessageContext.ts @@ -10,7 +10,6 @@ export const useCreateMessageContext = < actionsEnabled, alignment, channel, - disabled, files, goToMessage, groupStyles, @@ -63,7 +62,6 @@ export const useCreateMessageContext = < actionsEnabled, alignment, channel, - disabled, files, goToMessage, groupStyles, @@ -106,7 +104,6 @@ export const useCreateMessageContext = < actionsEnabled, quotedMessageDeletedValue, alignment, - disabled, goToMessage, groupStylesLength, hasReactions, diff --git a/package/src/components/MessageInput/AttachButton.tsx b/package/src/components/MessageInput/AttachButton.tsx index 5e16f55cfc..de68fa77a2 100644 --- a/package/src/components/MessageInput/AttachButton.tsx +++ b/package/src/components/MessageInput/AttachButton.tsx @@ -1,15 +1,16 @@ -import React from 'react'; -import type { GestureResponderEvent } from 'react-native'; +import React, { useState } from 'react'; +import type { GestureResponderEvent, LayoutChangeEvent, LayoutRectangle } from 'react-native'; import { Pressable } from 'react-native'; +import { NativeAttachmentPicker } from './components/NativeAttachmentPicker'; + import { useAttachmentPickerContext } from '../../contexts/attachmentPickerContext/AttachmentPickerContext'; -import { - ChannelContextValue, - useChannelContext, -} from '../../contexts/channelContext/ChannelContext'; +import { ChannelContextValue } from '../../contexts/channelContext/ChannelContext'; +import { useMessageInputContext } from '../../contexts/messageInputContext/MessageInputContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { Attach } from '../../icons/Attach'; +import { isImageMediaLibraryAvailable } from '../../native'; import type { DefaultStreamChatGenerics } from '../../types/types'; type AttachButtonPropsWithContext< @@ -25,6 +26,8 @@ const AttachButtonWithContext = < >( props: AttachButtonPropsWithContext, ) => { + const [showAttachButtonPicker, setShowAttachButtonPicker] = useState(false); + const [attachButtonLayoutRectangle, setAttachButtonLayoutRectangle] = useState(); const { disabled, handleOnPress, selectedPicker } = props; const { theme: { @@ -32,36 +35,71 @@ const AttachButtonWithContext = < messageInput: { attachButton }, }, } = useTheme(); + const { handleAttachButtonPress, toggleAttachmentPicker } = useMessageInputContext(); + + const onAttachButtonLayout = (event: LayoutChangeEvent) => { + const layout = event.nativeEvent.layout; + setAttachButtonLayoutRectangle((prev) => { + if ( + prev && + prev.width === layout.width && + prev.height === layout.height && + prev.x === layout.x && + prev.y === layout.y + ) { + return prev; + } + return layout; + }); + }; + + const attachButtonHandler = () => { + setShowAttachButtonPicker(true); + }; + + const onPressHandler = () => { + if (handleOnPress) { + handleOnPress(); + return; + } + if (handleAttachButtonPress) { + handleAttachButtonPress(); + return; + } + if (isImageMediaLibraryAvailable()) { + toggleAttachmentPicker(); + } else { + attachButtonHandler(); + } + }; return ( - null : handleOnPress} - style={[attachButton]} - testID='attach-button' - > - - + <> + null : onPressHandler} + style={[attachButton]} + testID='attach-button' + > + + + {showAttachButtonPicker ? ( + setShowAttachButtonPicker(false)} + /> + ) : null} + ); }; -const areEqual = ( - prevProps: AttachButtonPropsWithContext, - nextProps: AttachButtonPropsWithContext, +const areEqual = ( + prevProps: AttachButtonPropsWithContext, + nextProps: AttachButtonPropsWithContext, ) => { - const { - disabled: prevDisabled, - handleOnPress: prevHandleOnPress, - selectedPicker: prevSelectedPicker, - } = prevProps; - const { - disabled: nextDisabled, - handleOnPress: nextHandleOnPress, - selectedPicker: nextSelectedPicker, - } = nextProps; - - const disabledEqual = prevDisabled === nextDisabled; - if (!disabledEqual) return false; + const { handleOnPress: prevHandleOnPress, selectedPicker: prevSelectedPicker } = prevProps; + const { handleOnPress: nextHandleOnPress, selectedPicker: nextSelectedPicker } = nextProps; const handleOnPressEqual = prevHandleOnPress === nextHandleOnPress; if (!handleOnPressEqual) return false; @@ -77,22 +115,15 @@ const MemoizedAttachButton = React.memo( areEqual, ) as typeof AttachButtonWithContext; -export type AttachButtonProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial>; +export type AttachButtonProps = Partial; /** * UI Component for attach button in MessageInput component. */ -export const AttachButton = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: AttachButtonProps, -) => { - const { disabled = false } = useChannelContext(); +export const AttachButton = (props: AttachButtonProps) => { const { selectedPicker } = useAttachmentPickerContext(); - return ; + return ; }; AttachButton.displayName = 'AttachButton{messageInput}'; diff --git a/package/src/components/MessageInput/CommandsButton.tsx b/package/src/components/MessageInput/CommandsButton.tsx index bc0ce5e043..ff536282ee 100644 --- a/package/src/components/MessageInput/CommandsButton.tsx +++ b/package/src/components/MessageInput/CommandsButton.tsx @@ -2,10 +2,6 @@ import React from 'react'; import type { GestureResponderEvent } from 'react-native'; import { Pressable } from 'react-native'; -import { - ChannelContextValue, - useChannelContext, -} from '../../contexts/channelContext/ChannelContext'; import { isSuggestionCommand, SuggestionsContextValue, @@ -18,18 +14,17 @@ import type { DefaultStreamChatGenerics } from '../../types/types'; type CommandsButtonPropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'disabled'> & - Pick, 'suggestions'> & { - /** Function that opens commands selector */ - handleOnPress?: ((event: GestureResponderEvent) => void) & (() => void); - }; +> = Pick, 'suggestions'> & { + /** Function that opens commands selector */ + handleOnPress?: ((event: GestureResponderEvent) => void) & (() => void); +}; const CommandsButtonWithContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: CommandsButtonPropsWithContext, ) => { - const { disabled, handleOnPress, suggestions } = props; + const { handleOnPress, suggestions } = props; const { theme: { @@ -39,18 +34,14 @@ const CommandsButtonWithContext = < } = useTheme(); return ( - + isSuggestionCommand(suggestion)) ? accent_blue : grey } + size={32} /> ); @@ -60,11 +51,8 @@ const areEqual = , nextProps: CommandsButtonPropsWithContext, ) => { - const { disabled: prevDisabled, suggestions: prevSuggestions } = prevProps; - const { disabled: nextDisabled, suggestions: nextSuggestions } = nextProps; - - const disabledEqual = prevDisabled === nextDisabled; - if (!disabledEqual) return false; + const { suggestions: prevSuggestions } = prevProps; + const { suggestions: nextSuggestions } = nextProps; const suggestionsEqual = !!prevSuggestions === !!nextSuggestions; if (!suggestionsEqual) return false; @@ -89,10 +77,9 @@ export const CommandsButton = < >( props: CommandsButtonProps, ) => { - const { disabled = false } = useChannelContext(); const { suggestions } = useSuggestionsContext(); - return ; + return ; }; CommandsButton.displayName = 'CommandsButton{messageInput}'; diff --git a/package/src/components/MessageInput/FileUploadPreview.tsx b/package/src/components/MessageInput/FileUploadPreview.tsx index afc00e950a..4f50d1527f 100644 --- a/package/src/components/MessageInput/FileUploadPreview.tsx +++ b/package/src/components/MessageInput/FileUploadPreview.tsx @@ -1,8 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { FlatList, I18nManager, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import dayjs from 'dayjs'; - import { UploadProgressIndicator } from './UploadProgressIndicator'; import { ChatContextValue, useChatContext } from '../../contexts'; @@ -21,7 +19,11 @@ import { Warning } from '../../icons/Warning'; import { isAudioPackageAvailable } from '../../native'; import type { DefaultStreamChatGenerics, FileUpload } from '../../types/types'; import { getTrimmedAttachmentTitle } from '../../utils/getTrimmedAttachmentTitle'; -import { getIndicatorTypeForFileState, ProgressIndicatorTypes } from '../../utils/utils'; +import { + getDurationLabelFromDuration, + getIndicatorTypeForFileState, + ProgressIndicatorTypes, +} from '../../utils/utils'; import { getFileSizeDisplayText } from '../Attachment/FileAttachment'; import { WritingDirectionAwareText } from '../RTLComponents/WritingDirectionAwareText'; @@ -97,19 +99,6 @@ const UnsupportedFileTypeOrFileSizeIndicator = ({ }, } = useTheme(); - const ONE_HOUR_IN_SECONDS = 3600; - let durationLabel = '00:00'; - const videoDuration = item.file.duration; - - if (videoDuration) { - const isDurationLongerThanHour = videoDuration / ONE_HOUR_IN_SECONDS >= 1; - const formattedDurationParam = isDurationLongerThanHour ? 'HH:mm:ss' : 'mm:ss'; - const formattedVideoDuration = dayjs - .duration(videoDuration, 'second') - .format(formattedDurationParam); - durationLabel = formattedVideoDuration; - } - const { t } = useTranslationContext(); return indicatorType === ProgressIndicatorTypes.NOT_SUPPORTED ? ( @@ -126,7 +115,9 @@ const UnsupportedFileTypeOrFileSizeIndicator = ({ ) : ( - {videoDuration ? durationLabel : getFileSizeDisplayText(item.file.size)} + {item.file.duration + ? getDurationLabelFromDuration(item.file.duration) + : getFileSizeDisplayText(item.file.size)} ); }; diff --git a/package/src/components/MessageInput/InputButtons.tsx b/package/src/components/MessageInput/InputButtons.tsx index 9d24b12168..4ffa702bee 100644 --- a/package/src/components/MessageInput/InputButtons.tsx +++ b/package/src/components/MessageInput/InputButtons.tsx @@ -25,6 +25,7 @@ export type InputButtonsWithContextProps< | 'AttachButton' | 'CommandsButton' | 'giphyActive' + | 'hasCameraPicker' | 'hasCommands' | 'hasFilePicker' | 'hasImagePicker' @@ -46,6 +47,7 @@ export const InputButtonsWithContext = < AttachButton, CommandsButton, giphyActive, + hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, @@ -54,7 +56,6 @@ export const InputButtonsWithContext = < setShowMoreOptions, showMoreOptions, text, - toggleAttachmentPicker, } = props; const { @@ -69,15 +70,15 @@ export const InputButtonsWithContext = < return null; } - return !showMoreOptions && (hasImagePicker || hasFilePicker) && hasCommands ? ( + return !showMoreOptions && (hasCameraPicker || hasImagePicker || hasFilePicker) && hasCommands ? ( setShowMoreOptions(true)} /> ) : ( <> - {(hasImagePicker || hasFilePicker) && ownCapabilities.uploadFile && ( + {(hasCameraPicker || hasImagePicker || hasFilePicker) && ownCapabilities.uploadFile && ( - + )} {hasCommands && !text && ( @@ -95,6 +96,7 @@ const areEqual = { const { giphyActive: prevGiphyActive, + hasCameraPicker: prevHasCameraPicker, hasCommands: prevHasCommands, hasFilePicker: prevHasFilePicker, hasImagePicker: prevHasImagePicker, @@ -105,6 +107,7 @@ const areEqual = = Pick & Pick, 'isOnline'> & - Pick< - ChannelContextValue, - 'disabled' | 'members' | 'threadList' | 'watchers' - > & + Pick, 'members' | 'threadList' | 'watchers'> & Pick< MessageInputContextValue, | 'additionalTextInputProps' @@ -122,6 +118,7 @@ type MessageInputPropsWithContext< | 'FileUploadPreview' | 'fileUploads' | 'giphyActive' + | 'hasImagePicker' | 'ImageUploadPreview' | 'imageUploads' | 'Input' @@ -184,11 +181,11 @@ const MessageInputWithContext = < closeAttachmentPicker, cooldownEndsAt, CooldownTimer, - disabled, editing, FileUploadPreview, fileUploads, giphyActive, + hasImagePicker, ImageUploadPreview, imageUploads, Input, @@ -282,6 +279,9 @@ const MessageInputWithContext = < const fileUploadsLength = hasResetFiles ? fileUploads.length : 0; const imagesForInput = (!!thread && !!threadList) || (!thread && !threadList); + /** + * Reset the selected images when the component is unmounted. + */ useEffect(() => { setSelectedImages([]); if (imageUploads.length) { @@ -290,11 +290,15 @@ const MessageInputWithContext = < return () => setSelectedImages([]); }, []); + /** + * Reset the selected files when the component is unmounted. + */ useEffect(() => { setSelectedFiles([]); if (fileUploads.length) { fileUploads.forEach((file) => removeFile(file.id)); } + return () => setSelectedFiles([]); }, []); @@ -311,10 +315,10 @@ const MessageInputWithContext = < }, [fileUploadsLength, selectedFilesLength]); useEffect(() => { - if (imagesForInput === false && imageUploads.length) { + if (imagesForInput === false && imageUploadsLength) { imageUploads.forEach((image) => removeImage(image.id)); } - }, [imagesForInput]); + }, [imagesForInput, imageUploadsLength]); const uploadImagesHandler = () => { const imageToUpload = selectedImages.find((selectedImage) => { @@ -376,9 +380,9 @@ const MessageInputWithContext = < }, [selectedFilesLength]); useEffect(() => { - if (imagesForInput) { + if (imagesForInput && hasImagePicker) { if (imageUploadsLength < selectedImagesLength) { - /** User removed some image from seleted images within ImageUploadPreview. */ + // /** User removed some image from seleted images within ImageUploadPreview. */ const updatedSelectedImages = selectedImages.filter((selectedImage) => { const uploadedImage = imageUploads.find( (imageUpload) => @@ -405,36 +409,38 @@ const MessageInputWithContext = < ); } } - }, [imageUploadsLength]); + }, [imageUploadsLength, hasImagePicker]); useEffect(() => { - if (fileUploadsLength < selectedFilesLength) { - /** User removed some video from seleted files within ImageUploadPreview. */ - const updatedSelectedFiles = selectedFiles.filter((selectedFile) => { - const uploadedFile = fileUploads.find( - (fileUpload) => - fileUpload.file.uri === selectedFile.uri || fileUpload.url === selectedFile.uri, + if (hasImagePicker) { + if (fileUploadsLength < selectedFilesLength) { + /** User removed some video from seleted files within ImageUploadPreview. */ + const updatedSelectedFiles = selectedFiles.filter((selectedFile) => { + const uploadedFile = fileUploads.find( + (fileUpload) => + fileUpload.file.uri === selectedFile.uri || fileUpload.url === selectedFile.uri, + ); + return uploadedFile; + }); + setSelectedFiles(updatedSelectedFiles); + } else if (fileUploadsLength > selectedFilesLength) { + /** + * User is editing some message which contains video attachments OR + * video attachment is added from custom image picker (other than the default bottom-sheet image picker) + * using `uploadNewFile` function from `MessageInputContext`. + **/ + setSelectedFiles( + fileUploads.map((fileUpload) => ({ + duration: fileUpload.file.duration, + mimeType: fileUpload.file.mimeType, + name: fileUpload.file.name, + size: fileUpload.file.size, + uri: fileUpload.file.uri, + })), ); - return uploadedFile; - }); - setSelectedFiles(updatedSelectedFiles); - } else if (fileUploadsLength > selectedFilesLength) { - /** - * User is editing some message which contains video attachments OR - * video attachment is added from custom image picker (other than the default bottom-sheet image picker) - * using `uploadNewFile` function from `MessageInputContext`. - **/ - setSelectedFiles( - fileUploads.map((fileUpload) => ({ - duration: fileUpload.file.duration, - mimeType: fileUpload.file.mimeType, - name: fileUpload.file.name, - size: fileUpload.file.size, - uri: fileUpload.file.uri, - })), - ); + } } - }, [fileUploadsLength]); + }, [fileUploadsLength, hasImagePicker]); const editingExists = !!editing; useEffect(() => { @@ -457,7 +463,8 @@ const MessageInputWithContext = < fileUploads.length > 0 || mentionedUsers.length > 0 || imageUploads.length > 0 || - numberOfUploads > 0) + numberOfUploads > 0) && + resetInput ) { resetInput(); } @@ -517,7 +524,6 @@ const MessageInputWithContext = < }; const additionalTextInputContainerProps = { - editable: disabled ? false : undefined, ...additionalTextInputProps, }; @@ -771,12 +777,7 @@ const MessageInputWithContext = < ) : ( ))} @@ -849,7 +850,6 @@ const areEqual = ; } @@ -1099,11 +1100,11 @@ export const MessageInput = < closeAttachmentPicker, cooldownEndsAt, CooldownTimer, - disabled, editing, FileUploadPreview, fileUploads, giphyActive, + hasImagePicker, ImageUploadPreview, imageUploads, Input, diff --git a/package/src/components/MessageInput/MoreOptionsButton.tsx b/package/src/components/MessageInput/MoreOptionsButton.tsx index fd77e4fab5..300453cd39 100644 --- a/package/src/components/MessageInput/MoreOptionsButton.tsx +++ b/package/src/components/MessageInput/MoreOptionsButton.tsx @@ -2,28 +2,16 @@ import React from 'react'; import type { GestureResponderEvent } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; -import { - ChannelContextValue, - useChannelContext, -} from '../../contexts/channelContext/ChannelContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { CircleRight } from '../../icons/CircleRight'; -import type { DefaultStreamChatGenerics } from '../../types/types'; - -type MoreOptionsButtonPropsWithContext< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'disabled'> & { +export type MoreOptionsButtonProps = { /** Function that opens attachment options bottom sheet */ handleOnPress?: ((event: GestureResponderEvent) => void) & (() => void); }; -const MoreOptionsButtonWithContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MoreOptionsButtonPropsWithContext, -) => { - const { disabled, handleOnPress } = props; +export const MoreOptionsButton = (props: MoreOptionsButtonProps) => { + const { handleOnPress } = props; const { theme: { @@ -34,7 +22,6 @@ const MoreOptionsButtonWithContext = < return ( ( - prevProps: MoreOptionsButtonPropsWithContext, - nextProps: MoreOptionsButtonPropsWithContext, -) => { - const { disabled: prevDisabled, handleOnPress: prevHandleOnPress } = prevProps; - const { disabled: nextDisabled, handleOnPress: nextHandleOnPress } = nextProps; - const disabledEqual = prevDisabled === nextDisabled; - if (!disabledEqual) return false; - - const handleOnPressEqual = prevHandleOnPress === nextHandleOnPress; - if (!handleOnPressEqual) return false; - - return true; -}; - -const MemoizedMoreOptionsButton = React.memo( - MoreOptionsButtonWithContext, - areEqual, -) as typeof MoreOptionsButtonWithContext; - -export type MoreOptionsButtonProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial>; - -/** - * UI Component for more options button in MessageInput component. - */ -export const MoreOptionsButton = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MoreOptionsButtonProps, -) => { - const { disabled = false } = useChannelContext(); - - return ; -}; - MoreOptionsButton.displayName = 'MoreOptionsButton{messageInput}'; diff --git a/package/src/components/MessageInput/__tests__/FileUploadPreview.test.js b/package/src/components/MessageInput/__tests__/FileUploadPreview.test.js index 4489fabeb1..8fc55877f6 100644 --- a/package/src/components/MessageInput/__tests__/FileUploadPreview.test.js +++ b/package/src/components/MessageInput/__tests__/FileUploadPreview.test.js @@ -31,6 +31,8 @@ jest.mock('../../../native.ts', () => { return { isAudioPackageAvailable: jest.fn(() => true), + isImageMediaLibraryAvailable: jest.fn(() => true), + isImagePickerAvailable: jest.fn(() => true), NetInfo: { addEventListener: jest.fn(), fetch: jest.fn(), diff --git a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap index aff41cff2e..e5908a1df0 100644 --- a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap +++ b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap @@ -25,6 +25,7 @@ exports[`AttachButton should render a disabled AttachButton 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -40,14 +41,14 @@ exports[`AttachButton should render a disabled AttachButton 1`] = ` > - + > + + + + + + + @@ -101,7 +148,7 @@ exports[`AttachButton should render an enabled AttachButton 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": false, + "disabled": undefined, "expanded": undefined, "selected": undefined, } @@ -120,6 +167,7 @@ exports[`AttachButton should render an enabled AttachButton 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -135,14 +183,14 @@ exports[`AttachButton should render an enabled AttachButton 1`] = ` > - + > + + + + + + + diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecorder.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecorder.tsx index 9340f22955..b092960c5a 100644 --- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecorder.tsx +++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecorder.tsx @@ -5,10 +5,6 @@ import Animated from 'react-native-reanimated'; import dayjs from 'dayjs'; -import { - ChannelContextValue, - useChannelContext, -} from '../../../../contexts/channelContext/ChannelContext'; import { MessageInputContextValue, useMessageInputContext, @@ -21,42 +17,41 @@ import type { DefaultStreamChatGenerics } from '../../../../types/types'; type AudioRecorderPropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'disabled'> & - Pick, 'asyncMessagesMultiSendEnabled'> & { - /** - * Function to stop and delete the voice recording. - */ - deleteVoiceRecording: () => Promise; - /** - * Boolean used to show if the voice recording state is locked. This makes sure the mic button shouldn't be pressed any longer. - * When the mic is locked the `AudioRecordingInProgress` component shows up. - */ - micLocked: boolean; - /** - * The current voice recording that is in progress. - */ - recording: AudioRecordingReturnType; - /** - * Boolean to determine if the recording has been stopped. - */ - recordingStopped: boolean; - /** - * Function to stop the ongoing voice recording. - */ - stopVoiceRecording: () => Promise; - /** - * Function to upload the voice recording. - */ - uploadVoiceRecording: (multiSendEnabled: boolean) => Promise; - /** - * The duration of the voice recording. - */ - recordingDuration?: number; - /** - * Style used in slide to cancel container. - */ - slideToCancelStyle?: StyleProp; - }; +> = Pick, 'asyncMessagesMultiSendEnabled'> & { + /** + * Function to stop and delete the voice recording. + */ + deleteVoiceRecording: () => Promise; + /** + * Boolean used to show if the voice recording state is locked. This makes sure the mic button shouldn't be pressed any longer. + * When the mic is locked the `AudioRecordingInProgress` component shows up. + */ + micLocked: boolean; + /** + * The current voice recording that is in progress. + */ + recording: AudioRecordingReturnType; + /** + * Boolean to determine if the recording has been stopped. + */ + recordingStopped: boolean; + /** + * Function to stop the ongoing voice recording. + */ + stopVoiceRecording: () => Promise; + /** + * Function to upload the voice recording. + */ + uploadVoiceRecording: (multiSendEnabled: boolean) => Promise; + /** + * The duration of the voice recording. + */ + recordingDuration?: number; + /** + * Style used in slide to cancel container. + */ + slideToCancelStyle?: StyleProp; +}; const StopRecording = ({ stopVoiceRecordingHandler, @@ -110,10 +105,8 @@ const UploadRecording = ({ const DeleteRecording = ({ deleteVoiceRecordingHandler, - disabled, }: { deleteVoiceRecordingHandler: () => Promise; - disabled?: boolean; }) => { const { theme: { @@ -125,7 +118,6 @@ const DeleteRecording = ({ } = useTheme(); return ( - + { const { asyncMessagesMultiSendEnabled: prevAsyncMessagesMultiSendEnabled, - disabled: prevDisabled, micLocked: prevMicLocked, recording: prevRecording, recordingDuration: prevRecordingDuration, @@ -220,7 +210,6 @@ const areEqual = ( props: AudioRecorderProps, ) => { - const { disabled = false } = useChannelContext(); const { asyncMessagesMultiSendEnabled } = useMessageInputContext(); return ( diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx index 826d972fb2..e1e76fe27b 100644 --- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx +++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx @@ -1,10 +1,6 @@ import React from 'react'; import { Alert, Linking, Pressable, StyleSheet } from 'react-native'; -import { - ChannelContextValue, - useChannelContext, -} from '../../../../contexts/channelContext/ChannelContext'; import { MessageInputContextValue, useMessageInputContext, @@ -18,33 +14,32 @@ import type { DefaultStreamChatGenerics } from '../../../../types/types'; type AudioRecordingButtonPropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'disabled'> & - Pick, 'asyncMessagesMinimumPressDuration'> & { - /** - * The current voice recording that is in progress. - */ - recording: AudioRecordingReturnType; - /** - * Size of the mic button. - */ - buttonSize?: number; - /** - * Handler to determine what should happen on long press of the mic button. - */ - handleLongPress?: () => void; - /** - * Handler to determine what should happen on press of the mic button. - */ - handlePress?: () => void; - /** - * Boolean to determine if the audio recording permissions are granted. - */ - permissionsGranted?: boolean; - /** - * Function to start the voice recording. - */ - startVoiceRecording?: () => Promise; - }; +> = Pick, 'asyncMessagesMinimumPressDuration'> & { + /** + * The current voice recording that is in progress. + */ + recording: AudioRecordingReturnType; + /** + * Size of the mic button. + */ + buttonSize?: number; + /** + * Handler to determine what should happen on long press of the mic button. + */ + handleLongPress?: () => void; + /** + * Handler to determine what should happen on press of the mic button. + */ + handlePress?: () => void; + /** + * Boolean to determine if the audio recording permissions are granted. + */ + permissionsGranted?: boolean; + /** + * Function to start the voice recording. + */ + startVoiceRecording?: () => Promise; +}; const AudioRecordingButtonWithContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -54,7 +49,6 @@ const AudioRecordingButtonWithContext = < const { asyncMessagesMinimumPressDuration, buttonSize, - disabled, handleLongPress, handlePress, permissionsGranted, @@ -107,7 +101,6 @@ const AudioRecordingButtonWithContext = < return ( [ @@ -132,12 +125,10 @@ const areEqual = { const { asyncMessagesMinimumPressDuration: prevAsyncMessagesMinimumPressDuration, - disabled: prevDisabled, recording: prevRecording, } = prevProps; const { asyncMessagesMinimumPressDuration: nextAsyncMessagesMinimumPressDuration, - disabled: nextDisabled, recording: nextRecording, } = nextProps; @@ -145,9 +136,6 @@ const areEqual = ( props: AudioRecordingButtonProps, ) => { - const { disabled = false } = useChannelContext(); const { asyncMessagesMinimumPressDuration } = useMessageInputContext(); - return ( - - ); + return ; }; const styles = StyleSheet.create({ diff --git a/package/src/components/MessageInput/components/InputEditingStateHeader.tsx b/package/src/components/MessageInput/components/InputEditingStateHeader.tsx index 807785458f..1faab83718 100644 --- a/package/src/components/MessageInput/components/InputEditingStateHeader.tsx +++ b/package/src/components/MessageInput/components/InputEditingStateHeader.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import type { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext'; import { MessageInputContextValue, useMessageInputContext, @@ -25,19 +24,23 @@ const styles = StyleSheet.create({ }, }); -export type InputEditingStateHeaderPropsWithContext< +export type InputEditingStateHeaderProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'clearEditingState' | 'resetInput'> & - Pick, 'disabled'>; +> = Partial, 'clearEditingState' | 'resetInput'>>; -export const InputEditingStateHeaderWithContext = < +export const InputEditingStateHeader = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ - clearEditingState, - disabled, - resetInput, -}: InputEditingStateHeaderPropsWithContext) => { + clearEditingState: propClearEditingState, + resetInput: propResetInput, +}: InputEditingStateHeaderProps) => { const { t } = useTranslationContext(); + const { clearEditingState: contextClearEditingState, resetInput: contextResetInput } = + useMessageInputContext(); + + const clearEditingState = propClearEditingState || contextClearEditingState; + const resetInput = propResetInput || contextResetInput; + const { theme: { colors: { black, grey, grey_gainsboro }, @@ -54,10 +57,13 @@ export const InputEditingStateHeaderWithContext = < {t('Editing Message')} { - resetInput(); - clearEditingState(); + if (resetInput) { + resetInput(); + } + if (clearEditingState) { + clearEditingState(); + } }} testID='close-button' > @@ -67,36 +73,4 @@ export const InputEditingStateHeaderWithContext = < ); }; -const areEqual = ( - prevProps: InputEditingStateHeaderPropsWithContext, - nextProps: InputEditingStateHeaderPropsWithContext, -) => { - const { disabled: prevDisabled } = prevProps; - const { disabled: nextDisabled } = nextProps; - - const disabledEqual = prevDisabled === nextDisabled; - if (!disabledEqual) return false; - - return true; -}; - -const MemoizedInputEditingStateHeader = React.memo( - InputEditingStateHeaderWithContext, - areEqual, -) as typeof InputEditingStateHeaderWithContext; - -export type InputEditingStateHeaderProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial>; - -export const InputEditingStateHeader = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: InputEditingStateHeaderProps, -) => { - const { clearEditingState, resetInput } = useMessageInputContext(); - - return ; -}; - InputEditingStateHeader.displayName = 'EditingStateHeader{messageInput}'; diff --git a/package/src/components/MessageInput/components/InputGiphySearch.tsx b/package/src/components/MessageInput/components/InputGiphySearch.tsx index 1e37883e62..f9c0638ae7 100644 --- a/package/src/components/MessageInput/components/InputGiphySearch.tsx +++ b/package/src/components/MessageInput/components/InputGiphySearch.tsx @@ -1,14 +1,13 @@ import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import type { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext'; import { MessageInputContextValue, useMessageInputContext, } from '../../../contexts/messageInputContext/MessageInputContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; -import { CircleClose, Lightning } from '../../../icons'; +import { CircleClose, GiphyLightning } from '../../../icons'; import type { DefaultStreamChatGenerics } from '../../../types/types'; import { AutoCompleteInput } from '../../AutoCompleteInput/AutoCompleteInput'; import { useCountdown } from '../hooks/useCountdown'; @@ -24,9 +23,9 @@ const styles = StyleSheet.create({ alignItems: 'center', borderRadius: 12, flexDirection: 'row', - height: 24, marginRight: 8, paddingHorizontal: 8, + paddingVertical: 4, }, giphyText: { fontSize: 12, @@ -34,23 +33,38 @@ const styles = StyleSheet.create({ }, }); -export type InputGiphySearchPropsWithContext< +export type InputGiphySearchProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - MessageInputContextValue, - 'additionalTextInputProps' | 'cooldownEndsAt' | 'setGiphyActive' | 'setShowMoreOptions' -> & - Pick, 'disabled'>; +> = Partial< + Pick< + MessageInputContextValue, + 'additionalTextInputProps' | 'cooldownEndsAt' | 'setGiphyActive' | 'setShowMoreOptions' + > +> & { + disabled: boolean; +}; -export const InputGiphySearchWithContext = < +export const InputGiphySearch = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ - additionalTextInputProps, - cooldownEndsAt, + additionalTextInputProps: propAdditionalTextInputProps, + cooldownEndsAt: propCooldownEndsAt, disabled, - setGiphyActive, - setShowMoreOptions, -}: InputGiphySearchPropsWithContext) => { + setGiphyActive: propSetGiphyActive, + setShowMoreOptions: propSetShowMoreOptions, +}: InputGiphySearchProps) => { + const { + additionalTextInputProps: contextAdditionalTextInputProps, + cooldownEndsAt: contextCooldownEndsAt, + setGiphyActive: contextSetGiphyActive, + setShowMoreOptions: contextSetShowMoreOptions, + } = useMessageInputContext(); + + const additionalTextInputProps = propAdditionalTextInputProps || contextAdditionalTextInputProps; + const cooldownEndsAt = propCooldownEndsAt || contextCooldownEndsAt; + const setGiphyActive = propSetGiphyActive || contextSetGiphyActive; + const setShowMoreOptions = propSetShowMoreOptions || contextSetShowMoreOptions; + const { seconds: cooldownRemainingSeconds } = useCountdown(cooldownEndsAt); const { @@ -66,7 +80,7 @@ export const InputGiphySearchWithContext = < return ( - + GIPHY @@ -88,42 +102,4 @@ export const InputGiphySearchWithContext = < ); }; -const areEqual = ( - prevProps: InputGiphySearchPropsWithContext, - nextProps: InputGiphySearchPropsWithContext, -) => { - const { disabled: prevDisabled } = prevProps; - const { disabled: nextDisabled } = nextProps; - - const disabledEqual = prevDisabled === nextDisabled; - if (!disabledEqual) return false; - - return true; -}; - -const MemoizedInputGiphySearch = React.memo( - InputGiphySearchWithContext, - areEqual, -) as typeof InputGiphySearchWithContext; - -export type InputGiphySearchProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial>; - -export const InputGiphySearch = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: InputGiphySearchProps, -) => { - const { additionalTextInputProps, cooldownEndsAt, setGiphyActive, setShowMoreOptions } = - useMessageInputContext(); - - return ( - - ); -}; - InputGiphySearch.displayName = 'InputGiphySearch{messageInput}'; diff --git a/package/src/components/MessageInput/components/InputReplyStateHeader.tsx b/package/src/components/MessageInput/components/InputReplyStateHeader.tsx index fd31f8ebba..7e45f3145e 100644 --- a/package/src/components/MessageInput/components/InputReplyStateHeader.tsx +++ b/package/src/components/MessageInput/components/InputReplyStateHeader.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import type { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext'; import { MessageInputContextValue, useMessageInputContext, @@ -25,19 +24,21 @@ const styles = StyleSheet.create({ }, }); -export type InputReplyStateHeaderPropsWithContext< +export type InputReplyStateHeaderProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'clearQuotedMessageState' | 'resetInput'> & - Pick, 'disabled'>; +> = Partial< + Pick, 'clearQuotedMessageState' | 'resetInput'> +>; -export const InputReplyStateHeaderWithContext = < +export const InputReplyStateHeader = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ - clearQuotedMessageState, - disabled, - resetInput, -}: InputReplyStateHeaderPropsWithContext) => { + clearQuotedMessageState: propClearQuotedMessageState, + resetInput: propResetInput, +}: InputReplyStateHeaderProps) => { const { t } = useTranslationContext(); + const { clearQuotedMessageState: contextClearQuotedMessageState, resetInput: contextResetInput } = + useMessageInputContext(); const { theme: { colors: { black, grey, grey_gainsboro }, @@ -47,6 +48,9 @@ export const InputReplyStateHeaderWithContext = < }, } = useTheme(); + const clearQuotedMessageState = propClearQuotedMessageState || contextClearQuotedMessageState; + const resetInput = propResetInput || contextResetInput; + return ( @@ -54,10 +58,13 @@ export const InputReplyStateHeaderWithContext = < {t('Reply to Message')} { - resetInput(); - clearQuotedMessageState(); + if (resetInput) { + resetInput(); + } + if (clearQuotedMessageState) { + clearQuotedMessageState(); + } }} testID='close-button' > @@ -67,36 +74,4 @@ export const InputReplyStateHeaderWithContext = < ); }; -const areEqual = ( - prevProps: InputReplyStateHeaderPropsWithContext, - nextProps: InputReplyStateHeaderPropsWithContext, -) => { - const { disabled: prevDisabled } = prevProps; - const { disabled: nextDisabled } = nextProps; - - const disabledEqual = prevDisabled === nextDisabled; - if (!disabledEqual) return false; - - return true; -}; - -const MemoizedInputReplyStateHeader = React.memo( - InputReplyStateHeaderWithContext, - areEqual, -) as typeof InputReplyStateHeaderWithContext; - -export type InputReplyStateHeaderProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial>; - -export const InputReplyStateHeader = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: InputReplyStateHeaderProps, -) => { - const { clearQuotedMessageState, resetInput } = useMessageInputContext(); - - return ; -}; - InputReplyStateHeader.displayName = 'ReplyStateHeader{messageInput}'; diff --git a/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx b/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx new file mode 100644 index 0000000000..762a7cb36e --- /dev/null +++ b/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated, Easing, LayoutRectangle, Pressable, StyleSheet } from 'react-native'; + +import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; +import { useTheme } from '../../../contexts/themeContext/ThemeContext'; + +import { CameraSelectorIcon } from '../../AttachmentPicker/components/CameraSelectorIcon'; +import { FileSelectorIcon } from '../../AttachmentPicker/components/FileSelectorIcon'; +import { ImageSelectorIcon } from '../../AttachmentPicker/components/ImageSelectorIcon'; + +type NativeAttachmentPickerProps = { + onRequestedClose: () => void; + attachButtonLayoutRectangle?: LayoutRectangle; +}; + +const TOP_PADDING = 4; +const ATTACH_MARGIN_BOTTOM = 4; + +export const NativeAttachmentPicker = ({ + attachButtonLayoutRectangle, + onRequestedClose, +}: NativeAttachmentPickerProps) => { + const size = attachButtonLayoutRectangle?.width ?? 0; + const attachButtonItemSize = 40; + const NUMBER_OF_BUTTONS = 3; + const { + theme: { + colors: { grey_whisper }, + messageInput: { + nativeAttachmentPicker: { + buttonContainer, + buttonDimmerStyle: buttonDimmerStyleTheme, + container, + }, + }, + }, + } = useTheme(); + const { + hasFilePicker, + hasImagePicker, + pickAndUploadImageFromNativePicker, + pickFile, + takeAndUploadImage, + } = useMessageInputContext(); + + const popupHeight = + // the top padding + TOP_PADDING + + // take margins into account + ATTACH_MARGIN_BOTTOM * NUMBER_OF_BUTTONS + + // the size of the attachment icon items (same size as attach button * amount of attachment button types) + attachButtonItemSize * NUMBER_OF_BUTTONS; + + const containerPopupStyle = { + borderTopEndRadius: size / 2, + // the popup should be rounded as the attach button + borderTopStartRadius: size / 2, + height: popupHeight, + // from the same side horizontal coordinate of the attach button + left: attachButtonLayoutRectangle?.x, + // we should show the popup right above the attach button and not top of it + top: (attachButtonLayoutRectangle?.y ?? 0) - popupHeight, + // the width of the popup should be the same as the attach button + width: size, + }; + + const elasticAnimRef = useRef(new Animated.Value(0.5)); // Initial value for scale: 0.5 + + useEffect(() => { + Animated.timing(elasticAnimRef.current, { + duration: 150, + easing: Easing.linear, + toValue: 1, + useNativeDriver: true, + }).start(); + }, []); + + const buttonStyle = { + borderRadius: attachButtonItemSize / 2, + height: attachButtonItemSize, + width: attachButtonItemSize, + }; + + const buttonDimmerStyle = { + ...styles.attachButtonDimmer, + height: size, + // from the same side horizontal coordinate of the attach button + left: attachButtonLayoutRectangle?.x, + // we should show the popup right on top of the attach button + top: attachButtonLayoutRectangle?.y ?? 0 - popupHeight + size, + width: size, + }; + + const onClose = ({ onPressHandler }: { onPressHandler?: () => Promise }) => { + if (onPressHandler) { + onPressHandler(); + } + Animated.timing(elasticAnimRef.current, { + duration: 150, + easing: Easing.linear, + toValue: 0.2, + useNativeDriver: true, + }).start(onRequestedClose); + }; + + const buttons = []; + + if (hasImagePicker) { + buttons.push({ + icon: , + id: 'Image', + onPressHandler: pickAndUploadImageFromNativePicker, + }); + } + if (hasFilePicker) { + buttons.push({ icon: , id: 'File', onPressHandler: pickFile }); + } + buttons.push({ icon: , id: 'Camera', onPressHandler: takeAndUploadImage }); + + return ( + <> + { + onClose({}); + }} + style={[styles.container, containerPopupStyle, container]} + > + {/* all the attach buttons */} + {buttons.map(({ icon, id, onPressHandler }) => ( + onClose({ onPressHandler })}> + + {icon} + + + ))} + + {/* a square view with 50% opacity that semi hides the attach button */} + onClose({})} style={[buttonDimmerStyle, buttonDimmerStyleTheme]} /> + + ); +}; + +const styles = StyleSheet.create({ + attachButtonDimmer: { + opacity: 0, + position: 'absolute', + }, + buttonContainer: { + alignItems: 'center', + justifyContent: 'center', + marginBottom: ATTACH_MARGIN_BOTTOM, + }, + container: { + alignItems: 'center', + justifyContent: 'flex-end', + paddingTop: TOP_PADDING, + position: 'absolute', + }, +}); diff --git a/package/src/components/MessageInput/hooks/useAudioController.tsx b/package/src/components/MessageInput/hooks/useAudioController.tsx index 917336f606..4580e32184 100644 --- a/package/src/components/MessageInput/hooks/useAudioController.tsx +++ b/package/src/components/MessageInput/hooks/useAudioController.tsx @@ -12,7 +12,7 @@ import { SoundReturnType, triggerHaptic, } from '../../../native'; -import { File } from '../../../types/types'; +import { File, FileTypes } from '../../../types/types'; import { resampleWaveformData } from '../utils/audioSampling'; import { normalizeAudioLevel } from '../utils/normalizeAudioLevel'; @@ -255,7 +255,7 @@ export const useAudioController = () => { duration: durationInSeconds, mimeType: 'audio/aac', name: `audio_recording_${date}.aac`, - type: 'voiceRecording', + type: FileTypes.VoiceRecording, uri: typeof recording !== 'string' ? (recording?.getURI() as string) : (recording as string), waveform_data: resampledWaveformData, }; diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index cad825b23a..de5b9c4994 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -52,7 +52,7 @@ import { import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext'; import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext'; -import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../types/types'; const WAIT_FOR_SCROLL_TO_OFFSET_TIMEOUT = 150; const MAX_RETRIES_AFTER_SCROLL_FAILURE = 10; @@ -973,7 +973,7 @@ const MessageListWithContext = < if (!isMessageTypeDeleted && message.attachments) { return message.attachments.some( (attachment) => - attachment.type === 'image' && + attachment.type === FileTypes.Image && !attachment.title_link && !attachment.og_scrape_url && (attachment.image_url || attachment.thumb_url), @@ -1102,7 +1102,7 @@ const MessageListWithContext = < {/* Don't show the empty list indicator for Thread messages */} {processedMessageList.length === 0 && !thread ? ( - + {EmptyStateIndicator ? : null} ) : ( ['threadMessages'], read?: ChannelContextValue['read'], ) => { - const readData = messages.reduce((acc, cur) => { - if (cur.id) { - acc[cur.id] = false; - } - return acc; - }, {} as { [key: string]: boolean | number }); - - const filteredMessagesReversed = messages.filter((msg) => msg.updated_at).reverse(); + const readData: Record = {}; if (read) { - /** - * Channel read state is stored by user and we only care about users who aren't the client - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [clientUserId ?? '']: _ignore, ...filteredRead } = read; - const members = Object.values(filteredRead); - - /** - * Track number of members who have read previous messages - */ - let memberReadCount = 0; - /** * Array is in reverse order so newest message is at 0, * we find the index of the first message that is older @@ -41,65 +22,37 @@ export const getReadStates = < * if there are no newer messages, the first message is * last read message. */ - for (const message of filteredMessagesReversed) { - /** - * If all members are removed then they have read these - * messages. We do not increment memberReadCount for 1:1 - * chats, so this should be true, not a number in that case. - */ - if (!members.length) { - readData[message.id] = memberReadCount || true; - } else { - for (const member of members) { - /** - * If no last read continue, we can't remove the user - * because this would mark all messages in a new channel - * true until at least one other user reads a message. - */ - if (!member.last_read) { - continue; + Object.values(read).forEach((readState) => { + if (!readState.last_read) return; + + let userLastReadMsgId: string | undefined; + + // loop messages sent by current user and add read data for other users in channel + messages.forEach((msg) => { + if (msg.created_at && msg.created_at < readState.last_read) { + userLastReadMsgId = msg.id; + + // if true, save other user's read data for all messages they've read + if (!readData[userLastReadMsgId]) { + readData[userLastReadMsgId] = 0; } - /** - * If there there is a last read message add the user - * to the array of last reads for that message and remove - * the user from the list of users being checked - */ - if (message.created_at < member.last_read) { - /** - * if this is a direct message the length will be 1 - * as we already deleted the current user from the object - */ - const numberOfReads = Object.keys(read).length; - if (numberOfReads === 1) { - readData[message.id] = true; - } else { - const currentMessageReadData = readData[message.id]; - readData[message.id] = - typeof currentMessageReadData === 'boolean' - ? memberReadCount + 1 - : currentMessageReadData + 1; - } - const userIndex = members.findIndex(({ user }) => user.id === member.user?.id); - if (userIndex !== -1) { - members.splice(userIndex, 1); - if (numberOfReads > 1) { - memberReadCount += 1; - } - } + // Only increment read count if the message is not sent by the current user + if (msg.user?.id !== clientUserId) { + readData[userLastReadMsgId] = readData[userLastReadMsgId] + 1; } } + }); - /** - * If this is not the last message for a user this will still be - * set to false. But if other users have read further the number - * should be how many have read beyond this message. - */ - if (readData[message.id] === false) { - readData[message.id] = memberReadCount || false; + // if true, only save read data for other user's last read message + if (userLastReadMsgId) { + if (!readData[userLastReadMsgId]) { + readData[userLastReadMsgId] = 0; } + + readData[userLastReadMsgId] = readData[userLastReadMsgId] + 1; } - } + }); } return readData; diff --git a/package/src/components/Reply/Reply.tsx b/package/src/components/Reply/Reply.tsx index f1f20be57c..c0207db88f 100644 --- a/package/src/components/Reply/Reply.tsx +++ b/package/src/components/Reply/Reply.tsx @@ -22,7 +22,7 @@ import { TranslationContextValue, useTranslationContext, } from '../../contexts/translationContext/TranslationContext'; -import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DefaultStreamChatGenerics, FileTypes } from '../../types/types'; import { getResizedImageUrl } from '../../utils/getResizedImageUrl'; import { getTrimmedAttachmentTitle } from '../../utils/getTrimmedAttachmentTitle'; import { hasOnlyEmojis } from '../../utils/utils'; @@ -94,36 +94,36 @@ const getMessageType = < ) => { let messageType; - const isLastAttachmentFile = lastAttachment.type === 'file'; + const isLastAttachmentFile = lastAttachment.type === FileTypes.File; - const isLastAttachmentAudio = lastAttachment.type === 'audio'; + const isLastAttachmentAudio = lastAttachment.type === FileTypes.Audio; - const isLastAttachmentVoiceRecording = lastAttachment.type === 'voiceRecording'; + const isLastAttachmentVoiceRecording = lastAttachment.type === FileTypes.VoiceRecording; - const isLastAttachmentVideo = lastAttachment.type === 'video'; + const isLastAttachmentVideo = lastAttachment.type === FileTypes.Video; const isLastAttachmentGiphy = - lastAttachment?.type === 'giphy' || lastAttachment?.type === 'imgur'; + lastAttachment?.type === FileTypes.Giphy || lastAttachment?.type === FileTypes.Imgur; const isLastAttachmentImageOrGiphy = - lastAttachment?.type === 'image' && + lastAttachment?.type === FileTypes.Image && !lastAttachment?.title_link && !lastAttachment?.og_scrape_url; const isLastAttachmentImage = lastAttachment?.image_url || lastAttachment?.thumb_url; if (isLastAttachmentFile) { - messageType = 'file'; + messageType = FileTypes.File; } else if (isLastAttachmentVideo) { - messageType = 'video'; + messageType = FileTypes.Video; } else if (isLastAttachmentAudio) { - messageType = 'audio'; + messageType = FileTypes.Audio; } else if (isLastAttachmentVoiceRecording) { - messageType = 'voiceRecording'; + messageType = FileTypes.VoiceRecording; } else if (isLastAttachmentImageOrGiphy) { - if (isLastAttachmentImage) messageType = 'image'; + if (isLastAttachmentImage) messageType = FileTypes.Image; else messageType = undefined; - } else if (isLastAttachmentGiphy) messageType = 'giphy'; + } else if (isLastAttachmentGiphy) messageType = FileTypes.Giphy; else messageType = 'other'; return messageType; @@ -184,10 +184,10 @@ const ReplyWithContext = < const hasImage = !error && lastAttachment && - messageType !== 'file' && - messageType !== 'video' && - messageType !== 'audio' && - messageType !== 'voiceRecording' && + messageType !== FileTypes.File && + messageType !== FileTypes.Video && + messageType !== FileTypes.Audio && + messageType !== FileTypes.VoiceRecording && (lastAttachment.image_url || lastAttachment.thumb_url || lastAttachment.og_scrape_url); const onlyEmojis = !lastAttachment && emojiOnlyText; @@ -209,7 +209,9 @@ const ReplyWithContext = < ]} > {!error && lastAttachment ? ( - messageType === 'file' || messageType === 'voiceRecording' || messageType === 'audio' ? ( + messageType === FileTypes.File || + messageType === FileTypes.Audio || + messageType === FileTypes.VoiceRecording ? ( ) : null ) : null} - {messageType === 'video' && !lastAttachment.og_scrape_url ? ( + {messageType === FileTypes.Video && !lastAttachment.og_scrape_url ? ( 170 ? `${quotedMessage.text.slice(0, 170)}...` : quotedMessage.text - : messageType === 'image' + : messageType === FileTypes.Image ? t('Photo') - : messageType === 'video' + : messageType === FileTypes.Video ? t('Video') - : messageType === 'file' || - messageType === 'audio' || - messageType === 'voiceRecording' + : messageType === FileTypes.File || + messageType === FileTypes.Audio || + messageType === FileTypes.VoiceRecording ? trimmedLastAttachmentTitle || '' : '', }} @@ -279,7 +281,7 @@ const ReplyWithContext = < textContainer: [ { marginRight: - hasImage || messageType === 'video' + hasImage || messageType === FileTypes.Video ? Number( stylesProp.imageAttachment?.height || imageAttachment.height || @@ -290,9 +292,9 @@ const ReplyWithContext = < imageAttachment.marginLeft || styles.imageAttachment.marginLeft, ) - : messageType === 'file' || - messageType === 'audio' || - messageType === 'voiceRecording' + : messageType === FileTypes.File || + messageType === FileTypes.Audio || + messageType === FileTypes.VoiceRecording ? attachmentSize + Number( stylesProp.fileAttachmentContainer?.paddingLeft || @@ -307,7 +309,7 @@ const ReplyWithContext = < ], }} /> - {messageType === 'audio' || messageType === 'voiceRecording' ? ( + {messageType === FileTypes.Audio || messageType === FileTypes.VoiceRecording ? ( {lastAttachment.duration ? dayjs.duration(lastAttachment.duration, 'second').format('mm:ss') diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index e5dff8cd6b..914a9e2165 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -1682,7 +1682,7 @@ exports[`Thread should match thread snapshot 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": false, + "disabled": undefined, "expanded": undefined, "selected": undefined, } @@ -1701,6 +1701,7 @@ exports[`Thread should match thread snapshot 1`] = ` onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onLayout={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -1716,14 +1717,14 @@ exports[`Thread should match thread snapshot 1`] = ` > - + > + + + + + + + @@ -1778,7 +1825,7 @@ exports[`Thread should match thread snapshot 1`] = ` { "busy": undefined, "checked": undefined, - "disabled": false, + "disabled": undefined, "expanded": undefined, "selected": undefined, } @@ -1812,14 +1859,14 @@ exports[`Thread should match thread snapshot 1`] = ` > @@ -1914,7 +1963,7 @@ exports[`Thread should match thread snapshot 1`] = ` [ { "flex": 1, - "fontSize": 14, + "fontSize": 16, "includeFontPadding": false, "padding": 0, "paddingTop": 0, diff --git a/package/src/contexts/messageContext/MessageContext.tsx b/package/src/contexts/messageContext/MessageContext.tsx index 1c062c296b..3cbe9ad1d5 100644 --- a/package/src/contexts/messageContext/MessageContext.tsx +++ b/package/src/contexts/messageContext/MessageContext.tsx @@ -110,7 +110,7 @@ export type MessageContextValue< preventPress?: boolean; /** Whether or not the avatar show show next to Message */ showAvatar?: boolean; -} & Pick, 'channel' | 'disabled' | 'members'>; +} & Pick, 'channel' | 'members'>; export const MessageContext = React.createContext( DEFAULT_BASE_CONTEXT_VALUE as MessageContextValue, diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx index 385e9146da..e3bbc60c98 100644 --- a/package/src/contexts/messageInputContext/MessageInputContext.tsx +++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx @@ -1,7 +1,6 @@ import type { LegacyRef } from 'react'; import React, { PropsWithChildren, useContext, useEffect, useRef, useState } from 'react'; -import type { TextInput, TextInputProps } from 'react-native'; -import { Alert, Keyboard } from 'react-native'; +import { Alert, Keyboard, Linking, TextInput, TextInputProps } from 'react-native'; import uniq from 'lodash/uniq'; import { lookup } from 'mime-types'; @@ -47,11 +46,12 @@ import type { SendButtonProps } from '../../components/MessageInput/SendButton'; import type { UploadProgressIndicatorProps } from '../../components/MessageInput/UploadProgressIndicator'; import type { MessageType } from '../../components/MessageList/hooks/useMessageList'; import type { Emoji } from '../../emoji-data'; -import { pickDocument } from '../../native'; -import type { +import { pickDocument, pickImage, takePhoto } from '../../native'; +import { Asset, DefaultStreamChatGenerics, File, + FileTypes, FileUpload, ImageUpload, UnknownType, @@ -159,6 +159,10 @@ export type LocalMessageInputContext< openCommandsPicker: () => void; openFilePicker: () => void; openMentionsPicker: () => void; + /** + * Function for picking a photo from native image picker and uploading it. + */ + pickAndUploadImageFromNativePicker: () => Promise; pickFile: () => Promise; /** * Function for removing a file from the upload preview @@ -202,6 +206,10 @@ export type LocalMessageInputContext< setShowMoreOptions: React.Dispatch>; setText: React.Dispatch>; showMoreOptions: boolean; + /** + * Function for taking a photo and uploading it + */ + takeAndUploadImage: () => Promise; text: string; toggleAttachmentPicker: () => void; /** @@ -219,7 +227,7 @@ export type LocalMessageInputContext< export type InputMessageInputContextValue< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'disabled'> & { +> = { /** * Controls how many pixels to the top side the user has to scroll in order to lock the recording view and allow the user to lift their finger from the screen without stopping the recording. */ @@ -241,7 +249,7 @@ export type InputMessageInputContextValue< * * Defaults to and accepts same props as: [AttachButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/attach-button/) */ - AttachButton: React.ComponentType>; + AttachButton: React.ComponentType; /** * Custom UI component for audio attachment upload preview. * @@ -307,6 +315,8 @@ export type InputMessageInputContextValue< */ FileUploadPreview: React.ComponentType>; + /** When false, CameraSelectorIcon will be hidden */ + hasCameraPicker: boolean; /** When false, CommandsButton will be hidden */ hasCommands: boolean; /** When false, FileSelectorIcon will be hidden */ @@ -329,7 +339,7 @@ export type InputMessageInputContextValue< * * Defaults to and accepts same props as: [MoreOptionsButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/more-options-button/) */ - MoreOptionsButton: React.ComponentType>; + MoreOptionsButton: React.ComponentType; /** Limit on the number of lines in the text input before scrolling */ numberOfLines: number; quotedMessage: boolean | MessageType; @@ -422,6 +432,11 @@ export type InputMessageInputContextValue< */ emojiSearchIndex?: EmojiSearchIndex; + /** + * Handler for when the attach button is pressed. + */ + handleAttachButtonPress?: () => void; + /** Initial value to set on input */ initialValue?: string; /** @@ -534,7 +549,7 @@ export const MessageInputProvider = < }>({}); const [giphyActive, setGiphyActive] = useState(false); const [sendThreadMessageInChannel, setSendThreadMessageInChannel] = useState(false); - const { editing, hasFilePicker, hasImagePicker, initialValue } = value; + const { editing, initialValue } = value; const { fileUploads, imageUploads, @@ -624,21 +639,77 @@ export const MessageInputProvider = < } }; - const openAttachmentPicker = () => { - if (hasImagePicker) { - Keyboard.dismiss(); - setSelectedPicker('images'); - openPicker(); - } else if (hasFilePicker) { - pickFile(); + /** + * Function for capturing a photo and uploading it + */ + const takeAndUploadImage = async () => { + setSelectedPicker(undefined); + closePicker(); + const photo = await takePhoto({ compressImageQuality: value.compressImageQuality }); + if (photo.askToOpenSettings) { + Alert.alert( + t('Allow camera access in device settings'), + t('Device camera is used to take photos or videos.'), + [ + { style: 'cancel', text: t('Cancel') }, + { onPress: () => Linking.openSettings(), style: 'default', text: t('Open Settings') }, + ], + ); + } + if (!photo.cancelled) { + setSelectedImages((images) => [...images, photo]); + } + }; + + /** + * Function for picking a photo from native image picker and uploading it + */ + const pickAndUploadImageFromNativePicker = async () => { + const result = await pickImage(); + if (result.askToOpenSettings) { + Alert.alert( + t('Allow access to your Gallery'), + t('Device gallery permissions is used to take photos or videos.'), + [ + { style: 'cancel', text: t('Cancel') }, + { onPress: () => Linking.openSettings(), style: 'default', text: t('Open Settings') }, + ], + ); + } + if (result.assets && result.assets.length > 0) { + result.assets.forEach((asset) => { + if (asset.type.includes('image')) { + setSelectedImages((prevImages) => [...prevImages, asset]); + } else { + setSelectedFiles((prevFiles) => [ + ...prevFiles, + { ...asset, mimeType: asset.type, type: FileTypes.Video }, + ]); + } + }); } }; + /** + * Function to open the attachment picker if the MediaLibary is installed. + */ + const openAttachmentPicker = () => { + Keyboard.dismiss(); + setSelectedPicker('images'); + openPicker(); + }; + + /** + * Function to close the attachment picker if the MediaLibrary is installed. + */ const closeAttachmentPicker = () => { setSelectedPicker(undefined); closePicker(); }; + /** + * Function to toggle the attachment picker if the MediaLibrary is installed. + */ const toggleAttachmentPicker = () => { if (selectedPicker) { closeAttachmentPicker(); @@ -721,20 +792,20 @@ export const MessageInputProvider = < original_height: image.height, original_width: image.width, originalImage: image.file, - type: 'image', + type: FileTypes.Image, }; }; const mapFileUploadToAttachment = (file: FileUpload): Attachment => { - if (file.type === 'image') { + if (file.type === FileTypes.Image) { return { fallback: file.file.name, image_url: file.url, mime_type: file.file.mimeType, originalFile: file.file, - type: 'image', + type: FileTypes.Image, }; - } else if (file.type === 'audio') { + } else if (file.type === FileTypes.Audio) { return { asset_url: file.url || file.file.uri, duration: file.file.duration, @@ -742,9 +813,9 @@ export const MessageInputProvider = < mime_type: file.file.mimeType, originalFile: file.file, title: file.file.name, - type: 'audio', + type: FileTypes.Audio, }; - } else if (file.type === 'video') { + } else if (file.type === FileTypes.Video) { return { asset_url: file.url || file.file.uri, duration: file.file.duration, @@ -753,9 +824,9 @@ export const MessageInputProvider = < originalFile: file.file, thumb_url: file.thumb_url, title: file.file.name, - type: 'video', + type: FileTypes.Video, }; - } else if (file.type === 'voiceRecording') { + } else if (file.type === FileTypes.VoiceRecording) { return { asset_url: file.url || file.file.uri, duration: file.file.duration, @@ -763,7 +834,7 @@ export const MessageInputProvider = < mime_type: file.file.mimeType, originalFile: file.file, title: file.file.name, - type: 'voiceRecording', + type: FileTypes.VoiceRecording, waveform_data: file.file.waveform_data, }; } else { @@ -773,7 +844,7 @@ export const MessageInputProvider = < mime_type: file.file.mimeType, originalFile: file.file, title: file.file.name, - type: 'file', + type: FileTypes.File, }; } }; @@ -938,7 +1009,7 @@ export const MessageInputProvider = < const attachments = [ { image_url: image.url, - type: 'image', + type: FileTypes.Image, }, ] as StreamMessage['attachments']; @@ -1285,6 +1356,7 @@ export const MessageInputProvider = < openCommandsPicker, openFilePicker: pickFile, openMentionsPicker, + pickAndUploadImageFromNativePicker, pickFile, removeFile, removeImage, @@ -1305,6 +1377,7 @@ export const MessageInputProvider = < setShowMoreOptions, setText, showMoreOptions, + takeAndUploadImage, text, thread, toggleAttachmentPicker, diff --git a/package/src/contexts/messageInputContext/__tests__/MessageInputContext.test.tsx b/package/src/contexts/messageInputContext/__tests__/MessageInputContext.test.tsx index ee9cb6feae..3670fce03d 100644 --- a/package/src/contexts/messageInputContext/__tests__/MessageInputContext.test.tsx +++ b/package/src/contexts/messageInputContext/__tests__/MessageInputContext.test.tsx @@ -7,15 +7,11 @@ import type { AppSettingsAPIResponse, StreamChat } from 'stream-chat'; import { ChatContextValue, ChatProvider } from '../../../contexts/chatContext/ChatContext'; -import { - generateFileAttachment, - generateImageAttachment, -} from '../../../mock-builders/generator/attachment'; +import { generateImageAttachment } from '../../../mock-builders/generator/attachment'; import { generateMessage } from '../../../mock-builders/generator/message'; import { generateUser } from '../../../mock-builders/generator/user'; -import * as NativeUtils from '../../../native'; import type { DefaultStreamChatGenerics } from '../../../types/types'; import { FileState } from '../../../utils/utils'; import { @@ -235,37 +231,4 @@ describe('MessageInputContext', () => { expect(result.current.text).toBe(`${initialProps.editing.text}@`); }); }); - - it('openAttachmentPicker works', async () => { - jest.spyOn(NativeUtils, 'pickDocument').mockImplementation( - jest.fn().mockResolvedValue({ - cancelled: false, - docs: [generateFileAttachment(), generateImageAttachment()], - }), - ); - const initialProps = { - editing: message, - hasFilePicker: true, - hasImagePicker: false, - }; - const { result } = renderHook(() => useMessageInputContext(), { - initialProps, - wrapper: (props) => ( - - ), - }); - - act(() => { - result.current.openAttachmentPicker(); - }); - - await waitFor(async () => { - expect(await result.current.pickFile()).toBe(undefined); - }); - }); }); diff --git a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts index a09a8737d7..2de2079521 100644 --- a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts +++ b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts @@ -31,7 +31,6 @@ export const useCreateMessageInputContext = < compressImageQuality, cooldownEndsAt, CooldownTimer, - disabled, doDocUploadRequest, doImageUploadRequest, editing, @@ -40,6 +39,8 @@ export const useCreateMessageInputContext = < FileUploadPreview, fileUploads, giphyActive, + handleAttachButtonPress, + hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, @@ -68,7 +69,7 @@ export const useCreateMessageInputContext = < openCommandsPicker, openFilePicker, openMentionsPicker, - + pickAndUploadImageFromNativePicker, pickFile, quotedMessage, removeFile, @@ -98,6 +99,7 @@ export const useCreateMessageInputContext = < showMoreOptions, ShowThreadMessageInChannelButton, StartAudioRecordingButton, + takeAndUploadImage, text, thread, toggleAttachmentPicker, @@ -151,7 +153,6 @@ export const useCreateMessageInputContext = < compressImageQuality, cooldownEndsAt, CooldownTimer, - disabled, doDocUploadRequest, doImageUploadRequest, editing, @@ -160,6 +161,8 @@ export const useCreateMessageInputContext = < FileUploadPreview, fileUploads, giphyActive, + handleAttachButtonPress, + hasCameraPicker, hasCommands, hasFilePicker, hasImagePicker, @@ -188,6 +191,7 @@ export const useCreateMessageInputContext = < openCommandsPicker, openFilePicker, openMentionsPicker, + pickAndUploadImageFromNativePicker, pickFile, quotedMessage, removeFile, @@ -217,6 +221,7 @@ export const useCreateMessageInputContext = < showMoreOptions, ShowThreadMessageInChannelButton, StartAudioRecordingButton, + takeAndUploadImage, text, toggleAttachmentPicker, triggerSettings, @@ -231,7 +236,6 @@ export const useCreateMessageInputContext = < asyncIdsLength, asyncUploadsValue, cooldownEndsAt, - disabled, editingdep, fileUploadsValue, giphyActive, diff --git a/package/src/contexts/messageInputContext/hooks/useMessageDetailsForState.ts b/package/src/contexts/messageInputContext/hooks/useMessageDetailsForState.ts index 9abf1c0b4b..17fda91d8d 100644 --- a/package/src/contexts/messageInputContext/hooks/useMessageDetailsForState.ts +++ b/package/src/contexts/messageInputContext/hooks/useMessageDetailsForState.ts @@ -2,7 +2,12 @@ import { useEffect, useState } from 'react'; import { Attachment } from 'stream-chat'; -import type { DefaultStreamChatGenerics, FileUpload, ImageUpload } from '../../../types/types'; +import { + DefaultStreamChatGenerics, + FileTypes, + FileUpload, + ImageUpload, +} from '../../../types/types'; import { generateRandomId } from '../../../utils/utils'; import type { MessageInputContextValue } from '../MessageInputContext'; @@ -44,7 +49,7 @@ export const useMessageDetailsForState = < const mapAttachmentToFileUpload = (attachment: Attachment): FileUpload => { const id = generateRandomId(); - if (attachment.type === 'audio') { + if (attachment.type === FileTypes.Audio) { return { file: { duration: attachment.duration, @@ -57,7 +62,7 @@ export const useMessageDetailsForState = < state: 'finished', url: attachment.asset_url, }; - } else if (attachment.type === 'video') { + } else if (attachment.type === FileTypes.Video) { return { file: { mimeType: attachment.mime_type, @@ -69,7 +74,7 @@ export const useMessageDetailsForState = < thumb_url: attachment.thumb_url, url: attachment.asset_url, }; - } else if (attachment.type === 'voiceRecording') { + } else if (attachment.type === FileTypes.VoiceRecording) { return { file: { duration: attachment.duration, @@ -83,7 +88,7 @@ export const useMessageDetailsForState = < state: 'finished', url: attachment.asset_url, }; - } else if (attachment.type === 'file') { + } else if (attachment.type === FileTypes.File) { return { file: { mimeType: attachment.mime_type, @@ -117,7 +122,7 @@ export const useMessageDetailsForState = < const attachments = Array.isArray(message.attachments) ? message.attachments : []; for (const attachment of attachments) { - if (attachment.type === 'image') { + if (attachment.type === FileTypes.Image) { const id = generateRandomId(); newImageUploads.push({ file: { diff --git a/package/src/contexts/overlayContext/OverlayProvider.tsx b/package/src/contexts/overlayContext/OverlayProvider.tsx index cf42500014..d74bdb1d8c 100644 --- a/package/src/contexts/overlayContext/OverlayProvider.tsx +++ b/package/src/contexts/overlayContext/OverlayProvider.tsx @@ -30,6 +30,7 @@ import { OverlayBackdrop } from '../../components/MessageOverlay/OverlayBackdrop import { useStreami18n } from '../../hooks/useStreami18n'; import { useViewport } from '../../hooks/useViewport'; +import { isImageMediaLibraryAvailable } from '../../native'; import type { DefaultStreamChatGenerics } from '../../types/types'; import { AttachmentPickerProvider } from '../attachmentPickerContext/AttachmentPickerContext'; import { ImageGalleryProvider } from '../imageGalleryContext/ImageGalleryContext'; @@ -253,7 +254,9 @@ export const OverlayProvider = < overlayOpacity={overlayOpacity} /> )} - + {isImageMediaLibraryAvailable() ? ( + + ) : null} diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index 87c203a750..25d78eccae 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -318,6 +318,11 @@ export type Theme = { inputBoxContainer: ViewStyle; micButtonContainer: ViewStyle; moreOptionsButton: ViewStyle; + nativeAttachmentPicker: { + buttonContainer: ViewStyle; + buttonDimmerStyle: ViewStyle; + container: ViewStyle; + }; optionsContainer: ViewStyle; replyContainer: ViewStyle; searchIcon: IconProps; @@ -885,6 +890,11 @@ export const defaultTheme: Theme = { inputBoxContainer: {}, micButtonContainer: {}, moreOptionsButton: {}, + nativeAttachmentPicker: { + buttonContainer: {}, + buttonDimmerStyle: {}, + container: {}, + }, optionsContainer: {}, replyContainer: {}, searchIcon: {}, diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json index 22e318a08c..29679442d5 100644 --- a/package/src/i18n/en.json +++ b/package/src/i18n/en.json @@ -14,6 +14,7 @@ "Delete": "Delete", "Delete Message": "Delete Message", "Device camera is used to take photos or videos.": "Device camera is used to take photos or videos.", + "Device gallery permissions is used to take photos or videos.": "Device gallery permissions is used to take photos or videos.", "Do you want to send a copy of this message to a moderator for further investigation?": "Do you want to send a copy of this message to a moderator for further investigation?", "Edit Message": "Edit Message", "Edited": "Edited", diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json index 95fd991062..b6c8300b13 100644 --- a/package/src/i18n/es.json +++ b/package/src/i18n/es.json @@ -14,6 +14,7 @@ "Delete": "Eliminar", "Delete Message": "Eliminar mensaje", "Device camera is used to take photos or videos.": "La cámara del dispositivo se utiliza para tomar fotografías o vídeos.", + "Device gallery permissions is used to take photos or videos.": "Los permisos de la galería del dispositivo se utilizan para tomar fotos o videos.", "Do you want to send a copy of this message to a moderator for further investigation?": "¿Deseas enviar una copia de este mensaje a un moderador para una investigación adicional?", "Edit Message": "Editar mensaje", "Edited": "Editado", diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json index 5a336e162a..6455b2e8b0 100644 --- a/package/src/i18n/fr.json +++ b/package/src/i18n/fr.json @@ -14,6 +14,7 @@ "Delete": "Supprimer", "Delete Message": "Supprimer un message", "Device camera is used to take photos or videos.": "L'appareil photo de l'appareil est utilisé pour prendre des photos ou des vidéos.", + "Device gallery permissions is used to take photos or videos.": "Les autorisations de la galerie de l'appareil sont utilisées pour prendre des photos ou des vidéos.", "Do you want to send a copy of this message to a moderator for further investigation?": "Voulez-vous envoyer une copie de ce message à un modérateur pour une enquête plus approfondie?", "Edit Message": "Éditer un message", "Edited": "Édité", diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json index d18cddad33..e73e217b52 100644 --- a/package/src/i18n/he.json +++ b/package/src/i18n/he.json @@ -14,6 +14,7 @@ "Delete": "מחק", "Delete Message": "מחק/י הודעה", "Device camera is used to take photos or videos.": "מצלמת המכשיר משמשת לצילום תמונות או סרטונים.", + "Device gallery permissions is used to take photos or videos.": "הרשאות גלריית המכשיר משמשות לצילום תמונות או סרטונים.", "Do you want to send a copy of this message to a moderator for further investigation?": "האם את/ה רוצה לשלוח עותק של הודעה זו למנחה להמשך חקירה?", "Edit Message": "ערוך הודעה", "Edited": "נערך", diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json index b007f5fe59..91e094dfbb 100644 --- a/package/src/i18n/hi.json +++ b/package/src/i18n/hi.json @@ -14,6 +14,7 @@ "Delete": "हटाएं", "Delete Message": "मैसेज को डिलीट करे", "Device camera is used to take photos or videos.": "डिवाइस कैमरे का उपयोग फ़ोटो या वीडियो लेने के लिए किया जाता है।", + "Device gallery permissions is used to take photos or videos.": "डिवाइस गैलरी की अनुमतियों का उपयोग फोटो या वीडियो लेने के लिए किया जाता है।", "Do you want to send a copy of this message to a moderator for further investigation?": "क्या आप इस संदेश की एक प्रति आगे की जाँच के लिए किसी मॉडरेटर को भेजना चाहते हैं?", "Edit Message": "मैसेज में बदलाव करे", "Edited": "मैसेज बदला गया है", diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json index ef20722fd2..27085dd103 100644 --- a/package/src/i18n/it.json +++ b/package/src/i18n/it.json @@ -14,6 +14,7 @@ "Delete": "Elimina", "Delete Message": "Cancella il Messaggio", "Device camera is used to take photos or videos.": "La fotocamera del dispositivo viene utilizzata per scattare foto o video.", + "Device gallery permissions is used to take photos or videos.": "Le autorizzazioni della galleria del dispositivo vengono utilizzate per scattare foto o video.", "Do you want to send a copy of this message to a moderator for further investigation?": "Vuoi inviare una copia di questo messaggio a un moderatore per ulteriori indagini?", "Edit Message": "Modifica Messaggio", "Edited": "Modificato", diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json index a94708e730..0d08199b57 100644 --- a/package/src/i18n/ja.json +++ b/package/src/i18n/ja.json @@ -14,6 +14,7 @@ "Delete": "消去", "Delete Message": "メッセージを削除", "Device camera is used to take photos or videos.": "デバイスのカメラは写真やビデオの撮影に使用されます。", + "Device gallery permissions is used to take photos or videos.": "デバイスギャラリーの権限は写真やビデオを撮るために使用されます。", "Do you want to send a copy of this message to a moderator for further investigation?": "このメッセージのコピーをモデレーターに送信して、さらに調査しますか?", "Edit Message": "メッセージを編集", "Edited": "編集済み", diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json index ad9f371912..e8851916b2 100644 --- a/package/src/i18n/ko.json +++ b/package/src/i18n/ko.json @@ -14,6 +14,7 @@ "Delete": "삭제", "Delete Message": "메시지 삭제", "Device camera is used to take photos or videos.": "기기 카메라는 사진이나 동영상을 촬영하는 데 사용됩니다.", + "Device gallery permissions is used to take photos or videos.": "장치 갤러리 권한은 사진 또는 비디오를 촬영하는 데 사용됩니다.", "Do you want to send a copy of this message to a moderator for further investigation?": "이 메시지의 복사본을 운영자에게 보내 추가 조사를합니까?", "Edit Message": "메시지 수정", "Edited": "편집됨", diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json index acd87a08be..0fb165cff2 100644 --- a/package/src/i18n/nl.json +++ b/package/src/i18n/nl.json @@ -14,6 +14,7 @@ "Delete": "Verwijderen", "Delete Message": "Verwijder bericht", "Device camera is used to take photos or videos.": "De camera van het apparaat wordt gebruikt om foto's of video's te maken.", + "Device gallery permissions is used to take photos or videos.": "Apparaatgallerijmachtigingen worden gebruikt om foto’s of video’s te maken.", "Do you want to send a copy of this message to a moderator for further investigation?": "Wil je een kopie van dit bericht naar een moderator sturen voor verder onderzoek?", "Edit Message": "Pas bericht aan", "Edited": "Bewerkt", diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json index bc544145f0..66b1a745a3 100644 --- a/package/src/i18n/pt-br.json +++ b/package/src/i18n/pt-br.json @@ -14,6 +14,7 @@ "Delete": "Excluir", "Delete Message": "Excluir Mensagem", "Device camera is used to take photos or videos.": "A câmera do dispositivo é usada para tirar fotos ou vídeos.", + "Device gallery permissions is used to take photos or videos.": "As permissões da galeria do dispositivo são usadas para tirar fotos ou vídeos.", "Do you want to send a copy of this message to a moderator for further investigation?": "Deseja enviar uma cópia desta mensagem para um moderador para investigação adicional?", "Edit Message": "Editar Mensagem", "Edited": "Editado", diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json index 1d3b07a725..9acf5d7c57 100644 --- a/package/src/i18n/ru.json +++ b/package/src/i18n/ru.json @@ -14,6 +14,7 @@ "Delete": "удалять", "Delete Message": "Удалить сообщение", "Device camera is used to take photos or videos.": "Камера устройства используется для съемки фотографий или видео.", + "Device gallery permissions is used to take photos or videos.": "Разрешения галереи устройства используются для съемки фото или видео.", "Do you want to send a copy of this message to a moderator for further investigation?": "Вы хотите отправить копию этого сообщения модератору для дальнейшего изучения?", "Edit Message": "Редактировать сообщение", "Edited": "Отредактировано", diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json index f10c13e48a..95901fd3f9 100644 --- a/package/src/i18n/tr.json +++ b/package/src/i18n/tr.json @@ -14,6 +14,7 @@ "Delete": "Sil", "Delete Message": "Mesajı Sil", "Device camera is used to take photos or videos.": "Cihaz kamerası fotoğraf veya video çekmek için kullanılır.", + "Device gallery permissions is used to take photos or videos.": "Cihaz galerisi izinleri fotoğraf veya video çekmek için kullanılır.", "Do you want to send a copy of this message to a moderator for further investigation?": "Detaylı inceleme için bu mesajın kopyasını moderatöre göndermek istiyor musunuz?", "Edit Message": "Mesajı Düzenle", "Edited": "Düzenlendi", diff --git a/package/src/icons/Attach.tsx b/package/src/icons/Attach.tsx index 700e1ca1f4..56c9ec5b66 100644 --- a/package/src/icons/Attach.tsx +++ b/package/src/icons/Attach.tsx @@ -1,12 +1,25 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; - -export const Attach = (props: IconProps) => ( - - - +import Svg, { ClipPath, Defs, G, Path, Rect } from 'react-native-svg'; + +import { IconProps } from './utils/base'; + +type Props = IconProps & { + size: number; +}; + +export const Attach = ({ size, ...rest }: Props) => ( + + + + + + + + + + ); diff --git a/package/src/icons/GiphyLightning.tsx b/package/src/icons/GiphyLightning.tsx new file mode 100644 index 0000000000..4b8c02ce2d --- /dev/null +++ b/package/src/icons/GiphyLightning.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; + +type Props = IconProps & { + size: number; +}; + +export const GiphyLightning = ({ size = 16, ...rest }: Props) => ( + + + +); diff --git a/package/src/icons/Lightning.tsx b/package/src/icons/Lightning.tsx index cb748ecdc1..9ca6c02750 100644 --- a/package/src/icons/Lightning.tsx +++ b/package/src/icons/Lightning.tsx @@ -1,9 +1,18 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; -export const Lightning = (props: IconProps) => ( - - - +import { IconProps } from './utils/base'; + +type Props = IconProps & { + size: number; +}; + +export const Lightning = ({ size = 32, ...rest }: Props) => ( + + + ); diff --git a/package/src/icons/index.ts b/package/src/icons/index.ts index 4da601145f..57f81dd6ee 100644 --- a/package/src/icons/index.ts +++ b/package/src/icons/index.ts @@ -30,6 +30,7 @@ export * from './Flag'; export * from './Folder'; export * from './GenericFile'; export * from './GiphyIcon'; +export * from './GiphyLightning'; export * from './Grid'; export * from './Group'; export * from './HTML'; diff --git a/package/src/native.ts b/package/src/native.ts index 44ee874048..7120c8c583 100644 --- a/package/src/native.ts +++ b/package/src/native.ts @@ -66,6 +66,15 @@ type PickDocument = ({ maxNumberOfFiles }: { maxNumberOfFiles?: number }) => | never; export let pickDocument: PickDocument = fail; +type PickImageAssetType = { + askToOpenSettings?: boolean; + assets?: Array & { source: 'picker' }>; + cancelled?: boolean; +}; + +type PickImage = () => Promise | never; +export let pickImage: PickImage = fail; + type SaveFileOptions = { fileName: string; fromUrl: string; @@ -83,16 +92,11 @@ type ShareOptions = { type ShareImage = (options: ShareOptions) => Promise | never; export let shareImage: ShareImage = fail; -type Photo = - | (Omit & { - cancelled: false; - source: 'camera'; - askToOpenSettings?: boolean; - }) - | { - cancelled: true; - askToOpenSettings?: boolean; - }; +type Photo = Omit & { + source: 'camera'; + askToOpenSettings?: boolean; + cancelled?: boolean; +}; type TakePhoto = (options: { compressImageQuality?: number }) => Promise | never; export let takePhoto: TakePhoto = fail; @@ -294,6 +298,7 @@ type Handlers = { NetInfo?: NetInfo; oniOS14GalleryLibrarySelectionChange?: OniOS14LibrarySelectionChange; pickDocument?: PickDocument; + pickImage?: PickImage; saveFile?: SaveFile; SDK?: string; setClipboardString?: SetClipboardString; @@ -324,19 +329,19 @@ export const registerNativeHandlers = (handlers: Handlers) => { NetInfo = handlers.NetInfo; } - if (handlers.getLocalAssetUri) { + if (handlers.getLocalAssetUri !== undefined) { getLocalAssetUri = handlers.getLocalAssetUri; } - if (handlers.getPhotos) { + if (handlers.getPhotos !== undefined) { getPhotos = handlers.getPhotos; } - if (handlers.iOS14RefreshGallerySelection) { + if (handlers.iOS14RefreshGallerySelection !== undefined) { iOS14RefreshGallerySelection = handlers.iOS14RefreshGallerySelection; } - if (handlers.oniOS14GalleryLibrarySelectionChange) { + if (handlers.oniOS14GalleryLibrarySelectionChange !== undefined) { oniOS14GalleryLibrarySelectionChange = handlers.oniOS14GalleryLibrarySelectionChange; } @@ -344,6 +349,10 @@ export const registerNativeHandlers = (handlers: Handlers) => { pickDocument = handlers.pickDocument; } + if (handlers.pickImage !== undefined) { + pickImage = handlers.pickImage; + } + if (handlers.saveFile) { saveFile = handlers.saveFile; } @@ -360,11 +369,11 @@ export const registerNativeHandlers = (handlers: Handlers) => { Sound = handlers.Sound; } - if (handlers.takePhoto) { + if (handlers.takePhoto !== undefined) { takePhoto = handlers.takePhoto; } - if (handlers.triggerHaptic) { + if (handlers.triggerHaptic !== undefined) { triggerHaptic = handlers.triggerHaptic; } @@ -377,6 +386,12 @@ export const registerNativeHandlers = (handlers: Handlers) => { } }; +export const isImagePickerAvailable = () => !!takePhoto; export const isVideoPackageAvailable = () => !!Video; export const isAudioPackageAvailable = () => !!Sound.Player || !!Sound.initializeSound; export const isRecordingPackageAvailable = () => !!Audio; +export const isImageMediaLibraryAvailable = () => + !!getPhotos && + !!iOS14RefreshGallerySelection && + !!oniOS14GalleryLibrarySelectionChange && + !!getLocalAssetUri; diff --git a/package/src/types/types.ts b/package/src/types/types.ts index e0d53f6973..3ff97c89f6 100644 --- a/package/src/types/types.ts +++ b/package/src/types/types.ts @@ -2,6 +2,16 @@ import type { ExtendableGenerics, LiteralStringForUnion } from 'stream-chat'; import type { FileStateValue } from '../utils/utils'; +export enum FileTypes { + Audio = 'audio', + File = 'file', + Giphy = 'giphy', + Image = 'image', + Imgur = 'imgur', + Video = 'video', + VoiceRecording = 'voiceRecording', +} + export type Asset = { duration: number; height: number; @@ -20,7 +30,7 @@ export type File = { id?: string; mimeType?: string; size?: number; - type?: 'file' | 'image' | 'video' | 'audio' | 'voiceRecording'; + type?: FileTypes; // The uri should be of type `string`. But is `string|undefined` because the same type is used for the response from Stream's Attachment. This shall be fixed. uri?: string; waveform_data?: number[]; diff --git a/package/src/utils/utils.ts b/package/src/utils/utils.ts index bcec4894e2..5240790493 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -1,5 +1,6 @@ import type React from 'react'; +import dayjs from 'dayjs'; import EmojiRegex from 'emoji-regex'; import type { DebouncedFunc } from 'lodash'; import debounce from 'lodash/debounce'; @@ -607,6 +608,7 @@ export const stringifyMessage = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ deleted_at, + i18n, latest_reactions, reaction_groups, readBy, @@ -630,7 +632,7 @@ export const stringifyMessage = < ) .join() : '' - }${type}${deleted_at}${text}${readBy}${reply_count}${status}${updated_at}`; + }${type}${deleted_at}${text}${readBy}${reply_count}${status}${updated_at}${JSON.stringify(i18n)}`; /** * Reduces a list of messages to strings that are used in useEffect & useMemo @@ -656,3 +658,22 @@ export const getFileNameFromPath = (path: string) => { const match = path.match(pattern); return match ? match[0] : ''; }; + +/** + * Utility to get the duration label from the duration in seconds. + * @param duration number + * @returns string + */ +export const getDurationLabelFromDuration = (duration: number) => { + const ONE_HOUR_IN_SECONDS = 3600; + const ONE_HOUR_IN_MILLISECONDS = ONE_HOUR_IN_SECONDS * 1000; + let durationLabel = '00:00'; + const isDurationLongerThanHour = duration / ONE_HOUR_IN_MILLISECONDS >= 1; + const formattedDurationParam = isDurationLongerThanHour ? 'HH:mm:ss' : 'mm:ss'; + const formattedVideoDuration = dayjs + .duration(duration, 'milliseconds') + .format(formattedDurationParam); + durationLabel = formattedVideoDuration; + + return durationLabel; +};