From 570b76a71d2f3a3d8d8d6bbefa7aedcf86ecffad Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 19 Jan 2023 17:36:49 -0600 Subject: [PATCH] Add the ability to paste images into the composer (#56) --- ios/Podfile.lock | 10 ++++ package.json | 10 +++- src/view/com/composer/ComposePost.tsx | 53 +++++++++++++++---- src/view/com/composer/PhotoCarouselPicker.tsx | 41 +++++++++----- yarn.lock | 21 +++++--- 5 files changed, 101 insertions(+), 34 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 80249b61..16acc15e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -244,6 +244,9 @@ PODS: - React-Core - react-native-pager-view (6.1.2): - React-Core + - react-native-paste-input (0.6.0): + - React-Core + - Swime (= 3.0.6) - react-native-safe-area-context (4.4.1): - RCT-Folly - RCTRequired @@ -390,6 +393,7 @@ PODS: - React-RCTImage - RNSVG (12.5.0): - React-Core + - Swime (3.0.6) - TOCropViewController (2.6.1) - Yoga (1.14.0) @@ -421,6 +425,7 @@ DEPENDENCIES: - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" - "react-native-image-resizer (from `../node_modules/@bam.tech/react-native-image-resizer`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) + - "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) - react-native-version-number (from `../node_modules/react-native-version-number`) @@ -454,6 +459,7 @@ SPEC REPOS: trunk: - fmt - libevent + - Swime - TOCropViewController EXTERNAL SOURCES: @@ -507,6 +513,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@bam.tech/react-native-image-resizer" react-native-pager-view: :path: "../node_modules/react-native-pager-view" + react-native-paste-input: + :path: "../node_modules/@mattermost/react-native-paste-input" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-splash-screen: @@ -592,6 +600,7 @@ SPEC CHECKSUMS: react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d react-native-image-resizer: 794abf75ec13ed1f0dbb1f134e27504ea65e9e66 react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 + react-native-paste-input: 5182843692fd2ec72be50f241a38a49796e225d7 react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f @@ -619,6 +628,7 @@ SPEC CHECKSUMS: RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d RNSVG: 6adc5c52d2488a476248413064b7f2832e639057 + Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 Yoga: c618b544ff8bd8865cdca602f00cbcdb92fd6d31 diff --git a/package.json b/package.json index fd2f4b98..b0bfdddd 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-native-fontawesome": "^0.3.0", "@gorhom/bottom-sheet": "^4", + "@mattermost/react-native-paste-input": "^0.6.0", "@react-native-async-storage/async-storage": "^1.17.6", "@react-native-camera-roll/camera-roll": "^5.1.0", "@react-native-clipboard/clipboard": "^1.10.0", @@ -115,7 +116,9 @@ "transformIgnorePatterns": [ "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|rollbar-react-native|@fortawesome|@react-native|@react-navigation)" ], - "modulePathIgnorePatterns": ["__tests__\/.*\/__mocks__"], + "modulePathIgnorePatterns": [ + "__tests__/.*/__mocks__" + ], "coveragePathIgnorePatterns": [ "/node_modules/", "/src/platform", @@ -124,7 +127,10 @@ "/src/state/lib", "/__tests__/test-utils.js" ], - "reporters": [ "default", "jest-junit" ] + "reporters": [ + "default", + "jest-junit" + ] }, "browserslist": { "production": [ diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx index a8def640..3614267b 100644 --- a/src/view/com/composer/ComposePost.tsx +++ b/src/view/com/composer/ComposePost.tsx @@ -7,11 +7,14 @@ import { SafeAreaView, ScrollView, StyleSheet, - TextInput, TouchableOpacity, TouchableWithoutFeedback, View, } from 'react-native' +import PasteInput, { + PastedFile, + PasteInputRef, +} from '@mattermost/react-native-paste-input' import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' @@ -33,7 +36,7 @@ import {detectLinkables, extractEntities} from '../../../lib/strings' import {getLinkMeta} from '../../../lib/link-meta' import {downloadAndResize} from '../../../lib/images' import {UserLocalPhotosModel} from '../../../state/models/user-local-photos' -import {PhotoCarouselPicker} from './PhotoCarouselPicker' +import {PhotoCarouselPicker, cropPhoto} from './PhotoCarouselPicker' import {SelectedPhoto} from './SelectedPhoto' import {usePalette} from '../../lib/hooks/usePalette' @@ -54,7 +57,7 @@ export const ComposePost = observer(function ComposePost({ }) { const pal = usePalette('default') const store = useStores() - const textInput = useRef(null) + const textInput = useRef(null) const [isProcessing, setIsProcessing] = useState(false) const [processingState, setProcessingState] = useState('') const [error, setError] = useState('') @@ -78,6 +81,16 @@ export const ComposePost = observer(function ComposePost({ [store], ) + // HACK + // there's a bug with @mattermost/react-native-paste-input where if the input + // is focused during unmount, an exception will throw (seems that a blur method isnt implemented) + // manually blurring before closing gets around that + // -prf + const hackfixOnClose = () => { + textInput.current?.blur() + onClose() + } + // initial setup useEffect(() => { autocompleteView.setup() @@ -134,7 +147,8 @@ export const ComposePost = observer(function ComposePost({ isLoading: false, // done }) } - }, [extLink]) + return cleanup + }, [store, extLink]) useEffect(() => { // HACK @@ -196,9 +210,21 @@ export const ComposePost = observer(function ComposePost({ } } } - const onPressCancel = () => { - onClose() + const onPaste = async (err: string | undefined, files: PastedFile[]) => { + if (err) { + return setError(err) + } + if (selectedPhotos.length >= 4) { + return + } + const imgFile = files.find(file => /\.(jpe?g|png)$/.test(file.fileName)) + if (!imgFile) { + return + } + const finalImgPath = await cropPhoto(imgFile.uri) + onSelectPhotos([...selectedPhotos, finalImgPath]) } + const onPressCancel = () => hackfixOnClose() const onPressPublish = async () => { if (isProcessing) { return @@ -229,7 +255,7 @@ export const ComposePost = observer(function ComposePost({ } store.me.mainFeed.loadLatest() onPost?.() - onClose() + hackfixOnClose() Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) } const onSelectAutocompleteItem = (item: string) => { @@ -254,7 +280,11 @@ export const ComposePost = observer(function ComposePost({ let i = 0 return detectLinkables(text).map(v => { if (typeof v === 'string') { - return v + return ( + + {v} + + ) } else { return ( @@ -263,7 +293,7 @@ export const ComposePost = observer(function ComposePost({ ) } }) - }, [text]) + }, [text, pal.link]) return ( - onChangeText(text)} + onPaste={onPaste} placeholder={selectTextInputPlaceholder} placeholderTextColor={pal.colors.textLight} style={[ @@ -368,7 +399,7 @@ export const ComposePost = observer(function ComposePost({ styles.textInputFormatting, ]}> {textDecorated} - + { try { - // choose target dimensions based on the original - // this causes the photo cropper to start with the full image "selected" - const {width, height} = scaleDownDimensions( - {width: item.node.image.width, height: item.node.image.height}, - {width: MAX_WIDTH, height: MAX_HEIGHT}, + const imgPath = await cropPhoto( + item.node.image.uri, + item.node.image.width, + item.node.image.height, ) - const cropperRes = await openCropper({ - mediaType: 'photo', - path: item.node.image.uri, - ...IMAGE_PARAMS, - width, - height, - }) - const img = await compressIfNeeded(cropperRes, MAX_SIZE) - onSelectPhotos([...selectedPhotos, img.path]) + onSelectPhotos([...selectedPhotos, imgPath]) } catch (err: any) { // ignore store.log.warn('Error selecting photo', err) diff --git a/yarn.lock b/yarn.lock index f2c983d3..69897fc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1907,6 +1907,13 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@mattermost/react-native-paste-input@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@mattermost/react-native-paste-input/-/react-native-paste-input-0.6.0.tgz#bc920c8f1b442266a6bc58f9122df137b73bc9fa" + integrity sha512-Hy4w8RaiiXl2AKcLXT0FjJJsh4FXtLiWCxfh6zaBtCkx7jsr4d9xwJ/zqrnjv0jkG7XbRUCp40dgNBpWYZ1pyQ== + dependencies: + semver "7.3.8" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -11039,6 +11046,13 @@ selfsigned@^2.1.1: dependencies: node-forge "^1" +semver@7.3.8, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -11049,13 +11063,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"