Split image cropping into secondary step (#473)

* Split image cropping into secondary step

* Use ImageModel and GalleryModel

* Add fix for pasting image URLs

* Move models to state folder

* Fix things that broke after rebase

* Latest -- has image display bug

* Remove contentFit

* Fix iOS display in gallery

* Tuneup the api signatures and implement compress/resize on web

* Fix await

* Lint fix and remove unused function

* Fix android image pathing

* Fix external embed x button on android

* Remove min-height from composer (no longer useful and was mispositioning the composer on android)

* Fix e2e picker

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Ollie Hsieh 2023-04-17 15:41:44 -07:00 committed by GitHub
parent 91fadadb58
commit 2509290fdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 875 additions and 833 deletions

View file

@ -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}),

View file

@ -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 || {}

View file

@ -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)))
}
}

View file

@ -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)
}
}
}

View file

@ -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 {