Merge pull request 'Add quoting by url / in replies' (#3488) from iamtakingiteasy/pleroma-fe:gitlab-mr-iid-2164 into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3488
This commit is contained in:
commit
8df10bd595
18 changed files with 501 additions and 110 deletions
1
changelog.d/quote-by-url.add
Normal file
1
changelog.d/quote-by-url.add
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add quoting by URL and in replies
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
:disable-notice="true"
|
:disable-notice="true"
|
||||||
:disable-lock-warning="true"
|
:disable-lock-warning="true"
|
||||||
:disable-polls="true"
|
:disable-polls="true"
|
||||||
|
:disable-quotes="true"
|
||||||
:disable-sensitivity-checkbox="true"
|
:disable-sensitivity-checkbox="true"
|
||||||
:disable-submit="errorLoadingChat || !currentChat"
|
:disable-submit="errorLoadingChat || !currentChat"
|
||||||
:disable-preview="true"
|
:disable-preview="true"
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,12 @@ const Draft = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
safeToSave() {
|
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() {
|
postStatusFormProps() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
v-bind="params"
|
v-bind="params"
|
||||||
:post-handler="doEditStatus"
|
:post-handler="doEditStatus"
|
||||||
:disable-polls="true"
|
:disable-polls="true"
|
||||||
|
:disable-quotes="true"
|
||||||
:disable-visibility-selector="true"
|
:disable-visibility-selector="true"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import EmojiInput from '../emoji_input/emoji_input.vue'
|
||||||
import suggestor from '../emoji_input/suggestor.js'
|
import suggestor from '../emoji_input/suggestor.js'
|
||||||
import MediaUpload from '../media_upload/media_upload.vue'
|
import MediaUpload from '../media_upload/media_upload.vue'
|
||||||
import PollForm from '../poll/poll_form.vue'
|
import PollForm from '../poll/poll_form.vue'
|
||||||
|
import QuoteForm from '../quote/quote_form.vue'
|
||||||
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
||||||
import Select from '../select/select.vue'
|
import Select from '../select/select.vue'
|
||||||
import StatusContent from '../status_content/status_content.vue'
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
|
|
@ -35,6 +36,7 @@ import {
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faCircleNotch,
|
faCircleNotch,
|
||||||
faPollH,
|
faPollH,
|
||||||
|
faQuoteRight,
|
||||||
faSmileBeam,
|
faSmileBeam,
|
||||||
faTimes,
|
faTimes,
|
||||||
faUpload,
|
faUpload,
|
||||||
|
|
@ -44,6 +46,7 @@ library.add(
|
||||||
faSmileBeam,
|
faSmileBeam,
|
||||||
faPollH,
|
faPollH,
|
||||||
faUpload,
|
faUpload,
|
||||||
|
faQuoteRight,
|
||||||
faBan,
|
faBan,
|
||||||
faTimes,
|
faTimes,
|
||||||
faCircleNotch,
|
faCircleNotch,
|
||||||
|
|
@ -105,6 +108,7 @@ const PostStatusForm = {
|
||||||
'disableNotice',
|
'disableNotice',
|
||||||
'disableLockWarning',
|
'disableLockWarning',
|
||||||
'disablePolls',
|
'disablePolls',
|
||||||
|
'disableQuotes',
|
||||||
'disableSensitivityCheckbox',
|
'disableSensitivityCheckbox',
|
||||||
'disableSubmit',
|
'disableSubmit',
|
||||||
'disablePreview',
|
'disablePreview',
|
||||||
|
|
@ -136,6 +140,7 @@ const PostStatusForm = {
|
||||||
MediaUpload,
|
MediaUpload,
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
PollForm,
|
PollForm,
|
||||||
|
QuoteForm,
|
||||||
ScopeSelector,
|
ScopeSelector,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -145,6 +150,9 @@ const PostStatusForm = {
|
||||||
DraftCloser,
|
DraftCloser,
|
||||||
Popover,
|
Popover,
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.initQuote()
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateIdempotencyKey()
|
this.updateIdempotencyKey()
|
||||||
this.resize(this.$refs.textarea)
|
this.resize(this.$refs.textarea)
|
||||||
|
|
@ -203,6 +211,8 @@ const PostStatusForm = {
|
||||||
files: [],
|
files: [],
|
||||||
poll: {},
|
poll: {},
|
||||||
hasPoll: false,
|
hasPoll: false,
|
||||||
|
hasQuote: false,
|
||||||
|
quote: {},
|
||||||
mediaDescriptions: {},
|
mediaDescriptions: {},
|
||||||
visibility: scope,
|
visibility: scope,
|
||||||
contentType,
|
contentType,
|
||||||
|
|
@ -220,6 +230,8 @@ const PostStatusForm = {
|
||||||
files: this.statusFiles || [],
|
files: this.statusFiles || [],
|
||||||
poll: this.statusPoll || {},
|
poll: this.statusPoll || {},
|
||||||
hasPoll: false,
|
hasPoll: false,
|
||||||
|
hasQuote: false,
|
||||||
|
quote: {},
|
||||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||||
visibility: this.statusScope || scope,
|
visibility: this.statusScope || scope,
|
||||||
contentType: statusContentType,
|
contentType: statusContentType,
|
||||||
|
|
@ -345,12 +357,28 @@ const PostStatusForm = {
|
||||||
isEdit() {
|
isEdit() {
|
||||||
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
|
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
|
||||||
},
|
},
|
||||||
quotable() {
|
quotingAvailable() {
|
||||||
if (!useInstanceCapabilitiesStore().quotingAvailable) {
|
if (!useInstanceCapabilitiesStore().quotingAvailable) {
|
||||||
return false
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -372,12 +400,25 @@ const PostStatusForm = {
|
||||||
|
|
||||||
return false
|
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() {
|
debouncedMaybeAutoSaveDraft() {
|
||||||
return debounce(this.maybeAutoSaveDraft, 3000)
|
return debounce(this.maybeAutoSaveDraft, 3000)
|
||||||
},
|
},
|
||||||
pollFormVisible() {
|
pollFormVisible() {
|
||||||
return this.newStatus.hasPoll
|
return this.newStatus.hasPoll
|
||||||
},
|
},
|
||||||
|
quoteFormVisible() {
|
||||||
|
return this.newStatus.hasQuote && !this.newStatus.quote.thread
|
||||||
|
},
|
||||||
shouldAutoSaveDraft() {
|
shouldAutoSaveDraft() {
|
||||||
return this.$store.getters.mergedConfig.autoSaveDraft
|
return this.$store.getters.mergedConfig.autoSaveDraft
|
||||||
},
|
},
|
||||||
|
|
@ -395,7 +436,8 @@ const PostStatusForm = {
|
||||||
(this.newStatus.status ||
|
(this.newStatus.status ||
|
||||||
this.newStatus.spoilerText ||
|
this.newStatus.spoilerText ||
|
||||||
this.newStatus.files?.length ||
|
this.newStatus.files?.length ||
|
||||||
this.newStatus.hasPoll) &&
|
this.newStatus.hasPoll ||
|
||||||
|
this.newStatus.hasQuote) &&
|
||||||
this.saveable
|
this.saveable
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -406,7 +448,8 @@ const PostStatusForm = {
|
||||||
this.newStatus.status ||
|
this.newStatus.status ||
|
||||||
this.newStatus.spoilerText ||
|
this.newStatus.spoilerText ||
|
||||||
this.newStatus.files?.length ||
|
this.newStatus.files?.length ||
|
||||||
this.newStatus.hasPoll
|
this.newStatus.hasPoll ||
|
||||||
|
this.newStatus.hasQuote
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -456,11 +499,13 @@ const PostStatusForm = {
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
poll: {},
|
poll: {},
|
||||||
hasPoll: false,
|
hasPoll: false,
|
||||||
|
hasQuote: false,
|
||||||
|
quote: {},
|
||||||
mediaDescriptions: {},
|
mediaDescriptions: {},
|
||||||
quoting: false,
|
|
||||||
}
|
}
|
||||||
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
|
||||||
this.clearPollForm()
|
this.clearPollForm()
|
||||||
|
this.clearQuoteForm()
|
||||||
if (this.preserveFocus) {
|
if (this.preserveFocus) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.textarea.focus()
|
this.$refs.textarea.focus()
|
||||||
|
|
@ -500,9 +545,7 @@ const PostStatusForm = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const poll = this.newStatus.hasPoll
|
const poll = newStatus.hasPoll ? pollFormToMasto(newStatus.poll) : {}
|
||||||
? pollFormToMasto(this.newStatus.poll)
|
|
||||||
: {}
|
|
||||||
if (this.pollContentError) {
|
if (this.pollContentError) {
|
||||||
this.error = this.pollContentError
|
this.error = this.pollContentError
|
||||||
return
|
return
|
||||||
|
|
@ -518,10 +561,6 @@ const PostStatusForm = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyOrQuoteAttr = newStatus.quoting
|
|
||||||
? 'quoteId'
|
|
||||||
: 'inReplyToStatusId'
|
|
||||||
|
|
||||||
const postingOptions = {
|
const postingOptions = {
|
||||||
status: newStatus.status,
|
status: newStatus.status,
|
||||||
spoilerText: newStatus.spoilerText || null,
|
spoilerText: newStatus.spoilerText || null,
|
||||||
|
|
@ -529,7 +568,8 @@ const PostStatusForm = {
|
||||||
sensitive: newStatus.nsfw,
|
sensitive: newStatus.nsfw,
|
||||||
media: newStatus.files,
|
media: newStatus.files,
|
||||||
store: this.$store,
|
store: this.$store,
|
||||||
[replyOrQuoteAttr]: this.replyTo,
|
inReplyToStatusId: this.inReplyStatusId,
|
||||||
|
quoteId: this.quoteId,
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
poll,
|
poll,
|
||||||
idempotencyKey: this.idempotencyKey,
|
idempotencyKey: this.idempotencyKey,
|
||||||
|
|
@ -558,9 +598,7 @@ const PostStatusForm = {
|
||||||
}
|
}
|
||||||
const newStatus = this.newStatus
|
const newStatus = this.newStatus
|
||||||
this.previewLoading = true
|
this.previewLoading = true
|
||||||
const replyOrQuoteAttr = newStatus.quoting
|
|
||||||
? 'quoteId'
|
|
||||||
: 'inReplyToStatusId'
|
|
||||||
statusPoster
|
statusPoster
|
||||||
.postStatus({
|
.postStatus({
|
||||||
status: newStatus.status,
|
status: newStatus.status,
|
||||||
|
|
@ -569,7 +607,8 @@ const PostStatusForm = {
|
||||||
sensitive: newStatus.nsfw,
|
sensitive: newStatus.nsfw,
|
||||||
media: [],
|
media: [],
|
||||||
store: this.$store,
|
store: this.$store,
|
||||||
[replyOrQuoteAttr]: this.replyTo,
|
inReplyToStatusId: this.inReplyStatusId,
|
||||||
|
quoteId: this.quoteId,
|
||||||
contentType: newStatus.contentType,
|
contentType: newStatus.contentType,
|
||||||
poll: {},
|
poll: {},
|
||||||
preview: true,
|
preview: true,
|
||||||
|
|
@ -813,6 +852,32 @@ const PostStatusForm = {
|
||||||
this.$refs.pollForm.clear()
|
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() {
|
dismissScopeNotice() {
|
||||||
this.$store.dispatch('setOption', {
|
this.$store.dispatch('setOption', {
|
||||||
name: 'hideScopeNotice',
|
name: 'hideScopeNotice',
|
||||||
|
|
|
||||||
|
|
@ -137,11 +137,17 @@
|
||||||
|
|
||||||
.poll-icon {
|
.poll-icon {
|
||||||
order: 3;
|
order: 3;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-icon {
|
||||||
|
order: 4;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-upload-icon,
|
.media-upload-icon,
|
||||||
.poll-icon,
|
.poll-icon,
|
||||||
|
.quote-icon,
|
||||||
.emoji-icon {
|
.emoji-icon {
|
||||||
font-size: 1.85em;
|
font-size: 1.85em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
|
|
||||||
|
|
@ -111,24 +111,26 @@
|
||||||
<button
|
<button
|
||||||
:id="`reply-or-quote-option-${randomSeed}-reply`"
|
:id="`reply-or-quote-option-${randomSeed}-reply`"
|
||||||
class="btn button-default reply-or-quote-option"
|
class="btn button-default reply-or-quote-option"
|
||||||
:class="{ toggled: !newStatus.quoting }"
|
:class="{ toggled: !quoteThreadToggled }"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="radio"
|
role="radio"
|
||||||
|
:disabled="quoteFormVisible"
|
||||||
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
|
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
|
||||||
:aria-checked="!newStatus.quoting"
|
:aria-checked="!newStatus.quote.thread"
|
||||||
@click="newStatus.quoting = false"
|
@click="setQuoteThread(false)"
|
||||||
>
|
>
|
||||||
{{ $t('post_status.reply_option') }}
|
{{ $t('post_status.reply_option') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:id="`reply-or-quote-option-${randomSeed}-quote`"
|
:id="`reply-or-quote-option-${randomSeed}-quote`"
|
||||||
class="btn button-default reply-or-quote-option"
|
class="btn button-default reply-or-quote-option"
|
||||||
:class="{ toggled: newStatus.quoting }"
|
:class="{ toggled: quoteThreadToggled }"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="radio"
|
role="radio"
|
||||||
|
:disabled="quoteFormVisible"
|
||||||
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
|
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
|
||||||
:aria-checked="newStatus.quoting"
|
:aria-checked="newStatus.quote.thread"
|
||||||
@click="newStatus.quoting = true"
|
@click="setQuoteThread(true)"
|
||||||
>
|
>
|
||||||
{{ $t('post_status.quote_option') }}
|
{{ $t('post_status.quote_option') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -266,6 +268,13 @@
|
||||||
:visible="pollFormVisible"
|
:visible="pollFormVisible"
|
||||||
:params="newStatus.poll"
|
:params="newStatus.poll"
|
||||||
/>
|
/>
|
||||||
|
<quote-form
|
||||||
|
v-if="quotingAvailable"
|
||||||
|
ref="quoteForm"
|
||||||
|
:visible="quoteFormVisible"
|
||||||
|
:reply="isReply"
|
||||||
|
:params="newStatus.quote"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="!disableDraft && shouldAutoSaveDraft"
|
v-if="!disableDraft && shouldAutoSaveDraft"
|
||||||
class="auto-save-status"
|
class="auto-save-status"
|
||||||
|
|
@ -296,6 +305,16 @@
|
||||||
>
|
>
|
||||||
<FAIcon icon="poll-h" />
|
<FAIcon icon="poll-h" />
|
||||||
</button>
|
</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>
|
||||||
<div class="btn-group post-button-group">
|
<div class="btn-group post-button-group">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
101
src/components/quote/quote.js
Normal file
101
src/components/quote/quote.js
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(faCircleNotch)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Status: defineAsyncComponent(() => 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)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
82
src/components/quote/quote.vue
Normal file
82
src/components/quote/quote.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<article
|
||||||
|
v-if="hasVisibleQuote"
|
||||||
|
class="quoted-status"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button-unstyled -link display-quoted-status-button"
|
||||||
|
:aria-expanded="shouldDisplayQuote"
|
||||||
|
@click="toggleDisplayQuote"
|
||||||
|
>
|
||||||
|
{{ shouldDisplayQuote ? $t('status.hide_quote') : $t('status.display_quote') }}
|
||||||
|
<FAIcon
|
||||||
|
class="display-quoted-status-button-icon"
|
||||||
|
:icon="shouldDisplayQuote ? 'chevron-up' : 'chevron-down'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-show="showSpinner"
|
||||||
|
class="loading-spinner"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-old-padding"
|
||||||
|
spin
|
||||||
|
icon="circle-notch"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<Status
|
||||||
|
v-if="shouldDisplayQuote"
|
||||||
|
:statusoid="quotedStatus"
|
||||||
|
:in-quote="true"
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
<p
|
||||||
|
v-else-if="hasInvisibleQuote"
|
||||||
|
class="quoted-status -unavailable-prompt"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
scope="global"
|
||||||
|
keypath="status.invisible_quote"
|
||||||
|
>
|
||||||
|
<template #link>
|
||||||
|
<bdi>
|
||||||
|
<a
|
||||||
|
v-if="statusId"
|
||||||
|
:href="statusUrl"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ statusUrl }}
|
||||||
|
</a>
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
|
:to="{ name: 'search', query: { query: statusUrl } }"
|
||||||
|
>
|
||||||
|
{{ statusUrl }}
|
||||||
|
</router-link>
|
||||||
|
</bdi>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./quote.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
src/components/quote/quote_form.js
Normal file
118
src/components/quote/quote_form.js
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
55
src/components/quote/quote_form.vue
Normal file
55
src/components/quote/quote_form.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="quote-form"
|
||||||
|
>
|
||||||
|
<div class="input-container">
|
||||||
|
<input
|
||||||
|
v-model="text"
|
||||||
|
type="text"
|
||||||
|
size="1"
|
||||||
|
class="input"
|
||||||
|
:placeholder="$t('post_status.quote_url')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<Quote
|
||||||
|
:status-id="params.id"
|
||||||
|
:status-url="params.url"
|
||||||
|
:status-visible="quoteVisible"
|
||||||
|
:initially-expanded="true"
|
||||||
|
:loading="loading"
|
||||||
|
@loading="setLoading"
|
||||||
|
@error="handleError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./quote_form.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.quote-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 0.5em 0.5em;
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
white-space: pre;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row-reverse;
|
||||||
|
line-height: 2;
|
||||||
|
column-gap: 0.5em;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.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 StatusContent from '../status_content/status_content.vue'
|
||||||
import StatusPopover from '../status_popover/status_popover.vue'
|
import StatusPopover from '../status_popover/status_popover.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
|
|
@ -127,6 +128,7 @@ const Status = {
|
||||||
MentionsLine,
|
MentionsLine,
|
||||||
UserPopover,
|
UserPopover,
|
||||||
UserLink,
|
UserLink,
|
||||||
|
Quote,
|
||||||
StatusActionButtons,
|
StatusActionButtons,
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
|
|
@ -172,7 +174,6 @@ const Status = {
|
||||||
suspendable: true,
|
suspendable: true,
|
||||||
error: null,
|
error: null,
|
||||||
headTailLinks: null,
|
headTailLinks: null,
|
||||||
displayQuote: !this.inQuote,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -500,19 +501,17 @@ const Status = {
|
||||||
editingAvailable() {
|
editingAvailable() {
|
||||||
return useInstanceCapabilitiesStore().editingAvailable
|
return useInstanceCapabilitiesStore().editingAvailable
|
||||||
},
|
},
|
||||||
hasVisibleQuote() {
|
quoteId() {
|
||||||
return this.status.quote_url && this.status.quote_visible
|
|
||||||
},
|
|
||||||
hasInvisibleQuote() {
|
|
||||||
return this.status.quote_url && !this.status.quote_visible
|
|
||||||
},
|
|
||||||
quotedStatus() {
|
|
||||||
return this.status.quote_id
|
return this.status.quote_id
|
||||||
? this.$store.state.statuses.allStatusesObject[this.status.quote_id]
|
|
||||||
: undefined
|
|
||||||
},
|
},
|
||||||
shouldDisplayQuote() {
|
quoteUrl() {
|
||||||
return this.quotedStatus && this.displayQuote
|
return this.status.quote_url
|
||||||
|
},
|
||||||
|
quoteVisible() {
|
||||||
|
return this.status.quote_visible
|
||||||
|
},
|
||||||
|
quoteExpanded() {
|
||||||
|
return !this.inQuote
|
||||||
},
|
},
|
||||||
scrobblePresent() {
|
scrobblePresent() {
|
||||||
if (this.mergedConfig.hideScrobbles) return false
|
if (this.mergedConfig.hideScrobbles) return false
|
||||||
|
|
@ -632,17 +631,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: {
|
watch: {
|
||||||
highlight: function (id) {
|
highlight: function (id) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -419,47 +419,12 @@
|
||||||
@parse-ready="setHeadTailLinks"
|
@parse-ready="setHeadTailLinks"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<article
|
<Quote
|
||||||
v-if="hasVisibleQuote"
|
:status-id="quoteId"
|
||||||
class="quoted-status"
|
:status-url="quoteUrl"
|
||||||
>
|
:status-visible="quoteVisible"
|
||||||
<button
|
:initially-expanded="quoteExpanded"
|
||||||
class="button-unstyled -link display-quoted-status-button"
|
|
||||||
:aria-expanded="shouldDisplayQuote"
|
|
||||||
@click="toggleDisplayQuote"
|
|
||||||
>
|
|
||||||
{{ shouldDisplayQuote ? $t('status.hide_quote') : $t('status.display_quote') }}
|
|
||||||
<FAIcon
|
|
||||||
class="display-quoted-status-button-icon"
|
|
||||||
:icon="shouldDisplayQuote ? 'chevron-up' : 'chevron-down'"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
|
||||||
<Status
|
|
||||||
v-if="shouldDisplayQuote"
|
|
||||||
:statusoid="quotedStatus"
|
|
||||||
:in-quote="true"
|
|
||||||
/>
|
|
||||||
</article>
|
|
||||||
<p
|
|
||||||
v-else-if="hasInvisibleQuote"
|
|
||||||
class="quoted-status -unavailable-prompt"
|
|
||||||
>
|
|
||||||
<i18n-t
|
|
||||||
scope="global"
|
|
||||||
keypath="status.invisible_quote"
|
|
||||||
>
|
|
||||||
<template #link>
|
|
||||||
<bdi>
|
|
||||||
<a
|
|
||||||
:href="status.quote_url"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ status.quote_url }}
|
|
||||||
</a>
|
|
||||||
</bdi>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="inConversation && !isPreview && replies && replies.length"
|
v-if="inConversation && !isPreview && replies && replies.length"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
grid-template-columns: repeat(auto-fill, minmax(10%, 3em));
|
grid-template-columns: repeat(auto-fill, minmax(10%, 3em));
|
||||||
grid-auto-flow: row dense;
|
grid-auto-flow: row dense;
|
||||||
grid-auto-rows: 1fr;
|
grid-auto-rows: 1fr;
|
||||||
grid-gap: 1.25em 1em;
|
grid-gap: 1.25em 0;
|
||||||
margin-top: var(--status-margin);
|
margin-top: var(--status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,7 @@
|
||||||
"new_status": "Post new status",
|
"new_status": "Post new status",
|
||||||
"reply_option": "Reply to this status",
|
"reply_option": "Reply to this status",
|
||||||
"quote_option": "Quote 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": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
|
||||||
"account_not_locked_warning_link": "locked",
|
"account_not_locked_warning_link": "locked",
|
||||||
"attachments_sensitive": "Mark attachments as sensitive",
|
"attachments_sensitive": "Mark attachments as sensitive",
|
||||||
|
|
@ -1763,6 +1764,7 @@
|
||||||
"favorite": "Favorite",
|
"favorite": "Favorite",
|
||||||
"unfavorite": "Unfavorite",
|
"unfavorite": "Unfavorite",
|
||||||
"add_reaction": "Add Reaction",
|
"add_reaction": "Add Reaction",
|
||||||
|
"add_quote": "Add quote",
|
||||||
"user_settings": "User Settings",
|
"user_settings": "User Settings",
|
||||||
"accept_follow_request": "Accept follow request",
|
"accept_follow_request": "Accept follow request",
|
||||||
"reject_follow_request": "Reject follow request",
|
"reject_follow_request": "Reject follow request",
|
||||||
|
|
|
||||||
|
|
@ -693,7 +693,7 @@ const fetchStatus = ({ id, credentials }) => {
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
throw new Error('Error fetching timeline', data)
|
throw new Error('Error fetching timeline', { cause: data })
|
||||||
})
|
})
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => parseStatus(data))
|
.then((data) => parseStatus(data))
|
||||||
|
|
@ -706,7 +706,7 @@ const fetchStatusSource = ({ id, credentials }) => {
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
throw new Error('Error fetching source', data)
|
throw new Error('Error fetching source', { cause: data })
|
||||||
})
|
})
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => parseSource(data))
|
.then((data) => parseSource(data))
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ export const parseStatus = (data) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
output.summary_raw_html = escapeHtml(data.spoiler_text)
|
output.summary_raw_html = escapeHtml(data.spoiler_text)
|
||||||
output.external_url = data.url
|
output.external_url = data.uri || data.url
|
||||||
output.poll = data.poll
|
output.poll = data.poll
|
||||||
if (output.poll) {
|
if (output.poll) {
|
||||||
output.poll.options = (output.poll.options || []).map((field) => ({
|
output.poll.options = (output.poll.options || []).map((field) => ({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue