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

@ -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)
})
},
},
}

View 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>

View 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
}
},
},
}

View 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>