Limit number of photos to paste in web composer
This commit is contained in:
@@ -35,6 +35,7 @@ import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
|
|||||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
|
||||||
|
const MAX_IMAGES = 4
|
||||||
const MAX_GRAPHEME_LENGTH = 300
|
const MAX_GRAPHEME_LENGTH = 300
|
||||||
|
|
||||||
export const ComposePost = observer(function ComposePost({
|
export const ComposePost = observer(function ComposePost({
|
||||||
@@ -127,12 +128,12 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
|
|
||||||
const onPhotoPasted = React.useCallback(
|
const onPhotoPasted = React.useCallback(
|
||||||
async (uri: string) => {
|
async (uri: string) => {
|
||||||
if (selectedPhotos.length >= 4) {
|
if (selectedPhotos.length >= MAX_IMAGES) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedPhotos(sp => {
|
setSelectedPhotos(sp => {
|
||||||
if (sp.length >= 4) {
|
if (sp.length >= MAX_IMAGES) {
|
||||||
return sp
|
return sp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,6 +291,7 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
placeholder={selectTextInputPlaceholder}
|
placeholder={selectTextInputPlaceholder}
|
||||||
suggestedLinks={suggestedLinks}
|
suggestedLinks={suggestedLinks}
|
||||||
autocompleteView={autocompleteView}
|
autocompleteView={autocompleteView}
|
||||||
|
numPasteableImages={MAX_IMAGES - selectedPhotos.length}
|
||||||
setRichText={setRichText}
|
setRichText={setRichText}
|
||||||
onPhotoPasted={onPhotoPasted}
|
onPhotoPasted={onPhotoPasted}
|
||||||
onSuggestedLinksChanged={setSuggestedLinks}
|
onSuggestedLinksChanged={setSuggestedLinks}
|
||||||
@@ -332,12 +334,12 @@ export const ComposePost = observer(function ComposePost({
|
|||||||
) : null}
|
) : null}
|
||||||
<View style={[pal.border, styles.bottomBar]}>
|
<View style={[pal.border, styles.bottomBar]}>
|
||||||
<SelectPhotoBtn
|
<SelectPhotoBtn
|
||||||
enabled={selectedPhotos.length < 4}
|
enabled={selectedPhotos.length < MAX_IMAGES}
|
||||||
selectedPhotos={selectedPhotos}
|
selectedPhotos={selectedPhotos}
|
||||||
onSelectPhotos={setSelectedPhotos}
|
onSelectPhotos={setSelectedPhotos}
|
||||||
/>
|
/>
|
||||||
<OpenCameraBtn
|
<OpenCameraBtn
|
||||||
enabled={selectedPhotos.length < 4}
|
enabled={selectedPhotos.length < MAX_IMAGES}
|
||||||
selectedPhotos={selectedPhotos}
|
selectedPhotos={selectedPhotos}
|
||||||
onSelectPhotos={setSelectedPhotos}
|
onSelectPhotos={setSelectedPhotos}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ interface TextInputProps {
|
|||||||
placeholder: string
|
placeholder: string
|
||||||
suggestedLinks: Set<string>
|
suggestedLinks: Set<string>
|
||||||
autocompleteView: UserAutocompleteViewModel
|
autocompleteView: UserAutocompleteViewModel
|
||||||
|
numPasteableImages: number
|
||||||
setRichText: (v: RichText) => void
|
setRichText: (v: RichText) => void
|
||||||
onPhotoPasted: (uri: string) => void
|
onPhotoPasted: (uri: string) => void
|
||||||
onSuggestedLinksChanged: (uris: Set<string>) => void
|
onSuggestedLinksChanged: (uris: Set<string>) => void
|
||||||
@@ -55,6 +56,7 @@ export const TextInput = React.forwardRef(
|
|||||||
placeholder,
|
placeholder,
|
||||||
suggestedLinks,
|
suggestedLinks,
|
||||||
autocompleteView,
|
autocompleteView,
|
||||||
|
numPasteableImages,
|
||||||
setRichText,
|
setRichText,
|
||||||
onPhotoPasted,
|
onPhotoPasted,
|
||||||
onSuggestedLinksChanged,
|
onSuggestedLinksChanged,
|
||||||
@@ -129,6 +131,9 @@ export const TextInput = React.forwardRef(
|
|||||||
if (err) {
|
if (err) {
|
||||||
return onError(cleanError(err))
|
return onError(cleanError(err))
|
||||||
}
|
}
|
||||||
|
if (numPasteableImages <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const uris = files.map(f => f.uri)
|
const uris = files.map(f => f.uri)
|
||||||
const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri))
|
const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri))
|
||||||
if (imgUri) {
|
if (imgUri) {
|
||||||
@@ -148,7 +153,7 @@ export const TextInput = React.forwardRef(
|
|||||||
onPhotoPasted(finalImgPath)
|
onPhotoPasted(finalImgPath)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[store, onError, onPhotoPasted],
|
[store, onError, onPhotoPasted, numPasteableImages],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSelectionChange = React.useCallback(
|
const onSelectionChange = React.useCallback(
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {Text} from '@tiptap/extension-text'
|
|||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
|
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
|
||||||
import {createSuggestion} from './web/Autocomplete'
|
import {createSuggestion} from './web/Autocomplete'
|
||||||
import {Transaction} from '@tiptap/pm/state'
|
|
||||||
import {cropAndCompressFlow} from 'lib/media/picker'
|
import {cropAndCompressFlow} from 'lib/media/picker'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +30,7 @@ interface TextInputProps {
|
|||||||
placeholder: string
|
placeholder: string
|
||||||
suggestedLinks: Set<string>
|
suggestedLinks: Set<string>
|
||||||
autocompleteView: UserAutocompleteViewModel
|
autocompleteView: UserAutocompleteViewModel
|
||||||
|
numPasteableImages: number
|
||||||
setRichText: (v: RichText) => void
|
setRichText: (v: RichText) => void
|
||||||
onPhotoPasted: (uri: string) => void
|
onPhotoPasted: (uri: string) => void
|
||||||
onSuggestedLinksChanged: (uris: Set<string>) => void
|
onSuggestedLinksChanged: (uris: Set<string>) => void
|
||||||
@@ -44,6 +44,7 @@ export const TextInput = React.forwardRef(
|
|||||||
placeholder,
|
placeholder,
|
||||||
suggestedLinks,
|
suggestedLinks,
|
||||||
autocompleteView,
|
autocompleteView,
|
||||||
|
numPasteableImages,
|
||||||
setRichText,
|
setRichText,
|
||||||
onPhotoPasted,
|
onPhotoPasted,
|
||||||
onSuggestedLinksChanged,
|
onSuggestedLinksChanged,
|
||||||
@@ -52,79 +53,91 @@ export const TextInput = React.forwardRef(
|
|||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const processClipboardItemAsPhoto = async (item: DataTransferItem) => {
|
const editorContent = React.useRef({}) // track the editor content in a ref to avoid triggering renders
|
||||||
const file = item.getAsFile()
|
const processClipboardItemAsPhoto = React.useCallback(
|
||||||
|
async (item: DataTransferItem) => {
|
||||||
|
const file = item.getAsFile()
|
||||||
|
if (numPasteableImages <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (file && file.type && file.type.startsWith('image/')) {
|
if (file && file.type && file.type.startsWith('image/')) {
|
||||||
const {uri, width, height} = await getImageInfoFromFile(file)
|
const {uri, width, height} = await getImageInfoFromFile(file)
|
||||||
const croppedUri = await cropAndCompressFlow(
|
const croppedUri = await cropAndCompressFlow(
|
||||||
store,
|
store,
|
||||||
uri,
|
uri,
|
||||||
{width, height},
|
{width, height},
|
||||||
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
{width: POST_IMG_MAX_WIDTH, height: POST_IMG_MAX_HEIGHT},
|
||||||
POST_IMG_MAX_SIZE,
|
POST_IMG_MAX_SIZE,
|
||||||
)
|
)
|
||||||
onPhotoPasted(croppedUri)
|
onPhotoPasted(croppedUri)
|
||||||
}
|
|
||||||
}
|
|
||||||
const editor = useEditor({
|
|
||||||
extensions: [
|
|
||||||
Document,
|
|
||||||
Link.configure({
|
|
||||||
protocols: ['http', 'https'],
|
|
||||||
autolink: true,
|
|
||||||
}),
|
|
||||||
Mention.configure({
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: 'mention',
|
|
||||||
},
|
|
||||||
suggestion: createSuggestion({autocompleteView}),
|
|
||||||
}),
|
|
||||||
Paragraph,
|
|
||||||
Placeholder.configure({
|
|
||||||
placeholder,
|
|
||||||
}),
|
|
||||||
Text,
|
|
||||||
],
|
|
||||||
content: richtext.text.toString(),
|
|
||||||
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()
|
|
||||||
|
|
||||||
const newRt = new RichText({text: editorJsonToText(json).trim()})
|
|
||||||
setRichText(newRt)
|
|
||||||
|
|
||||||
const newSuggestedLinks = new Set(editorJsonToLinks(json))
|
|
||||||
if (!isEqual(newSuggestedLinks, suggestedLinks)) {
|
|
||||||
onSuggestedLinksChanged(newSuggestedLinks)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
[store, onPhotoPasted, numPasteableImages],
|
||||||
|
)
|
||||||
|
const editor = useEditor(
|
||||||
|
{
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Link.configure({
|
||||||
|
protocols: ['http', 'https'],
|
||||||
|
autolink: true,
|
||||||
|
}),
|
||||||
|
Mention.configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'mention',
|
||||||
|
},
|
||||||
|
suggestion: createSuggestion({autocompleteView}),
|
||||||
|
}),
|
||||||
|
Paragraph,
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder,
|
||||||
|
}),
|
||||||
|
Text,
|
||||||
|
],
|
||||||
|
// use editorContent when possible to preserve state across re-renders (see useEditor deps below)
|
||||||
|
content: editorContent.current || richtext.text.toString(),
|
||||||
|
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).slice(0, numPasteableImages)) {
|
||||||
|
processClipboardItemAsPhoto(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let tiptap know that we handled the paste event
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onUpdate({editor: editorProp}) {
|
||||||
|
const json = editorProp.getJSON()
|
||||||
|
editorContent.current = json
|
||||||
|
|
||||||
|
const newRt = new RichText({text: editorJsonToText(json).trim()})
|
||||||
|
setRichText(newRt)
|
||||||
|
|
||||||
|
const newSuggestedLinks = new Set(editorJsonToLinks(json))
|
||||||
|
if (!isEqual(newSuggestedLinks, suggestedLinks)) {
|
||||||
|
onSuggestedLinksChanged(newSuggestedLinks)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[numPasteableImages],
|
||||||
|
)
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
focus: () => {}, // TODO
|
focus: () => {}, // TODO
|
||||||
|
|||||||
Reference in New Issue
Block a user