diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts index 7f70a1bd4..ed925914f 100644 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -4,6 +4,7 @@ import { createAction } from '@reduxjs/toolkit'; import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { apiUpdateMedia } from 'mastodon/api/compose'; +import { apiGetSearch } from 'mastodon/api/search'; import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; import type { MediaAttachment } from 'mastodon/models/media_attachment'; import { @@ -16,6 +17,7 @@ import type { Status } from '../models/status'; import { showAlert } from './alerts'; import { focusCompose } from './compose'; +import { importFetchedStatuses } from './importer'; import { openModal } from './modal'; const messages = defineMessages({ @@ -165,6 +167,41 @@ export const quoteComposeById = createAppThunk( }, ); +export const pasteLinkCompose = createDataLoadingThunk( + 'compose/pasteLink', + async ({ url }: { url: string }) => { + return await apiGetSearch({ + q: url, + type: 'statuses', + resolve: true, + limit: 2, + }); + }, + (data, { dispatch, getState }) => { + const composeState = getState().compose; + + if ( + composeState.get('quoted_status_id') || + composeState.get('is_submitting') || + composeState.get('poll') || + composeState.get('is_uploading') + ) + return; + + dispatch(importFetchedStatuses(data.statuses)); + + if ( + data.statuses.length === 1 && + data.statuses[0] && + ['automatic', 'manual'].includes( + data.statuses[0].quote_approval?.current_user ?? 'denied', + ) + ) { + dispatch(quoteComposeById(data.statuses[0].id)); + } + }, +); + export const quoteComposeCancel = createAction('compose/quoteComposeCancel'); export const setComposeQuotePolicy = createAction( diff --git a/app/javascript/mastodon/components/autosuggest_textarea.jsx b/app/javascript/mastodon/components/autosuggest_textarea.jsx index de5accc4b..68cf9e17f 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.jsx +++ b/app/javascript/mastodon/components/autosuggest_textarea.jsx @@ -150,10 +150,7 @@ const AutosuggestTextarea = forwardRef(({ }, [suggestions, onSuggestionSelected, textareaRef]); const handlePaste = useCallback((e) => { - if (e.clipboardData && e.clipboardData.files.length === 1) { - onPaste(e.clipboardData.files); - e.preventDefault(); - } + onPaste(e); }, [onPaste]); // Show the suggestions again whenever they change and the textarea is focused diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 5f86426c4..3dad46bc5 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -10,10 +10,13 @@ import { insertEmojiCompose, uploadCompose, } from 'mastodon/actions/compose'; +import { pasteLinkCompose } from 'mastodon/actions/compose_typed'; import { openModal } from 'mastodon/actions/modal'; import ComposeForm from '../components/compose_form'; +const urlLikeRegex = /^https?:\/\/[^\s]+\/[^\s]+$/i; + const mapStateToProps = state => ({ text: state.getIn(['compose', 'text']), suggestions: state.getIn(['compose', 'suggestions']), @@ -71,8 +74,21 @@ const mapDispatchToProps = (dispatch, props) => ({ dispatch(changeComposeSpoilerText(checked)); }, - onPaste (files) { - dispatch(uploadCompose(files)); + onPaste (e) { + if (e.clipboardData && e.clipboardData.files.length === 1) { + dispatch(uploadCompose(e.clipboardData.files)); + e.preventDefault(); + } else if (e.clipboardData && e.clipboardData.files.length === 0) { + const data = e.clipboardData.getData('text/plain'); + if (!data.match(urlLikeRegex)) return; + + try { + const url = new URL(data); + dispatch(pasteLinkCompose({ url })); + } catch { + return; + } + } }, onPickEmoji (position, data, needsSpace) {