about summary refs log tree commit diff
path: root/src/state/models
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models')
-rw-r--r--src/state/models/cache/image-sizes.ts18
-rw-r--r--src/state/models/content/profile.ts6
-rw-r--r--src/state/models/media/gallery.ts85
-rw-r--r--src/state/models/media/image.ts85
-rw-r--r--src/state/models/ui/shell.ts4
5 files changed, 184 insertions, 14 deletions
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts
index 2fd6e0013..bbfb9612b 100644
--- a/src/state/models/cache/image-sizes.ts
+++ b/src/state/models/cache/image-sizes.ts
@@ -1,24 +1,24 @@
 import {Image} from 'react-native'
-import {Dim} from 'lib/media/manip'
+import type {Dimensions} from 'lib/media/types'
 
 export class ImageSizesCache {
-  sizes: Map<string, Dim> = new Map()
-  activeRequests: Map<string, Promise<Dim>> = new Map()
+  sizes: Map<string, Dimensions> = new Map()
+  activeRequests: Map<string, Promise<Dimensions>> = new Map()
 
   constructor() {}
 
-  get(uri: string): Dim | undefined {
+  get(uri: string): Dimensions | undefined {
     return this.sizes.get(uri)
   }
 
-  async fetch(uri: string): Promise<Dim> {
-    const dim = this.sizes.get(uri)
-    if (dim) {
-      return dim
+  async fetch(uri: string): Promise<Dimensions> {
+    const Dimensions = this.sizes.get(uri)
+    if (Dimensions) {
+      return Dimensions
     }
     const prom =
       this.activeRequests.get(uri) ||
-      new Promise<Dim>(resolve => {
+      new Promise<Dimensions>(resolve => {
         Image.getSize(
           uri,
           (width: number, height: number) => resolve({width, height}),
diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts
index 45d928c92..c26dc8749 100644
--- a/src/state/models/content/profile.ts
+++ b/src/state/models/content/profile.ts
@@ -1,5 +1,4 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {PickedMedia} from 'lib/media/picker'
 import {
   ComAtprotoLabelDefs,
   AppBskyActorGetProfile as GetProfile,
@@ -10,6 +9,7 @@ import {RootStoreModel} from '../root-store'
 import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
 import {FollowState} from '../cache/my-follows'
+import {Image as RNImage} from 'react-native-image-crop-picker'
 
 export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
 
@@ -122,8 +122,8 @@ export class ProfileModel {
 
   async updateProfile(
     updates: AppBskyActorProfile.Record,
-    newUserAvatar: PickedMedia | undefined | null,
-    newUserBanner: PickedMedia | undefined | null,
+    newUserAvatar: RNImage | undefined | null,
+    newUserBanner: RNImage | undefined | null,
   ) {
     await this.rootStore.agent.upsertProfile(async existing => {
       existing = existing || {}
diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts
new file mode 100644
index 000000000..fbe6c92a0
--- /dev/null
+++ b/src/state/models/media/gallery.ts
@@ -0,0 +1,85 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {RootStoreModel} from 'state/index'
+import {ImageModel} from './image'
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import {openPicker} from 'lib/media/picker'
+import {getImageDim} from 'lib/media/manip'
+import {getDataUriSize} from 'lib/media/util'
+
+export class GalleryModel {
+  images: ImageModel[] = []
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(this, {
+      rootStore: false,
+    })
+  }
+
+  get isEmpty() {
+    return this.size === 0
+  }
+
+  get size() {
+    return this.images.length
+  }
+
+  get paths() {
+    return this.images.map(image =>
+      image.compressed === undefined ? image.path : image.compressed.path,
+    )
+  }
+
+  async add(image_: RNImage) {
+    if (this.size >= 4) {
+      return
+    }
+
+    // Temporarily enforce uniqueness but can eventually also use index
+    if (!this.images.some(i => i.path === image_.path)) {
+      const image = new ImageModel(this.rootStore, image_)
+      await image.compress()
+
+      runInAction(() => {
+        this.images.push(image)
+      })
+    }
+  }
+
+  async paste(uri: string) {
+    if (this.size >= 4) {
+      return
+    }
+
+    const {width, height} = await getImageDim(uri)
+
+    const image: RNImage = {
+      path: uri,
+      height,
+      width,
+      size: getDataUriSize(uri),
+      mime: 'image/jpeg',
+    }
+
+    runInAction(() => {
+      this.add(image)
+    })
+  }
+
+  crop(image: ImageModel) {
+    image.crop()
+  }
+
+  remove(image: ImageModel) {
+    const index = this.images.findIndex(image_ => image_.path === image.path)
+    this.images.splice(index, 1)
+  }
+
+  async pick() {
+    const images = await openPicker(this.rootStore, {
+      multiple: true,
+      maxFiles: 4 - this.images.length,
+    })
+
+    await Promise.all(images.map(image => this.add(image)))
+  }
+}
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts
new file mode 100644
index 000000000..584bf90cc
--- /dev/null
+++ b/src/state/models/media/image.ts
@@ -0,0 +1,85 @@
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import {RootStoreModel} from 'state/index'
+import {compressAndResizeImageForPost} from 'lib/media/manip'
+import {makeAutoObservable, runInAction} from 'mobx'
+import {openCropper} from 'lib/media/picker'
+import {POST_IMG_MAX} from 'lib/constants'
+import {scaleDownDimensions} from 'lib/media/util'
+
+// TODO: EXIF embed
+// Cases to consider: ExternalEmbed
+export class ImageModel implements RNImage {
+  path: string
+  mime = 'image/jpeg'
+  width: number
+  height: number
+  size: number
+  cropped?: RNImage = undefined
+  compressed?: RNImage = undefined
+  scaledWidth: number = POST_IMG_MAX.width
+  scaledHeight: number = POST_IMG_MAX.height
+
+  constructor(public rootStore: RootStoreModel, image: RNImage) {
+    makeAutoObservable(this, {
+      rootStore: false,
+    })
+
+    this.path = image.path
+    this.width = image.width
+    this.height = image.height
+    this.size = image.size
+    this.calcScaledDimensions()
+  }
+
+  calcScaledDimensions() {
+    const {width, height} = scaleDownDimensions(
+      {width: this.width, height: this.height},
+      POST_IMG_MAX,
+    )
+
+    this.scaledWidth = width
+    this.scaledHeight = height
+  }
+
+  async crop() {
+    try {
+      const cropped = await openCropper(this.rootStore, {
+        mediaType: 'photo',
+        path: this.path,
+        freeStyleCropEnabled: true,
+        width: this.scaledWidth,
+        height: this.scaledHeight,
+      })
+
+      runInAction(() => {
+        this.cropped = cropped
+      })
+    } catch (err) {
+      this.rootStore.log.error('Failed to crop photo', err)
+    }
+
+    this.compress()
+  }
+
+  async compress() {
+    try {
+      const {width, height} = scaleDownDimensions(
+        this.cropped
+          ? {width: this.cropped.width, height: this.cropped.height}
+          : {width: this.width, height: this.height},
+        POST_IMG_MAX,
+      )
+      const compressed = await compressAndResizeImageForPost({
+        ...(this.cropped === undefined ? this : this.cropped),
+        width,
+        height,
+      })
+
+      runInAction(() => {
+        this.compressed = compressed
+      })
+    } catch (err) {
+      this.rootStore.log.error('Failed to compress photo', err)
+    }
+  }
+}
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index dd5c899b3..47cc0aa82 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -3,7 +3,7 @@ import {RootStoreModel} from '../root-store'
 import {makeAutoObservable} from 'mobx'
 import {ProfileModel} from '../content/profile'
 import {isObj, hasProp} from 'lib/type-guards'
-import {PickedMedia} from 'lib/media/types'
+import {Image} from 'lib/media/types'
 
 export interface ConfirmModal {
   name: 'confirm'
@@ -38,7 +38,7 @@ export interface ReportAccountModal {
 export interface CropImageModal {
   name: 'crop-image'
   uri: string
-  onSelect: (img?: PickedMedia) => void
+  onSelect: (img?: Image) => void
 }
 
 export interface DeleteAccountModal {