Merge branch 'tusooa/quote' into 'develop'

Quote

See merge request pleroma/pleroma-fe!1846
This commit is contained in:
tusooa 2023-08-19 02:33:26 +00:00
commit f059c1f314
13 changed files with 175 additions and 6 deletions

1
changelog.d/quote.add Normal file
View File

@ -0,0 +1 @@
Implement quoting

View File

@ -259,6 +259,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') }) store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })

View File

@ -156,11 +156,13 @@ const PostStatusForm = {
poll: this.statusPoll || {}, poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {}, mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || scope, visibility: this.statusScope || scope,
contentType: statusContentType contentType: statusContentType,
quoting: false
} }
} }
return { return {
randomSeed: `${Math.random()}`.replace('.', '-'),
dropFiles: [], dropFiles: [],
uploadingFiles: false, uploadingFiles: false,
error: null, error: null,
@ -265,6 +267,30 @@ const PostStatusForm = {
isEdit () { isEdit () {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
}, },
quotable () {
if (!this.$store.state.instance.quotingAvailable) {
return false
}
if (!this.replyTo) {
return false
}
const repliedStatus = this.$store.state.statuses.allStatusesObject[this.replyTo]
if (!repliedStatus) {
return false
}
if (repliedStatus.visibility === 'public' ||
repliedStatus.visibility === 'unlisted' ||
repliedStatus.visibility === 'local') {
return true
} else if (repliedStatus.visibility === 'private') {
return repliedStatus.user.id === this.$store.state.users.currentUser.id
}
return false
},
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
mobileLayout: state => state.interface.mobileLayout mobileLayout: state => state.interface.mobileLayout
@ -292,7 +318,8 @@ const PostStatusForm = {
visibility: newStatus.visibility, visibility: newStatus.visibility,
contentType: newStatus.contentType, contentType: newStatus.contentType,
poll: {}, poll: {},
mediaDescriptions: {} mediaDescriptions: {},
quoting: false
} }
this.pollFormVisible = false this.pollFormVisible = false
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile() this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
@ -340,6 +367,8 @@ 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,
@ -347,7 +376,7 @@ const PostStatusForm = {
sensitive: newStatus.nsfw, sensitive: newStatus.nsfw,
media: newStatus.files, media: newStatus.files,
store: this.$store, store: this.$store,
inReplyToStatusId: this.replyTo, [replyOrQuoteAttr]: this.replyTo,
contentType: newStatus.contentType, contentType: newStatus.contentType,
poll, poll,
idempotencyKey: this.idempotencyKey idempotencyKey: this.idempotencyKey
@ -373,6 +402,7 @@ const PostStatusForm = {
} }
const newStatus = this.newStatus const newStatus = this.newStatus
this.previewLoading = true this.previewLoading = true
const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId'
statusPoster.postStatus({ statusPoster.postStatus({
status: newStatus.status, status: newStatus.status,
spoilerText: newStatus.spoilerText || null, spoilerText: newStatus.spoilerText || null,
@ -380,7 +410,7 @@ const PostStatusForm = {
sensitive: newStatus.nsfw, sensitive: newStatus.nsfw,
media: [], media: [],
store: this.$store, store: this.$store,
inReplyToStatusId: this.replyTo, [replyOrQuoteAttr]: this.replyTo,
contentType: newStatus.contentType, contentType: newStatus.contentType,
poll: {}, poll: {},
preview: true preview: true

View File

@ -126,6 +126,36 @@
class="preview-status" class="preview-status"
/> />
</div> </div>
<div
v-if="quotable"
role="radiogroup"
class="btn-group reply-or-quote-selector"
>
<button
:id="`reply-or-quote-option-${randomSeed}-reply`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: !newStatus.quoting }"
tabindex="0"
role="radio"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
:aria-checked="!newStatus.quoting"
@click="newStatus.quoting = 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 }"
tabindex="0"
role="radio"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
:aria-checked="newStatus.quoting"
@click="newStatus.quoting = true"
>
{{ $t('post_status.quote_option') }}
</button>
</div>
<EmojiInput <EmojiInput
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)" v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
v-model="newStatus.spoilerText" v-model="newStatus.spoilerText"
@ -420,6 +450,10 @@
margin: 0; margin: 0;
} }
.reply-or-quote-selector {
margin-bottom: 0.5em;
}
.text-format { .text-format {
.only-format { .only-format {
color: $fallback--faint; color: $fallback--faint;

View File

@ -133,6 +133,7 @@ const Status = {
'showPinned', 'showPinned',
'inProfile', 'inProfile',
'profileUserId', 'profileUserId',
'inQuote',
'simpleTree', 'simpleTree',
'controlledThreadDisplayStatus', 'controlledThreadDisplayStatus',
@ -159,7 +160,8 @@ const Status = {
uncontrolledMediaPlaying: [], uncontrolledMediaPlaying: [],
suspendable: true, suspendable: true,
error: null, error: null,
headTailLinks: null headTailLinks: null,
displayQuote: !this.inQuote
} }
}, },
computed: { computed: {
@ -401,6 +403,18 @@ const Status = {
}, },
editingAvailable () { editingAvailable () {
return this.$store.state.instance.editingAvailable return this.$store.state.instance.editingAvailable
},
hasVisibleQuote () {
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 ? this.$store.state.statuses.allStatusesObject[this.status.quote_id] : undefined
},
shouldDisplayQuote () {
return this.quotedStatus && this.displayQuote
} }
}, },
methods: { methods: {
@ -469,6 +483,18 @@ const Status = {
window.scrollBy(0, rect.bottom - window.innerHeight + 50) window.scrollBy(0, rect.bottom - window.innerHeight + 50)
} }
} }
},
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: {

View File

@ -422,4 +422,22 @@
} }
} }
} }
.quoted-status {
margin-top: 0.5em;
border: 1px solid var(--border, $fallback--border);
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
&.-unavailable-prompt {
padding: 0.5em;
}
}
.display-quoted-status-button {
margin: 0.5em;
&-icon {
color: inherit;
}
}
} }

View File

@ -364,6 +364,45 @@
@parseReady="setHeadTailLinks" @parseReady="setHeadTailLinks"
/> />
<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>
<Status
v-if="shouldDisplayQuote"
:statusoid="quotedStatus"
:in-quote="true"
/>
</article>
<p
v-else-if="hasInvisibleQuote"
class="quoted-status -unavailable-prompt"
>
<i18n-t 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"
class="replies" class="replies"

View File

@ -261,6 +261,8 @@
"post_status": { "post_status": {
"edit_status": "Edit status", "edit_status": "Edit status",
"new_status": "Post new status", "new_status": "Post new status",
"reply_option": "Reply to this status",
"quote_option": "Quote this status",
"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",
@ -1028,7 +1030,10 @@
"show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)", "show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)",
"show_only_conversation_under_this": "Only show replies to this status", "show_only_conversation_under_this": "Only show replies to this status",
"status_history": "Status history", "status_history": "Status history",
"reaction_count_label": "{num} person reacted | {num} people reacted" "reaction_count_label": "{num} person reacted | {num} people reacted",
"hide_quote": "Hide the quoted status",
"display_quote": "Display the quoted status",
"invisible_quote": "Quoted status unavailable: {link}"
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",

View File

@ -128,6 +128,7 @@ const defaultState = {
mediaProxyAvailable: false, mediaProxyAvailable: false,
suggestionsEnabled: false, suggestionsEnabled: false,
suggestionsWeb: '', suggestionsWeb: '',
quotingAvailable: false,
// Html stuff // Html stuff
instanceSpecificPanelContent: '', instanceSpecificPanelContent: '',

View File

@ -229,6 +229,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
timelineObject.newStatusCount += 1 timelineObject.newStatusCount += 1
} }
if (status.quote) {
addStatus(status.quote, /* showImmediately = */ false, /* addToTimeline = */ false)
}
return status return status
} }

View File

@ -827,6 +827,7 @@ const postStatus = ({
poll, poll,
mediaIds = [], mediaIds = [],
inReplyToStatusId, inReplyToStatusId,
quoteId,
contentType, contentType,
preview, preview,
idempotencyKey idempotencyKey
@ -859,6 +860,9 @@ const postStatus = ({
if (inReplyToStatusId) { if (inReplyToStatusId) {
form.append('in_reply_to_id', inReplyToStatusId) form.append('in_reply_to_id', inReplyToStatusId)
} }
if (quoteId) {
form.append('quote_id', quoteId)
}
if (preview) { if (preview) {
form.append('preview', 'true') form.append('preview', 'true')
} }

View File

@ -325,6 +325,10 @@ export const parseStatus = (data) => {
output.thread_muted = pleroma.thread_muted output.thread_muted = pleroma.thread_muted
output.emoji_reactions = pleroma.emoji_reactions output.emoji_reactions = pleroma.emoji_reactions
output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
output.quote = pleroma.quote ? parseStatus(pleroma.quote) : undefined
output.quote_id = pleroma.quote_id ? pleroma.quote_id : (output.quote ? output.quote.id : undefined)
output.quote_url = pleroma.quote_url
output.quote_visible = pleroma.quote_visible
} else { } else {
output.text = data.content output.text = data.content
output.summary = data.spoiler_text output.summary = data.spoiler_text

View File

@ -10,6 +10,7 @@ const postStatus = ({
poll, poll,
media = [], media = [],
inReplyToStatusId = undefined, inReplyToStatusId = undefined,
quoteId = undefined,
contentType = 'text/plain', contentType = 'text/plain',
preview = false, preview = false,
idempotencyKey = '' idempotencyKey = ''
@ -24,6 +25,7 @@ const postStatus = ({
sensitive, sensitive,
mediaIds, mediaIds,
inReplyToStatusId, inReplyToStatusId,
quoteId,
contentType, contentType,
poll, poll,
preview, preview,