diff --git a/changelog.d/mute-dropdown.fix b/changelog.d/mute-dropdown.fix new file mode 100644 index 000000000..33f12a571 --- /dev/null +++ b/changelog.d/mute-dropdown.fix @@ -0,0 +1 @@ +Fixed status action mute hiding itself on click diff --git a/changelog.d/quote-by-url.add b/changelog.d/quote-by-url.add new file mode 100644 index 000000000..ef401f93c --- /dev/null +++ b/changelog.d/quote-by-url.add @@ -0,0 +1 @@ +Add quoting by URL and in replies diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue index dfd197e56..cedbdce69 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -73,6 +73,7 @@ :disable-notice="true" :disable-lock-warning="true" :disable-polls="true" + :disable-quotes="true" :disable-sensitivity-checkbox="true" :disable-submit="errorLoadingChat || !currentChat" :disable-preview="true" diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index ffc299af6..45786614e 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -45,7 +45,12 @@ const Draft = { } }, safeToSave() { - return this.draft.status || this.draft.files?.length || this.draft.hasPoll + return ( + this.draft.status || + this.draft.files?.length || + this.draft.hasPoll || + this.draft.hasQuote + ) }, postStatusFormProps() { return { diff --git a/src/components/edit_status_form/edit_status_form.vue b/src/components/edit_status_form/edit_status_form.vue index 0a7ec760a..1452be422 100644 --- a/src/components/edit_status_form/edit_status_form.vue +++ b/src/components/edit_status_form/edit_status_form.vue @@ -4,6 +4,7 @@ v-bind="params" :post-handler="doEditStatus" :disable-polls="true" + :disable-quotes="true" :disable-visibility-selector="true" /> diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js index b387bcf4c..d77a0a839 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -24,7 +24,6 @@ const MRFTransparencyPanel = { mrfPolicies: (state) => get(state, 'federationPolicy.mrf_policies', []), quarantineInstances: (state) => - console.log(state) || toInstanceReasonObject( get(state, 'federationPolicy.quarantined_instances', []), get( diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 7ebbeeaeb..d08a8b9c2 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -15,6 +15,7 @@ import EmojiInput from '../emoji_input/emoji_input.vue' import suggestor from '../emoji_input/suggestor.js' import MediaUpload from '../media_upload/media_upload.vue' import PollForm from '../poll/poll_form.vue' +import QuoteForm from '../quote/quote_form.vue' import ScopeSelector from '../scope_selector/scope_selector.vue' import Select from '../select/select.vue' import StatusContent from '../status_content/status_content.vue' @@ -37,6 +38,7 @@ import { faChevronRight, faCircleNotch, faPollH, + faQuoteRight, faSmileBeam, faTimes, faUpload, @@ -46,6 +48,7 @@ library.add( faSmileBeam, faPollH, faUpload, + faQuoteRight, faBan, faTimes, faCircleNotch, @@ -107,6 +110,7 @@ const PostStatusForm = { 'disableNotice', 'disableLockWarning', 'disablePolls', + 'disableQuotes', 'disableSensitivityCheckbox', 'disableSubmit', 'disablePreview', @@ -138,6 +142,7 @@ const PostStatusForm = { MediaUpload, EmojiInput, PollForm, + QuoteForm, ScopeSelector, Checkbox, Select, @@ -147,6 +152,9 @@ const PostStatusForm = { DraftCloser, Popover, }, + created() { + this.initQuote() + }, mounted() { this.updateIdempotencyKey() this.resize(this.$refs.textarea) @@ -205,6 +213,8 @@ const PostStatusForm = { files: [], poll: {}, hasPoll: false, + hasQuote: false, + quote: {}, mediaDescriptions: {}, visibility: scope, contentType, @@ -222,6 +232,8 @@ const PostStatusForm = { files: this.statusFiles || [], poll: this.statusPoll || {}, hasPoll: false, + hasQuote: false, + quote: {}, mediaDescriptions: this.statusMediaDescriptions || {}, visibility: this.statusScope || scope, contentType: statusContentType, @@ -348,12 +360,28 @@ const PostStatusForm = { isEdit() { return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' }, - quotable() { + quotingAvailable() { if (!useInstanceCapabilitiesStore().quotingAvailable) { return false } - if (!this.replyTo) { + return this.disableQuotes !== true + }, + isReply() { + return this.newStatus.type === 'reply' + }, + quotable() { + return this.quotingAvailable && this.replyTo + }, + quoteThreadToggled() { + return this.newStatus.hasQuote && this.newStatus.quote.thread + }, + defaultQuotable() { + if ( + !this.quotingAvailable || + !this.isReply || + !this.$store.getters.mergedConfig.quoteReply + ) { return false } @@ -375,12 +403,25 @@ const PostStatusForm = { return false }, + inReplyStatusId() { + return !this.newStatus.hasQuote || + !this.newStatus.quote.thread || + !this.newStatus.quote.id + ? this.replyTo + : undefined + }, + quoteId() { + return this.newStatus.hasQuote ? this.newStatus.quote.id : undefined + }, debouncedMaybeAutoSaveDraft() { return debounce(this.maybeAutoSaveDraft, 3000) }, pollFormVisible() { return this.newStatus.hasPoll }, + quoteFormVisible() { + return this.newStatus.hasQuote && !this.newStatus.quote.thread + }, shouldAutoSaveDraft() { return useMergedConfigStore().mergedConfig.autoSaveDraft }, @@ -398,7 +439,8 @@ const PostStatusForm = { (this.newStatus.status || this.newStatus.spoilerText || this.newStatus.files?.length || - this.newStatus.hasPoll) && + this.newStatus.hasPoll || + this.newStatus.hasQuote) && this.saveable ) }, @@ -409,7 +451,8 @@ const PostStatusForm = { this.newStatus.status || this.newStatus.spoilerText || this.newStatus.files?.length || - this.newStatus.hasPoll + this.newStatus.hasPoll || + this.newStatus.hasQuote ) ) }, @@ -459,11 +502,13 @@ const PostStatusForm = { contentType: newStatus.contentType, poll: {}, hasPoll: false, + hasQuote: false, + quote: {}, mediaDescriptions: {}, - quoting: false, } this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile() this.clearPollForm() + this.clearQuoteForm() if (this.preserveFocus) { this.$nextTick(() => { this.$refs.textarea.focus() @@ -503,9 +548,7 @@ const PostStatusForm = { return } - const poll = this.newStatus.hasPoll - ? pollFormToMasto(this.newStatus.poll) - : {} + const poll = newStatus.hasPoll ? pollFormToMasto(newStatus.poll) : {} if (this.pollContentError) { this.error = this.pollContentError return @@ -521,10 +564,6 @@ const PostStatusForm = { return } - const replyOrQuoteAttr = newStatus.quoting - ? 'quoteId' - : 'inReplyToStatusId' - const postingOptions = { status: newStatus.status, spoilerText: newStatus.spoilerText || null, @@ -532,7 +571,8 @@ const PostStatusForm = { sensitive: newStatus.nsfw, media: newStatus.files, store: this.$store, - [replyOrQuoteAttr]: this.replyTo, + inReplyToStatusId: this.inReplyStatusId, + quoteId: this.quoteId, contentType: newStatus.contentType, poll, idempotencyKey: this.idempotencyKey, @@ -561,9 +601,7 @@ const PostStatusForm = { } const newStatus = this.newStatus this.previewLoading = true - const replyOrQuoteAttr = newStatus.quoting - ? 'quoteId' - : 'inReplyToStatusId' + statusPoster .postStatus({ status: newStatus.status, @@ -572,7 +610,8 @@ const PostStatusForm = { sensitive: newStatus.nsfw, media: [], store: this.$store, - [replyOrQuoteAttr]: this.replyTo, + inReplyToStatusId: this.inReplyStatusId, + quoteId: this.quoteId, contentType: newStatus.contentType, poll: {}, preview: true, @@ -816,6 +855,32 @@ const PostStatusForm = { this.$refs.pollForm.clear() } }, + initQuote() { + const quote = this.newStatus.quote + + if (Object.keys(quote).length > 0) { + return + } + + const quotable = this.defaultQuotable + + quote.id = quotable ? this.replyTo : '' + quote.url = '' + quote.thread = quotable + }, + setQuoteThread(v) { + this.newStatus.hasQuote = v + this.newStatus.quote.thread = v + this.newStatus.quote.id = v ? this.replyTo : '' + }, + clearQuoteForm() { + if (this.$refs.quoteForm) { + this.$refs.quoteForm.clear() + } + }, + toggleQuoteForm() { + this.newStatus.hasQuote = !this.newStatus.hasQuote + }, dismissScopeNotice() { useSyncConfigStore().setSimplePrefAndSave({ path: 'hideScopeNotice', diff --git a/src/components/post_status_form/post_status_form.scss b/src/components/post_status_form/post_status_form.scss index c7fdb0806..684a92267 100644 --- a/src/components/post_status_form/post_status_form.scss +++ b/src/components/post_status_form/post_status_form.scss @@ -136,11 +136,17 @@ .poll-icon { order: 3; + justify-content: center; + } + + .quote-icon { + order: 4; justify-content: right; } .media-upload-icon, .poll-icon, + .quote-icon, .emoji-icon { font-size: 1.85em; line-height: 1.1; diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index fe37394f5..5fdde4784 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -115,24 +115,26 @@ {{ $t('post_status.reply_option') }} {{ $t('post_status.quote_option') }} @@ -270,6 +272,13 @@ :visible="pollFormVisible" :params="newStatus.poll" /> + + + + import('../status/status.vue')), + }, + name: 'Quote', + props: { + visible: { + type: Boolean, + }, + loading: { + type: Boolean, + }, + statusId: { + type: String, + }, + statusUrl: { + type: String, + }, + statusVisible: { + type: Boolean, + }, + initiallyExpanded: { + type: Boolean, + }, + }, + data() { + return { + displayQuote: this.initiallyExpanded, + fetchAttempted: false, + fetching: false, + error: null, + } + }, + created() { + this.maybeFetchStatus() + }, + watch: { + statusId() { + this.maybeFetchStatus() + }, + }, + computed: { + quotedStatus() { + return this.statusId + ? this.$store.state.statuses.allStatusesObject[this.statusId] + : undefined + }, + shouldDisplayQuote() { + return this.displayQuote && this.quotedStatus + }, + hasVisibleQuote() { + return ( + this.statusUrl && + this.statusVisible && + (this.showSpinner || this.quotedStatus) + ) + }, + hasInvisibleQuote() { + return this.statusUrl && !this.statusVisible + }, + showSpinner() { + return this.loading || this.fetching + }, + }, + methods: { + toggleDisplayQuote() { + this.displayQuote = !this.displayQuote + this.maybeFetchStatus() + }, + maybeFetchStatus() { + if (this.statusId && this.displayQuote && !this.quotedStatus) { + this.fetchStatus() + } + }, + fetchStatus() { + this.fetchAttempted = true + this.fetching = true + this.$emit('loading', true) + this.$store + .dispatch('fetchStatus', this.statusId) + .then(() => { + this.displayQuote = true + }) + .catch((error) => { + this.error = error + this.$emit('error', error) + }) + .finally(() => { + this.fetching = false + this.$emit('loading', false) + }) + }, + }, +} diff --git a/src/components/quote/quote.vue b/src/components/quote/quote.vue new file mode 100644 index 000000000..f518db7ab --- /dev/null +++ b/src/components/quote/quote.vue @@ -0,0 +1,82 @@ + + + + {{ shouldDisplayQuote ? $t('status.hide_quote') : $t('status.display_quote') }} + + + + + + + + + + + + + {{ statusUrl }} + + + {{ statusUrl }} + + + + + + + + + + diff --git a/src/components/quote/quote_form.js b/src/components/quote/quote_form.js new file mode 100644 index 000000000..401211724 --- /dev/null +++ b/src/components/quote/quote_form.js @@ -0,0 +1,118 @@ +import { debounce } from 'lodash' + +import Checkbox from '../checkbox/checkbox.vue' +import Quote from './quote.vue' + +import { useInstanceStore } from 'src/stores/instance.js' + +export default { + components: { + Quote, + Checkbox, + }, + name: 'QuoteForm', + props: { + visible: { + type: Boolean, + }, + reply: { + type: Boolean, + }, + params: { + type: Object, + required: true, + }, + }, + data() { + return { + text: this.params.url, + loading: false, + error: false, + debounceSetQuote: debounce((value) => { + this.fetchStatus(value) + }, 1000), + } + }, + created() { + if (this.params.url && !this.params.id) { + this.fetchStatus(this.params.url) + } else if (this.params.id) { + this.text = + window.location.protocol + + '//' + + this.instanceHost + + '/notice/' + + this.params.id + this.params.url = this.text + } + }, + computed: { + instanceHost() { + return new URL(useInstanceStore().server).host + }, + noticeRegex() { + return new RegExp( + `^([^/:]*:?//|)(${window.location.host}|${this.instanceHost})/notice/(.*)$`, + ) + }, + quoteVisible() { + return (!!this.params.id || this.loading) && !this.error + }, + }, + watch: { + text(value) { + this.debounceSetQuote(value) + }, + visible(value) { + if (value && this.params.url) { + this.fetchStatus(this.params.url) + } + }, + }, + methods: { + clear() { + this.text = this.params.url + this.loading = false + this.error = false + }, + setLoading(value) { + this.loading = value + }, + handleError(error) { + this.params.id = null + this.error = !!error + }, + fetchStatus(value) { + this.params.url = value + this.error = false + + const notice = this.noticeRegex.exec(value) + if (notice && notice.length === 4) { + this.params.id = notice[3] + } else if (value) { + this.loading = true + this.$store + .dispatch('search', { + q: value, + resolve: true, + offset: 0, + limit: 1, + type: 'statuses', + }) + .then((data) => { + if (data && data.statuses && data.statuses.length === 1) { + this.params.id = data.statuses[0].id + } else { + this.handleError(true) + } + }) + .catch(this.handleError) + .finally(() => { + this.loading = false + }) + } else { + this.params.id = null + } + }, + }, +} diff --git a/src/components/quote/quote_form.vue b/src/components/quote/quote_form.vue new file mode 100644 index 000000000..b0b277a2b --- /dev/null +++ b/src/components/quote/quote_form.vue @@ -0,0 +1,55 @@ + + + + + + + + + + + + diff --git a/src/components/status/status.js b/src/components/status/status.js index b24146d9d..7b4a31561 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -12,6 +12,7 @@ import { import AvatarList from '../avatar_list/avatar_list.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' +import Quote from '../quote/quote.vue' import StatusContent from '../status_content/status_content.vue' import StatusPopover from '../status_popover/status_popover.vue' import Timeago from '../timeago/timeago.vue' @@ -129,6 +130,7 @@ const Status = { MentionsLine, UserPopover, UserLink, + Quote, StatusActionButtons, }, props: [ @@ -174,7 +176,6 @@ const Status = { suspendable: true, error: null, headTailLinks: null, - displayQuote: !this.inQuote, } }, computed: { @@ -498,19 +499,17 @@ const Status = { editingAvailable() { return useInstanceCapabilitiesStore().editingAvailable }, - hasVisibleQuote() { - return this.status.quote_url && this.status.quote_visible - }, - hasInvisibleQuote() { - return this.status.quote_url && !this.status.quote_visible - }, - quotedStatus() { + quoteId() { return this.status.quote_id - ? this.$store.state.statuses.allStatusesObject[this.status.quote_id] - : undefined }, - shouldDisplayQuote() { - return this.quotedStatus && this.displayQuote + quoteUrl() { + return this.status.quote_url + }, + quoteVisible() { + return this.status.quote_visible + }, + quoteExpanded() { + return !this.inQuote }, scrobblePresent() { if (this.mergedConfig.hideScrobbles) return false @@ -630,17 +629,6 @@ const Status = { } } }, - toggleDisplayQuote() { - if (this.shouldDisplayQuote) { - this.displayQuote = false - } else if (!this.quotedStatus) { - this.$store.dispatch('fetchStatus', this.status.quote_id).then(() => { - this.displayQuote = true - }) - } else { - this.displayQuote = true - } - }, }, watch: { highlight: function (id) { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 897730a07..2a40f0dde 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -388,22 +388,4 @@ } } } - - .quoted-status { - margin-top: 0.5em; - border: 1px solid var(--border); - border-radius: var(--roundness); - - &.-unavailable-prompt { - padding: 0.5em; - } - } - - .display-quoted-status-button { - margin: 0.5em; - - &-icon { - color: inherit; - } - } } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 9351d53c8..015a7017c 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -420,47 +420,12 @@ @parse-ready="setHeadTailLinks" /> - - - {{ shouldDisplayQuote ? $t('status.hide_quote') : $t('status.display_quote') }} - - - - - - - - - - {{ status.quote_url }} - - - - - + loggedIn, - toggleable: true, + toggleable: false, dropdown: true, - // action ({ status, dispatch, emit }) { - // } + action({ status, dispatch, emit }) { + /* prevent hiding */ + }, }, { // ========= diff --git a/src/components/status_action_buttons/status_action_buttons.scss b/src/components/status_action_buttons/status_action_buttons.scss index 23b86e5bd..3bbcdeefb 100644 --- a/src/components/status_action_buttons/status_action_buttons.scss +++ b/src/components/status_action_buttons/status_action_buttons.scss @@ -6,7 +6,7 @@ grid-template-columns: repeat(auto-fill, minmax(10%, 3em)); grid-auto-flow: row dense; grid-auto-rows: 1fr; - grid-gap: 1.25em 1em; + grid-gap: 1.25em 0; margin-top: var(--status-margin); } diff --git a/src/i18n/en.json b/src/i18n/en.json index 0165af240..7851c297c 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -284,6 +284,7 @@ "new_status": "Post new status", "reply_option": "Reply to this status", "quote_option": "Quote this status", + "quote_url": "Link to quoted post", "account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.", "account_not_locked_warning_link": "locked", "attachments_sensitive": "Mark attachments as sensitive", @@ -1765,6 +1766,7 @@ "favorite": "Favorite", "unfavorite": "Unfavorite", "add_reaction": "Add Reaction", + "add_quote": "Add quote", "user_settings": "User Settings", "accept_follow_request": "Accept follow request", "reject_follow_request": "Reject follow request", diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 48f8b9e8a..2e2ca5a5c 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -693,7 +693,7 @@ const fetchStatus = ({ id, credentials }) => { if (data.ok) { return data } - throw new Error('Error fetching timeline', data) + throw new Error('Error fetching timeline', { cause: data }) }) .then((data) => data.json()) .then((data) => parseStatus(data)) @@ -706,7 +706,7 @@ const fetchStatusSource = ({ id, credentials }) => { if (data.ok) { return data } - throw new Error('Error fetching source', data) + throw new Error('Error fetching source', { cause: data }) }) .then((data) => data.json()) .then((data) => parseSource(data)) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index bcc27e8cb..8fb98bc9b 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -384,7 +384,7 @@ export const parseStatus = (data) => { } output.summary_raw_html = escapeHtml(data.spoiler_text) - output.external_url = data.url + output.external_url = data.uri || data.url output.poll = data.poll if (output.poll) { output.poll.options = (output.poll.options || []).map((field) => ({
+ + + + + {{ statusUrl }} + + + {{ statusUrl }} + + + + +
- - - - - {{ status.quote_url }} - - - - -