Adds support for pasting images on the web (#313)

This commit is contained in:
John Fawcett
2023-03-22 23:04:47 -05:00
committed by Paul Frazee
parent 9652d994dd
commit 0d26763e11
3 changed files with 125 additions and 3 deletions
+66
View File
@@ -5,3 +5,69 @@ export function extractDataUriMime(uri: string): string {
export function getDataUriSize(uri: string): number {
return Math.round((uri.length * 3) / 4) // very rough estimate
}
// TODO: Can we consolidate this with an existing type?
interface ImageInfo {
uri: string
width: number
height: number
}
/**
* Given a File instance (like one pulled from a paste event), return
* a data URI and the image dimensions for the pasted file. Returns
* width 0, height 0 for non-images
*/
export function getImageInfoFromFile(file: File): Promise<ImageInfo> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onloadend = () => {
const maybeUri = reader.result
// reader.result can be different data types depending on what
// readAs... method is called. We're using readAsDataUrl which
// will read as a string. If someone were to change that
// accidentally or otherwise, then reader.result might not be
// a string. In which case, we'll bail out and yell at the dev.
if (typeof maybeUri === 'string') {
const uri = maybeUri
// Non-images proooobably shouldn't even be accepted in this function
// TODO: Maybe just reject for non-images?
if (!file.type.startsWith('image/')) {
return resolve({
uri,
width: 0,
height: 0,
})
}
// Get the dimensions of said file
const img = new Image()
img.src = uri
img.onload = () => {
return resolve({
uri,
width: img.width,
height: img.height,
})
}
return
}
reject(
new Error(
'File was not called with .readAsDataURL(...). ' +
'This absolutely should not happen. Make sure ' +
'the reader instance is calling .readAsDataURL() ' +
'on the file',
),
)
}
reader.onerror = reject
reader.onabort = () => reject(new Error('File read aborted'))
})
}
+10 -2
View File
@@ -130,9 +130,17 @@ export const ComposePost = observer(function ComposePost({
if (selectedPhotos.length >= 4) {
return
}
onSelectPhotos([...selectedPhotos, uri])
setSelectedPhotos(sp => {
if (sp.length >= 4) {
return sp
}
track('Composer:SelectedPhotos')
return [...sp, uri]
})
},
[selectedPhotos, onSelectPhotos],
[selectedPhotos, setSelectedPhotos, track],
)
const onPressPublish = React.useCallback(async () => {
@@ -11,6 +11,15 @@ import {Text} from '@tiptap/extension-text'
import isEqual from 'lodash.isequal'
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
import {createSuggestion} from './web/Autocomplete'
import {Transaction} from '@tiptap/pm/state'
import {cropAndCompressFlow} from 'lib/media/picker'
import {useStores} from 'state/index'
import {
POST_IMG_MAX_HEIGHT,
POST_IMG_MAX_SIZE,
POST_IMG_MAX_WIDTH,
} from 'lib/constants'
import {getImageInfoFromFile} from 'lib/media/util'
export interface TextInputRef {
focus: () => void
@@ -36,12 +45,28 @@ export const TextInput = React.forwardRef(
suggestedLinks,
autocompleteView,
setRichText,
// onPhotoPasted, TODO
onPhotoPasted,
onSuggestedLinksChanged,
}: // onError, TODO
TextInputProps,
ref,
) => {
const store = useStores()
const processClipboardItemAsPhoto = async (item: DataTransferItem) => {
const file = item.getAsFile()
if (file && file.type && file.type.startsWith('image/')) {
const {uri, width, height} = await getImageInfoFromFile(file)
const croppedUri = await cropAndCompressFlow(
store,
uri,
{width, height},
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
POST_IMG_MAX_SIZE,
)
onPhotoPasted(croppedUri)
}
}
const editor = useEditor({
extensions: [
Document,
@@ -65,6 +90,29 @@ export const TextInput = React.forwardRef(
autofocus: true,
editable: true,
injectCSS: true,
editorProps: {
handlePaste(_, event) {
// It's possible to copy a single screenshot or multiple files
// In the case of a single screenshot (e.g. cmd+shift+4), this
// list will be length 1. In the case of selecting multiple
// files in a file explorer and copying/pasting, this will be
// those list of copied files
const items = event.clipboardData?.items
// Let tiptap know to fallback to default paste behavior
if (!items || items.length === 0) {
return false
}
// For any pasted images, bring them through the crop and compress flow
for (const item of Array.from(items)) {
processClipboardItemAsPhoto(item)
}
// Let tiptap know that we handled the paste event
return true
},
},
onUpdate({editor: editorProp}) {
const json = editorProp.getJSON()