Add quotes (#59)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Reviewed-on: AkkomaGang/pleroma-fe#59
This commit is contained in:
floatingghost 2022-07-25 16:25:41 +00:00
parent 04bb4112c0
commit a2541bb4e0
17 changed files with 274 additions and 11 deletions

View file

@ -56,6 +56,7 @@ const pxStringToNumber = (str) => {
const PostStatusForm = { const PostStatusForm = {
props: [ props: [
'replyTo', 'replyTo',
'quoteId',
'repliedUser', 'repliedUser',
'attentions', 'attentions',
'copyMessageScope', 'copyMessageScope',
@ -99,12 +100,12 @@ const PostStatusForm = {
this.updateIdempotencyKey() this.updateIdempotencyKey()
this.resize(this.$refs.textarea) this.resize(this.$refs.textarea)
if (this.replyTo) { if (this.replyTo || this.quoteId) {
const textLength = this.$refs.textarea.value.length const textLength = this.$refs.textarea.value.length
this.$refs.textarea.setSelectionRange(textLength, textLength) this.$refs.textarea.setSelectionRange(textLength, textLength)
} }
if (this.replyTo || this.autoFocus) { if (this.replyTo || this.quoteId || this.autoFocus) {
this.$refs.textarea.focus() this.$refs.textarea.focus()
} }
}, },
@ -112,7 +113,7 @@ const PostStatusForm = {
const preset = this.$route.query.message const preset = this.$route.query.message
let statusText = preset || '' let statusText = preset || ''
if (this.replyTo) { if (this.replyTo || this.quoteId) {
const currentUser = this.$store.state.users.currentUser const currentUser = this.$store.state.users.currentUser
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
} }
@ -314,6 +315,7 @@ const PostStatusForm = {
media: newStatus.files, media: newStatus.files,
store: this.$store, store: this.$store,
inReplyToStatusId: this.replyTo, inReplyToStatusId: this.replyTo,
quoteId: this.quoteId,
contentType: newStatus.contentType, contentType: newStatus.contentType,
poll, poll,
idempotencyKey: this.idempotencyKey idempotencyKey: this.idempotencyKey
@ -347,6 +349,7 @@ const PostStatusForm = {
media: [], media: [],
store: this.$store, store: this.$store,
inReplyToStatusId: this.replyTo, inReplyToStatusId: this.replyTo,
quoteId: this.quoteId,
contentType: newStatus.contentType, contentType: newStatus.contentType,
poll: {}, poll: {},
preview: true preview: true

View file

@ -0,0 +1,16 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faQuoteLeft } from '@fortawesome/free-solid-svg-icons'
library.add(faQuoteLeft)
const QuoteButton = {
name: 'QuoteButton',
props: ['status', 'quoting', 'visibility'],
computed: {
loggedIn () {
return !!this.$store.state.users.currentUser
}
}
}
export default QuoteButton

View file

@ -0,0 +1,55 @@
<template>
<div
v-if="loggedIn"
class="QuoteButton"
>
<button
v-if="visibility === 'public' || visibility === 'unlisted'"
class="button-unstyled interactive"
:class="{'-active': quoting}"
:title="$t('tool_tip.quote')"
@click.prevent="$emit('toggle')"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="quote-left"
/>
</button>
<span v-else-if="loggedIn">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="lock"
:title="$t('timeline.no_quote_hint')"
/>
</span>
</div>
</template>
<script src="./quote_button.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.QuoteButton {
display: flex;
> :first-child {
padding: 10px;
margin: -10px -8px -10px -10px;
}
.action-counter {
pointer-events: none;
user-select: none;
}
.interactive {
&:hover .svg-inline--fa,
&.-active .svg-inline--fa {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
}
}
</style>

View file

@ -0,0 +1,32 @@
import { mapGetters } from 'vuex'
import QuoteCardContent from '../quote_card_content/quote_card_content.vue'
const QuoteCard = {
name: 'QuoteCard',
props: [
'status'
],
data () {
return {
imageLoaded: false
}
},
computed: {
...mapGetters([
'mergedConfig'
]),
statusLink () {
return {
name: 'conversation',
params: {
id: this.status.id
}
}
}
},
components: {
QuoteCardContent
}
}
export default QuoteCard

View file

@ -0,0 +1,76 @@
<template>
<div>
<a
class="quote-card"
:href="$router.resolve(statusLink).href"
target="_blank"
rel="noopener"
>
<QuoteCardContent
:status="status"
/>
</a>
</div>
</template>
<script src="./quote_card"></script>
<style lang="scss">
@import '../../_variables.scss';
.quote-card {
display: flex;
flex-direction: row;
cursor: pointer;
overflow: hidden;
margin-top: 0.5em;
.card-image {
flex-shrink: 0;
width: 120px;
max-width: 25%;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
}
}
.card-content {
max-height: 100%;
margin: 0.5em;
display: flex;
flex-direction: column;
}
.card-host {
font-size: 0.85em;
}
.card-description {
margin: 0.5em 0 0 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
line-height: 1.2em;
// cap description at 3 lines, the 1px is to clean up some stray pixels
// TODO: fancier fade-out at the bottom to show off that it's too long?
max-height: calc(1.2em * 3 - 1px);
}
.nsfw-alert {
margin: 2em 0;
}
color: $fallback--text;
color: var(--text, $fallback--text);
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
</style>

View file

@ -0,0 +1,22 @@
<template>
<Status
v-if="status"
:is-preview="true"
:statusoid="status"
:compact="true"
/>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
name: 'QuoteCardContent',
components: {
Status: defineAsyncComponent(() => import('../status/status.vue'))
},
props: [
'status'
]
}
</script>

View file

@ -1,7 +1,7 @@
import { extractCommit } from 'src/services/version/version.service' import { extractCommit } from 'src/services/version/version.service'
const pleromaFeCommitUrl = 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/' const pleromaFeCommitUrl = 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/'
const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commits/' const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commit/'
const VersionTab = { const VersionTab = {
data () { data () {

View file

@ -1,4 +1,5 @@
import ReplyButton from '../reply_button/reply_button.vue' import ReplyButton from '../reply_button/reply_button.vue'
import QuoteButton from '../quote_button/quote_button.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue' import FavoriteButton from '../favorite_button/favorite_button.vue'
import ReactButton from '../react_button/react_button.vue' import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue'
@ -115,7 +116,8 @@ const Status = {
StatusContent, StatusContent,
RichContent, RichContent,
MentionLink, MentionLink,
MentionsLine MentionsLine,
QuoteButton
}, },
props: [ props: [
'statusoid', 'statusoid',
@ -145,6 +147,8 @@ const Status = {
'controlledToggleShowingLongSubject', 'controlledToggleShowingLongSubject',
'controlledReplying', 'controlledReplying',
'controlledToggleReplying', 'controlledToggleReplying',
'controlledQuoting',
'controlledToggleQuoting',
'controlledMediaPlaying', 'controlledMediaPlaying',
'controlledSetMediaPlaying', 'controlledSetMediaPlaying',
'dive' 'dive'
@ -152,6 +156,7 @@ const Status = {
data () { data () {
return { return {
uncontrolledReplying: false, uncontrolledReplying: false,
uncontrolledQuoting: false,
unmuted: false, unmuted: false,
userExpanded: false, userExpanded: false,
uncontrolledMediaPlaying: [], uncontrolledMediaPlaying: [],
@ -161,7 +166,7 @@ const Status = {
} }
}, },
computed: { computed: {
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']), ...controlledOrUncontrolledGetters(['replying', 'quoting', 'mediaPlaying']),
muteWords () { muteWords () {
return this.mergedConfig.muteWords return this.mergedConfig.muteWords
}, },
@ -418,6 +423,9 @@ const Status = {
toggleReplying () { toggleReplying () {
controlledOrUncontrolledToggle(this, 'replying') controlledOrUncontrolledToggle(this, 'replying')
}, },
toggleQuoting () {
controlledOrUncontrolledToggle(this, 'quoting')
},
gotoOriginal (id) { gotoOriginal (id) {
if (this.inConversation) { if (this.inConversation) {
this.$emit('goto', id) this.$emit('goto', id)

View file

@ -101,6 +101,10 @@
.status-heading { .status-heading {
margin-bottom: 0.5em; margin-bottom: 0.5em;
.emoji {
--emoji-size: 16px;
}
} }
.heading-name-row { .heading-name-row {
@ -355,6 +359,15 @@
flex: 1; flex: 1;
} }
.quote-form {
padding-top: 0;
padding-bottom: 0;
}
.quote-body {
flex: 1;
}
.favs-repeated-users { .favs-repeated-users {
margin-top: var(--status-margin, $status-margin); margin-top: var(--status-margin, $status-margin);
} }

View file

@ -430,6 +430,12 @@
:status="status" :status="status"
@toggle="toggleReplying" @toggle="toggleReplying"
/> />
<quote-button
:visibility="status.visibility"
:quoting="quoting"
:status="status"
@toggle="toggleQuoting"
/>
<retweet-button <retweet-button
:visibility="status.visibility" :visibility="status.visibility"
:logged-in="loggedIn" :logged-in="loggedIn"
@ -488,6 +494,20 @@
@posted="toggleReplying" @posted="toggleReplying"
/> />
</div> </div>
<div
v-if="quoting"
class="status-container quote-form"
>
<PostStatusForm
class="quote-body"
:quote-id="status.id"
:attentions="[status.user]"
:replied-user="status.user"
:copy-message-scope="status.visibility"
:subject="replySubject"
@posted="toggleQuoting"
/>
</div>
</template> </template>
</div> </div>
</template> </template>

View file

@ -6,9 +6,8 @@
.emoji { .emoji {
--_still_image-label-scale: 0.5; --_still_image-label-scale: 0.5;
--emoji-size: 50px;
width: 50px; --emoji-size: 50px;
height: 50px;
} }
._mfm_x2_ { ._mfm_x2_ {

View file

@ -3,6 +3,7 @@ import Poll from '../poll/poll.vue'
import Gallery from '../gallery/gallery.vue' import Gallery from '../gallery/gallery.vue'
import StatusBody from 'src/components/status_body/status_body.vue' import StatusBody from 'src/components/status_body/status_body.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import QuoteCard from '../quote_card/quote_card.vue'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -109,7 +110,8 @@ const StatusContent = {
Poll, Poll,
Gallery, Gallery,
LinkPreview, LinkPreview,
StatusBody StatusBody,
QuoteCard
}, },
methods: { methods: {
toggleShowingTall () { toggleShowingTall () {

View file

@ -40,7 +40,14 @@
@play="$emit('mediaplay', attachment.id)" @play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)" @pause="$emit('mediapause', attachment.id)"
/> />
<div
v-if="status.quote && !compact"
class="quote"
>
<QuoteCard
:status="status.quote"
/>
</div>
<div <div
v-if="status.card && !noHeading && !compact" v-if="status.card && !noHeading && !compact"
class="link-preview media-body" class="link-preview media-body"

View file

@ -782,6 +782,7 @@
"error": "Error fetching timeline: {0}", "error": "Error fetching timeline: {0}",
"load_older": "Load older statuses", "load_older": "Load older statuses",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
"no_quote_hint": "Post is marked as followers-only or direct and cannot be quoted",
"repeated": "repeated", "repeated": "repeated",
"show_new": "Show new", "show_new": "Show new",
"reload": "Reload", "reload": "Reload",

View file

@ -763,6 +763,7 @@ const postStatus = ({
poll, poll,
mediaIds = [], mediaIds = [],
inReplyToStatusId, inReplyToStatusId,
quoteId,
contentType, contentType,
preview, preview,
idempotencyKey idempotencyKey
@ -795,6 +796,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

@ -347,6 +347,9 @@ export const parseStatus = (data) => {
output.visibility = data.visibility output.visibility = data.visibility
output.card = data.card output.card = data.card
output.created_at = new Date(data.created_at) output.created_at = new Date(data.created_at)
if (data.quote) {
output.quote = parseStatus(data.quote)
}
// Converting to string, the right way. // Converting to string, the right way.
output.in_reply_to_status_id = output.in_reply_to_status_id output.in_reply_to_status_id = output.in_reply_to_status_id

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,