Add quoting by url / in replies

This commit is contained in:
Alexander Tumin 2025-07-28 11:39:50 +03:00
commit 7aefda4211
18 changed files with 501 additions and 110 deletions

View file

@ -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'
@ -35,6 +36,7 @@ import {
faChevronRight,
faCircleNotch,
faPollH,
faQuoteRight,
faSmileBeam,
faTimes,
faUpload,
@ -44,6 +46,7 @@ library.add(
faSmileBeam,
faPollH,
faUpload,
faQuoteRight,
faBan,
faTimes,
faCircleNotch,
@ -105,6 +108,7 @@ const PostStatusForm = {
'disableNotice',
'disableLockWarning',
'disablePolls',
'disableQuotes',
'disableSensitivityCheckbox',
'disableSubmit',
'disablePreview',
@ -136,6 +140,7 @@ const PostStatusForm = {
MediaUpload,
EmojiInput,
PollForm,
QuoteForm,
ScopeSelector,
Checkbox,
Select,
@ -145,6 +150,9 @@ const PostStatusForm = {
DraftCloser,
Popover,
},
created() {
this.initQuote()
},
mounted() {
this.updateIdempotencyKey()
this.resize(this.$refs.textarea)
@ -203,6 +211,8 @@ const PostStatusForm = {
files: [],
poll: {},
hasPoll: false,
hasQuote: false,
quote: {},
mediaDescriptions: {},
visibility: scope,
contentType,
@ -220,6 +230,8 @@ const PostStatusForm = {
files: this.statusFiles || [],
poll: this.statusPoll || {},
hasPoll: false,
hasQuote: false,
quote: {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || scope,
contentType: statusContentType,
@ -345,12 +357,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
}
@ -372,12 +400,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 this.$store.getters.mergedConfig.autoSaveDraft
},
@ -395,7 +436,8 @@ const PostStatusForm = {
(this.newStatus.status ||
this.newStatus.spoilerText ||
this.newStatus.files?.length ||
this.newStatus.hasPoll) &&
this.newStatus.hasPoll ||
this.newStatus.hasQuote) &&
this.saveable
)
},
@ -406,7 +448,8 @@ const PostStatusForm = {
this.newStatus.status ||
this.newStatus.spoilerText ||
this.newStatus.files?.length ||
this.newStatus.hasPoll
this.newStatus.hasPoll ||
this.newStatus.hasQuote
)
)
},
@ -456,11 +499,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()
@ -500,9 +545,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
@ -518,10 +561,6 @@ const PostStatusForm = {
return
}
const replyOrQuoteAttr = newStatus.quoting
? 'quoteId'
: 'inReplyToStatusId'
const postingOptions = {
status: newStatus.status,
spoilerText: newStatus.spoilerText || null,
@ -529,7 +568,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,
@ -558,9 +598,7 @@ const PostStatusForm = {
}
const newStatus = this.newStatus
this.previewLoading = true
const replyOrQuoteAttr = newStatus.quoting
? 'quoteId'
: 'inReplyToStatusId'
statusPoster
.postStatus({
status: newStatus.status,
@ -569,7 +607,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,
@ -813,6 +852,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() {
this.$store.dispatch('setOption', {
name: 'hideScopeNotice',

View file

@ -137,11 +137,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;

View file

@ -111,24 +111,26 @@
<button
:id="`reply-or-quote-option-${randomSeed}-reply`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: !newStatus.quoting }"
:class="{ toggled: !quoteThreadToggled }"
tabindex="0"
role="radio"
:disabled="quoteFormVisible"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
:aria-checked="!newStatus.quoting"
@click="newStatus.quoting = false"
:aria-checked="!newStatus.quote.thread"
@click="setQuoteThread(false)"
>
{{ $t('post_status.reply_option') }}
</button>
<button
:id="`reply-or-quote-option-${randomSeed}-quote`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: newStatus.quoting }"
:class="{ toggled: quoteThreadToggled }"
tabindex="0"
role="radio"
:disabled="quoteFormVisible"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
:aria-checked="newStatus.quoting"
@click="newStatus.quoting = true"
:aria-checked="newStatus.quote.thread"
@click="setQuoteThread(true)"
>
{{ $t('post_status.quote_option') }}
</button>
@ -266,6 +268,13 @@
:visible="pollFormVisible"
:params="newStatus.poll"
/>
<quote-form
v-if="quotingAvailable"
ref="quoteForm"
:visible="quoteFormVisible"
:reply="isReply"
:params="newStatus.quote"
/>
<span
v-if="!disableDraft && shouldAutoSaveDraft"
class="auto-save-status"
@ -296,6 +305,16 @@
>
<FAIcon icon="poll-h" />
</button>
<button
v-if="quotingAvailable"
class="quote-icon button-unstyled"
:disabled="newStatus.quote.thread"
:class="{ selected: quoteFormVisible }"
:title="$t('tool_tip.add_quote')"
@click="toggleQuoteForm"
>
<FAIcon icon="quote-right" />
</button>
</div>
<div class="btn-group post-button-group">
<button