diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index bf04643879..0b1efdedff 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -35,6 +35,7 @@ import QuoteEmbed from '../util/post-embeds/QuoteEmbed' import {useExternalLinkFetch} from './useExternalLinkFetch' import {isDesktopWeb} from 'platform/detection' +const MAX_IMAGES = 4 const MAX_GRAPHEME_LENGTH = 300 export const ComposePost = observer(function ComposePost({ @@ -127,12 +128,12 @@ export const ComposePost = observer(function ComposePost({ const onPhotoPasted = React.useCallback( async (uri: string) => { - if (selectedPhotos.length >= 4) { + if (selectedPhotos.length >= MAX_IMAGES) { return } setSelectedPhotos(sp => { - if (sp.length >= 4) { + if (sp.length >= MAX_IMAGES) { return sp } @@ -290,6 +291,7 @@ export const ComposePost = observer(function ComposePost({ placeholder={selectTextInputPlaceholder} suggestedLinks={suggestedLinks} autocompleteView={autocompleteView} + numPasteableImages={MAX_IMAGES - selectedPhotos.length} setRichText={setRichText} onPhotoPasted={onPhotoPasted} onSuggestedLinksChanged={setSuggestedLinks} @@ -332,12 +334,12 @@ export const ComposePost = observer(function ComposePost({ ) : null} diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 393d168fe6..ad86a532e5 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -37,6 +37,7 @@ interface TextInputProps { placeholder: string suggestedLinks: Set autocompleteView: UserAutocompleteViewModel + numPasteableImages: number setRichText: (v: RichText) => void onPhotoPasted: (uri: string) => void onSuggestedLinksChanged: (uris: Set) => void @@ -55,6 +56,7 @@ export const TextInput = React.forwardRef( placeholder, suggestedLinks, autocompleteView, + numPasteableImages, setRichText, onPhotoPasted, onSuggestedLinksChanged, @@ -129,6 +131,9 @@ export const TextInput = React.forwardRef( if (err) { return onError(cleanError(err)) } + if (numPasteableImages <= 0) { + return + } const uris = files.map(f => f.uri) const imgUri = uris.find(uri => /\.(jpe?g|png)$/.test(uri)) if (imgUri) { @@ -148,7 +153,7 @@ export const TextInput = React.forwardRef( onPhotoPasted(finalImgPath) } }, - [store, onError, onPhotoPasted], + [store, onError, onPhotoPasted, numPasteableImages], ) const onSelectionChange = React.useCallback( diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 80ff5849fe..b5d0fbec74 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -11,7 +11,6 @@ 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 { @@ -31,6 +30,7 @@ interface TextInputProps { placeholder: string suggestedLinks: Set autocompleteView: UserAutocompleteViewModel + numPasteableImages: number setRichText: (v: RichText) => void onPhotoPasted: (uri: string) => void onSuggestedLinksChanged: (uris: Set) => void @@ -44,6 +44,7 @@ export const TextInput = React.forwardRef( placeholder, suggestedLinks, autocompleteView, + numPasteableImages, setRichText, onPhotoPasted, onSuggestedLinksChanged, @@ -52,79 +53,91 @@ export const TextInput = React.forwardRef( ref, ) => { const store = useStores() - const processClipboardItemAsPhoto = async (item: DataTransferItem) => { - const file = item.getAsFile() + const editorContent = React.useRef({}) // track the editor content in a ref to avoid triggering renders + const processClipboardItemAsPhoto = React.useCallback( + async (item: DataTransferItem) => { + const file = item.getAsFile() + if (numPasteableImages <= 0) { + return + } - 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, - 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) + 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) } }, - }) + [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, () => ({ focus: () => {}, // TODO