Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add the ability to paste images into the composer (#56)

authored by

Paul Frazee and committed by
GitHub
570b76a7 15589d21

+101 -34
+10
ios/Podfile.lock
··· 244 244 - React-Core 245 245 - react-native-pager-view (6.1.2): 246 246 - React-Core 247 + - react-native-paste-input (0.6.0): 248 + - React-Core 249 + - Swime (= 3.0.6) 247 250 - react-native-safe-area-context (4.4.1): 248 251 - RCT-Folly 249 252 - RCTRequired ··· 390 393 - React-RCTImage 391 394 - RNSVG (12.5.0): 392 395 - React-Core 396 + - Swime (3.0.6) 393 397 - TOCropViewController (2.6.1) 394 398 - Yoga (1.14.0) 395 399 ··· 421 425 - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" 422 426 - "react-native-image-resizer (from `../node_modules/@bam.tech/react-native-image-resizer`)" 423 427 - react-native-pager-view (from `../node_modules/react-native-pager-view`) 428 + - "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)" 424 429 - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) 425 430 - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) 426 431 - react-native-version-number (from `../node_modules/react-native-version-number`) ··· 454 459 trunk: 455 460 - fmt 456 461 - libevent 462 + - Swime 457 463 - TOCropViewController 458 464 459 465 EXTERNAL SOURCES: ··· 507 513 :path: "../node_modules/@bam.tech/react-native-image-resizer" 508 514 react-native-pager-view: 509 515 :path: "../node_modules/react-native-pager-view" 516 + react-native-paste-input: 517 + :path: "../node_modules/@mattermost/react-native-paste-input" 510 518 react-native-safe-area-context: 511 519 :path: "../node_modules/react-native-safe-area-context" 512 520 react-native-splash-screen: ··· 592 600 react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d 593 601 react-native-image-resizer: 794abf75ec13ed1f0dbb1f134e27504ea65e9e66 594 602 react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 603 + react-native-paste-input: 5182843692fd2ec72be50f241a38a49796e225d7 595 604 react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a 596 605 react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 597 606 react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f ··· 619 628 RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e 620 629 RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d 621 630 RNSVG: 6adc5c52d2488a476248413064b7f2832e639057 631 + Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b 622 632 TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 623 633 Yoga: c618b544ff8bd8865cdca602f00cbcdb92fd6d31 624 634
+8 -2
package.json
··· 24 24 "@fortawesome/free-solid-svg-icons": "^6.1.1", 25 25 "@fortawesome/react-native-fontawesome": "^0.3.0", 26 26 "@gorhom/bottom-sheet": "^4", 27 + "@mattermost/react-native-paste-input": "^0.6.0", 27 28 "@react-native-async-storage/async-storage": "^1.17.6", 28 29 "@react-native-camera-roll/camera-roll": "^5.1.0", 29 30 "@react-native-clipboard/clipboard": "^1.10.0", ··· 115 116 "transformIgnorePatterns": [ 116 117 "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|rollbar-react-native|@fortawesome|@react-native|@react-navigation)" 117 118 ], 118 - "modulePathIgnorePatterns": ["__tests__\/.*\/__mocks__"], 119 + "modulePathIgnorePatterns": [ 120 + "__tests__/.*/__mocks__" 121 + ], 119 122 "coveragePathIgnorePatterns": [ 120 123 "<rootDir>/node_modules/", 121 124 "<rootDir>/src/platform", ··· 124 127 "<rootDir>/src/state/lib", 125 128 "<rootDir>/__tests__/test-utils.js" 126 129 ], 127 - "reporters": [ "default", "jest-junit" ] 130 + "reporters": [ 131 + "default", 132 + "jest-junit" 133 + ] 128 134 }, 129 135 "browserslist": { 130 136 "production": [
+42 -11
src/view/com/composer/ComposePost.tsx
··· 7 7 SafeAreaView, 8 8 ScrollView, 9 9 StyleSheet, 10 - TextInput, 11 10 TouchableOpacity, 12 11 TouchableWithoutFeedback, 13 12 View, 14 13 } from 'react-native' 14 + import PasteInput, { 15 + PastedFile, 16 + PasteInputRef, 17 + } from '@mattermost/react-native-paste-input' 15 18 import LinearGradient from 'react-native-linear-gradient' 16 19 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 17 20 import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view' ··· 33 36 import {getLinkMeta} from '../../../lib/link-meta' 34 37 import {downloadAndResize} from '../../../lib/images' 35 38 import {UserLocalPhotosModel} from '../../../state/models/user-local-photos' 36 - import {PhotoCarouselPicker} from './PhotoCarouselPicker' 39 + import {PhotoCarouselPicker, cropPhoto} from './PhotoCarouselPicker' 37 40 import {SelectedPhoto} from './SelectedPhoto' 38 41 import {usePalette} from '../../lib/hooks/usePalette' 39 42 ··· 54 57 }) { 55 58 const pal = usePalette('default') 56 59 const store = useStores() 57 - const textInput = useRef<TextInput>(null) 60 + const textInput = useRef<PasteInputRef>(null) 58 61 const [isProcessing, setIsProcessing] = useState(false) 59 62 const [processingState, setProcessingState] = useState('') 60 63 const [error, setError] = useState('') ··· 78 81 [store], 79 82 ) 80 83 84 + // HACK 85 + // there's a bug with @mattermost/react-native-paste-input where if the input 86 + // is focused during unmount, an exception will throw (seems that a blur method isnt implemented) 87 + // manually blurring before closing gets around that 88 + // -prf 89 + const hackfixOnClose = () => { 90 + textInput.current?.blur() 91 + onClose() 92 + } 93 + 81 94 // initial setup 82 95 useEffect(() => { 83 96 autocompleteView.setup() ··· 134 147 isLoading: false, // done 135 148 }) 136 149 } 137 - }, [extLink]) 150 + return cleanup 151 + }, [store, extLink]) 138 152 139 153 useEffect(() => { 140 154 // HACK ··· 196 210 } 197 211 } 198 212 } 199 - const onPressCancel = () => { 200 - onClose() 213 + const onPaste = async (err: string | undefined, files: PastedFile[]) => { 214 + if (err) { 215 + return setError(err) 216 + } 217 + if (selectedPhotos.length >= 4) { 218 + return 219 + } 220 + const imgFile = files.find(file => /\.(jpe?g|png)$/.test(file.fileName)) 221 + if (!imgFile) { 222 + return 223 + } 224 + const finalImgPath = await cropPhoto(imgFile.uri) 225 + onSelectPhotos([...selectedPhotos, finalImgPath]) 201 226 } 227 + const onPressCancel = () => hackfixOnClose() 202 228 const onPressPublish = async () => { 203 229 if (isProcessing) { 204 230 return ··· 229 255 } 230 256 store.me.mainFeed.loadLatest() 231 257 onPost?.() 232 - onClose() 258 + hackfixOnClose() 233 259 Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) 234 260 } 235 261 const onSelectAutocompleteItem = (item: string) => { ··· 254 280 let i = 0 255 281 return detectLinkables(text).map(v => { 256 282 if (typeof v === 'string') { 257 - return v 283 + return ( 284 + <Text key={i++} style={styles.textInputFormatting}> 285 + {v} 286 + </Text> 287 + ) 258 288 } else { 259 289 return ( 260 290 <Text key={i++} style={[pal.link, styles.textInputFormatting]}> ··· 263 293 ) 264 294 } 265 295 }) 266 - }, [text]) 296 + }, [text, pal.link]) 267 297 268 298 return ( 269 299 <KeyboardAvoidingView ··· 354 384 avatar={store.me.avatar} 355 385 size={50} 356 386 /> 357 - <TextInput 387 + <PasteInput 358 388 testID="composerTextInput" 359 389 ref={textInput} 360 390 multiline 361 391 scrollEnabled 362 392 onChangeText={(text: string) => onChangeText(text)} 393 + onPaste={onPaste} 363 394 placeholder={selectTextInputPlaceholder} 364 395 placeholderTextColor={pal.colors.textLight} 365 396 style={[ ··· 368 399 styles.textInputFormatting, 369 400 ]}> 370 401 {textDecorated} 371 - </TextInput> 402 + </PasteInput> 372 403 </View> 373 404 <SelectedPhoto 374 405 selectedPhotos={selectedPhotos}
+27 -14
src/view/com/composer/PhotoCarouselPicker.tsx
··· 26 26 compressImageQuality: 1.0, 27 27 } 28 28 29 + export async function cropPhoto( 30 + path: string, 31 + imgWidth = MAX_WIDTH, 32 + imgHeight = MAX_HEIGHT, 33 + ) { 34 + // choose target dimensions based on the original 35 + // this causes the photo cropper to start with the full image "selected" 36 + const {width, height} = scaleDownDimensions( 37 + {width: imgWidth, height: imgHeight}, 38 + {width: MAX_WIDTH, height: MAX_HEIGHT}, 39 + ) 40 + const cropperRes = await openCropper({ 41 + mediaType: 'photo', 42 + path, 43 + ...IMAGE_PARAMS, 44 + width, 45 + height, 46 + }) 47 + const img = await compressIfNeeded(cropperRes, MAX_SIZE) 48 + return img.path 49 + } 50 + 29 51 export const PhotoCarouselPicker = ({ 30 52 selectedPhotos, 31 53 onSelectPhotos, ··· 55 77 const handleSelectPhoto = useCallback( 56 78 async (item: PhotoIdentifier) => { 57 79 try { 58 - // choose target dimensions based on the original 59 - // this causes the photo cropper to start with the full image "selected" 60 - const {width, height} = scaleDownDimensions( 61 - {width: item.node.image.width, height: item.node.image.height}, 62 - {width: MAX_WIDTH, height: MAX_HEIGHT}, 80 + const imgPath = await cropPhoto( 81 + item.node.image.uri, 82 + item.node.image.width, 83 + item.node.image.height, 63 84 ) 64 - const cropperRes = await openCropper({ 65 - mediaType: 'photo', 66 - path: item.node.image.uri, 67 - ...IMAGE_PARAMS, 68 - width, 69 - height, 70 - }) 71 - const img = await compressIfNeeded(cropperRes, MAX_SIZE) 72 - onSelectPhotos([...selectedPhotos, img.path]) 85 + onSelectPhotos([...selectedPhotos, imgPath]) 73 86 } catch (err: any) { 74 87 // ignore 75 88 store.log.warn('Error selecting photo', err)
+14 -7
yarn.lock
··· 1907 1907 resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" 1908 1908 integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== 1909 1909 1910 + "@mattermost/react-native-paste-input@^0.6.0": 1911 + version "0.6.0" 1912 + resolved "https://registry.yarnpkg.com/@mattermost/react-native-paste-input/-/react-native-paste-input-0.6.0.tgz#bc920c8f1b442266a6bc58f9122df137b73bc9fa" 1913 + integrity sha512-Hy4w8RaiiXl2AKcLXT0FjJJsh4FXtLiWCxfh6zaBtCkx7jsr4d9xwJ/zqrnjv0jkG7XbRUCp40dgNBpWYZ1pyQ== 1914 + dependencies: 1915 + semver "7.3.8" 1916 + 1910 1917 "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": 1911 1918 version "5.1.1-v1" 1912 1919 resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" ··· 11039 11046 dependencies: 11040 11047 node-forge "^1" 11041 11048 11049 + semver@7.3.8, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: 11050 + version "7.3.8" 11051 + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" 11052 + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== 11053 + dependencies: 11054 + lru-cache "^6.0.0" 11055 + 11042 11056 semver@^5.5.0, semver@^5.6.0: 11043 11057 version "5.7.1" 11044 11058 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" ··· 11048 11062 version "6.3.0" 11049 11063 resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" 11050 11064 integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== 11051 - 11052 - semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: 11053 - version "7.3.8" 11054 - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" 11055 - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== 11056 - dependencies: 11057 - lru-cache "^6.0.0" 11058 11065 11059 11066 send@0.18.0: 11060 11067 version "0.18.0"