about summary refs log tree commit diff
path: root/src/view/com/composer
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-01-27 15:51:24 -0600
committerPaul Frazee <pfrazee@gmail.com>2023-01-27 15:51:24 -0600
commit7916b26aadb7e003728d9dc653ab8b8deabf4076 (patch)
tree507d24512fd71c67d4fe49af4ae5f8746444cceb /src/view/com/composer
parent0673129b2018c9db0f7c3fc3e2c3214150efcfb8 (diff)
downloadvoidsky-7916b26aadb7e003728d9dc653ab8b8deabf4076.tar.zst
Break out the web/native image picking code and make some progress on the web version
Diffstat (limited to 'src/view/com/composer')
-rw-r--r--src/view/com/composer/ComposePost.tsx27
-rw-r--r--src/view/com/composer/photos/PhotoCarouselPicker.tsx (renamed from src/view/com/composer/PhotoCarouselPicker.tsx)68
-rw-r--r--src/view/com/composer/photos/PhotoCarouselPicker.web.tsx158
3 files changed, 207 insertions, 46 deletions
diff --git a/src/view/com/composer/ComposePost.tsx b/src/view/com/composer/ComposePost.tsx
index 2f30a1cf4..1144b5e48 100644
--- a/src/view/com/composer/ComposePost.tsx
+++ b/src/view/com/composer/ComposePost.tsx
@@ -37,8 +37,7 @@ import {
 } from '../../../lib/strings'
 import {getLinkMeta} from '../../../lib/link-meta'
 import {downloadAndResize} from '../../../lib/images'
-import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
-import {PhotoCarouselPicker, cropPhoto} from './PhotoCarouselPicker'
+import {PhotoCarouselPicker, cropPhoto} from './photos/PhotoCarouselPicker'
 import {SelectedPhoto} from './SelectedPhoto'
 import {usePalette} from '../../lib/hooks/usePalette'
 
@@ -77,10 +76,6 @@ export const ComposePost = observer(function ComposePost({
     () => new UserAutocompleteViewModel(store),
     [store],
   )
-  const localPhotos = React.useMemo<UserLocalPhotosModel>(
-    () => new UserLocalPhotosModel(store),
-    [store],
-  )
 
   // HACK
   // there's a bug with @mattermost/react-native-paste-input where if the input
@@ -95,8 +90,7 @@ export const ComposePost = observer(function ComposePost({
   // initial setup
   useEffect(() => {
     autocompleteView.setup()
-    localPhotos.setup()
-  }, [autocompleteView, localPhotos])
+  }, [autocompleteView])
 
   // external link metadata-fetch flow
   useEffect(() => {
@@ -220,7 +214,7 @@ export const ComposePost = observer(function ComposePost({
     }
     const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri))
     if (imgUri) {
-      const finalImgPath = await cropPhoto(imgUri)
+      const finalImgPath = await cropPhoto(store, imgUri)
       onSelectPhotos([...selectedPhotos, finalImgPath])
     }
   }
@@ -412,15 +406,12 @@ export const ComposePost = observer(function ComposePost({
               />
             )}
           </ScrollView>
-          {isSelectingPhotos &&
-            localPhotos.photos != null &&
-            selectedPhotos.length < 4 && (
-              <PhotoCarouselPicker
-                selectedPhotos={selectedPhotos}
-                onSelectPhotos={onSelectPhotos}
-                localPhotos={localPhotos}
-              />
-            )}
+          {isSelectingPhotos && selectedPhotos.length < 4 && (
+            <PhotoCarouselPicker
+              selectedPhotos={selectedPhotos}
+              onSelectPhotos={onSelectPhotos}
+            />
+          )}
           <View style={[pal.border, styles.bottomBar]}>
             <TouchableOpacity
               testID="composerSelectPhotosButton"
diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/photos/PhotoCarouselPicker.tsx
index eb5b4dcf2..7a5c9f65d 100644
--- a/src/view/com/composer/PhotoCarouselPicker.tsx
+++ b/src/view/com/composer/photos/PhotoCarouselPicker.tsx
@@ -8,14 +8,14 @@ import {
   openPicker,
   openCamera,
   openCropper,
-} from '../util/images/ImageCropPicker'
+} from '../../util/images/image-crop-picker/ImageCropPicker'
 import {
   UserLocalPhotosModel,
   PhotoIdentifier,
-} from '../../../state/models/user-local-photos'
-import {compressIfNeeded, scaleDownDimensions} from '../../../lib/images'
-import {usePalette} from '../../lib/hooks/usePalette'
-import {useStores} from '../../../state'
+} from '../../../../state/models/user-local-photos'
+import {compressIfNeeded, scaleDownDimensions} from '../../../../lib/images'
+import {usePalette} from '../../../lib/hooks/usePalette'
+import {useStores, RootStoreModel} from '../../../../state'
 
 const MAX_WIDTH = 1000
 const MAX_HEIGHT = 1000
@@ -25,11 +25,10 @@ const IMAGE_PARAMS = {
   width: 1000,
   height: 1000,
   freeStyleCropEnabled: true,
-  forceJpg: true, // ios only
-  compressImageQuality: 1.0,
 }
 
 export async function cropPhoto(
+  store: RootStoreModel,
   path: string,
   imgWidth = MAX_WIDTH,
   imgHeight = MAX_HEIGHT,
@@ -40,10 +39,10 @@ export async function cropPhoto(
     {width: imgWidth, height: imgHeight},
     {width: MAX_WIDTH, height: MAX_HEIGHT},
   )
-  const cropperRes = await openCropper({
+  const cropperRes = await openCropper(store, {
     mediaType: 'photo',
     path,
-    ...IMAGE_PARAMS,
+    freeStyleCropEnabled: true,
     width,
     height,
   })
@@ -54,19 +53,30 @@ export async function cropPhoto(
 export const PhotoCarouselPicker = ({
   selectedPhotos,
   onSelectPhotos,
-  localPhotos,
 }: {
   selectedPhotos: string[]
   onSelectPhotos: (v: string[]) => void
-  localPhotos: UserLocalPhotosModel
 }) => {
   const pal = usePalette('default')
   const store = useStores()
+  const [localPhotos, setLocalPhotos] = React.useState<
+    UserLocalPhotosModel | undefined
+  >(undefined)
+
+  // initial setup
+  React.useEffect(() => {
+    const photos = new UserLocalPhotosModel(store)
+    photos.setup().then(() => {
+      if (photos.photos) {
+        setLocalPhotos(photos)
+      }
+    })
+  }, [store])
+
   const handleOpenCamera = useCallback(async () => {
     try {
-      const cameraRes = await openCamera({
+      const cameraRes = await openCamera(store, {
         mediaType: 'photo',
-        cropping: true,
         ...IMAGE_PARAMS,
       })
       const img = await compressIfNeeded(cameraRes, MAX_SIZE)
@@ -75,12 +85,13 @@ export const PhotoCarouselPicker = ({
       // ignore
       store.log.warn('Error using camera', err)
     }
-  }, [store.log, selectedPhotos, onSelectPhotos])
+  }, [store, selectedPhotos, onSelectPhotos])
 
   const handleSelectPhoto = useCallback(
     async (item: PhotoIdentifier) => {
       try {
         const imgPath = await cropPhoto(
+          store,
           item.node.image.uri,
           item.node.image.width,
           item.node.image.height,
@@ -91,11 +102,11 @@ export const PhotoCarouselPicker = ({
         store.log.warn('Error selecting photo', err)
       }
     },
-    [store.log, selectedPhotos, onSelectPhotos],
+    [store, selectedPhotos, onSelectPhotos],
   )
 
   const handleOpenGallery = useCallback(() => {
-    openPicker({
+    openPicker(store, {
       multiple: true,
       maxFiles: 4 - selectedPhotos.length,
       mediaType: 'photo',
@@ -109,10 +120,10 @@ export const PhotoCarouselPicker = ({
           {width: image.width, height: image.height},
           {width: MAX_WIDTH, height: MAX_HEIGHT},
         )
-        const cropperRes = await openCropper({
+        const cropperRes = await openCropper(store, {
           mediaType: 'photo',
           path: image.path,
-          ...IMAGE_PARAMS,
+          freeStyleCropEnabled: true,
           width,
           height,
         })
@@ -121,7 +132,7 @@ export const PhotoCarouselPicker = ({
       }
       onSelectPhotos([...selectedPhotos, ...result])
     })
-  }, [selectedPhotos, onSelectPhotos])
+  }, [store, selectedPhotos, onSelectPhotos])
 
   return (
     <ScrollView
@@ -150,15 +161,16 @@ export const PhotoCarouselPicker = ({
           size={24}
         />
       </TouchableOpacity>
-      {localPhotos.photos.map((item: PhotoIdentifier, index: number) => (
-        <TouchableOpacity
-          testID="openSelectPhotoButton"
-          key={`local-image-${index}`}
-          style={[pal.border, styles.photoButton]}
-          onPress={() => handleSelectPhoto(item)}>
-          <Image style={styles.photo} source={{uri: item.node.image.uri}} />
-        </TouchableOpacity>
-      ))}
+      {localPhotos != null &&
+        localPhotos.photos.map((item: PhotoIdentifier, index: number) => (
+          <TouchableOpacity
+            testID="openSelectPhotoButton"
+            key={`local-image-${index}`}
+            style={[pal.border, styles.photoButton]}
+            onPress={() => handleSelectPhoto(item)}>
+            <Image style={styles.photo} source={{uri: item.node.image.uri}} />
+          </TouchableOpacity>
+        ))}
     </ScrollView>
   )
 }
diff --git a/src/view/com/composer/photos/PhotoCarouselPicker.web.tsx b/src/view/com/composer/photos/PhotoCarouselPicker.web.tsx
new file mode 100644
index 000000000..bb2800026
--- /dev/null
+++ b/src/view/com/composer/photos/PhotoCarouselPicker.web.tsx
@@ -0,0 +1,158 @@
+import React, {useCallback} from 'react'
+import {StyleSheet, TouchableOpacity, ScrollView} from 'react-native'
+import {
+  FontAwesomeIcon,
+  FontAwesomeIconStyle,
+} from '@fortawesome/react-native-fontawesome'
+import {
+  openPicker,
+  openCamera,
+  openCropper,
+} from '../../util/images/image-crop-picker/ImageCropPicker'
+import {compressIfNeeded, scaleDownDimensions} from '../../../../lib/images'
+import {usePalette} from '../../../lib/hooks/usePalette'
+import {useStores, RootStoreModel} from '../../../../state'
+
+const MAX_WIDTH = 1000
+const MAX_HEIGHT = 1000
+const MAX_SIZE = 300000
+
+const IMAGE_PARAMS = {
+  width: 1000,
+  height: 1000,
+  freeStyleCropEnabled: true,
+}
+
+export async function cropPhoto(
+  store: RootStoreModel,
+  path: string,
+  imgWidth = MAX_WIDTH,
+  imgHeight = MAX_HEIGHT,
+) {
+  // choose target dimensions based on the original
+  // this causes the photo cropper to start with the full image "selected"
+  const {width, height} = scaleDownDimensions(
+    {width: imgWidth, height: imgHeight},
+    {width: MAX_WIDTH, height: MAX_HEIGHT},
+  )
+  const cropperRes = await openCropper(store, {
+    mediaType: 'photo',
+    path,
+    freeStyleCropEnabled: true,
+    width,
+    height,
+  })
+  const img = await compressIfNeeded(cropperRes, MAX_SIZE)
+  return img.path
+}
+
+export const PhotoCarouselPicker = ({
+  selectedPhotos,
+  onSelectPhotos,
+}: {
+  selectedPhotos: string[]
+  onSelectPhotos: (v: string[]) => void
+}) => {
+  const pal = usePalette('default')
+  const store = useStores()
+
+  const handleOpenCamera = useCallback(async () => {
+    try {
+      const cameraRes = await openCamera(store, {
+        mediaType: 'photo',
+        ...IMAGE_PARAMS,
+      })
+      const img = await compressIfNeeded(cameraRes, MAX_SIZE)
+      onSelectPhotos([...selectedPhotos, img.path])
+    } catch (err: any) {
+      // ignore
+      store.log.warn('Error using camera', err)
+    }
+  }, [store, selectedPhotos, onSelectPhotos])
+
+  const handleOpenGallery = useCallback(() => {
+    openPicker(store, {
+      multiple: true,
+      maxFiles: 4 - selectedPhotos.length,
+      mediaType: 'photo',
+    }).then(async items => {
+      const result = []
+
+      for (const image of items) {
+        // choose target dimensions based on the original
+        // this causes the photo cropper to start with the full image "selected"
+        const {width, height} = scaleDownDimensions(
+          {width: image.width, height: image.height},
+          {width: MAX_WIDTH, height: MAX_HEIGHT},
+        )
+        const cropperRes = await openCropper(store, {
+          mediaType: 'photo',
+          path: image.path,
+          freeStyleCropEnabled: true,
+          width,
+          height,
+        })
+        const finalImg = await compressIfNeeded(cropperRes, MAX_SIZE)
+        result.push(finalImg.path)
+      }
+      onSelectPhotos([...selectedPhotos, ...result])
+    })
+  }, [store, selectedPhotos, onSelectPhotos])
+
+  return (
+    <ScrollView
+      testID="photoCarouselPickerView"
+      horizontal
+      style={[pal.view, styles.photosContainer]}
+      keyboardShouldPersistTaps="always"
+      showsHorizontalScrollIndicator={false}>
+      <TouchableOpacity
+        testID="openCameraButton"
+        style={[styles.galleryButton, pal.border, styles.photo]}
+        onPress={handleOpenCamera}>
+        <FontAwesomeIcon
+          icon="camera"
+          size={24}
+          style={pal.link as FontAwesomeIconStyle}
+        />
+      </TouchableOpacity>
+      <TouchableOpacity
+        testID="openGalleryButton"
+        style={[styles.galleryButton, pal.border, styles.photo]}
+        onPress={handleOpenGallery}>
+        <FontAwesomeIcon
+          icon="image"
+          style={pal.link as FontAwesomeIconStyle}
+          size={24}
+        />
+      </TouchableOpacity>
+    </ScrollView>
+  )
+}
+
+const styles = StyleSheet.create({
+  photosContainer: {
+    width: '100%',
+    maxHeight: 96,
+    padding: 8,
+    overflow: 'hidden',
+  },
+  galleryButton: {
+    borderWidth: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  photoButton: {
+    width: 75,
+    height: 75,
+    marginRight: 8,
+    borderWidth: 1,
+    borderRadius: 16,
+  },
+  photo: {
+    width: 75,
+    height: 75,
+    marginRight: 8,
+    borderRadius: 16,
+  },
+})