Co-authored-by: Sean King <seanking2919@protonmail.com>
Co-authored-by: Tusooa Zhu <tusooa@kazv.moe>
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: https://akkoma.dev/AkkomaGang/pleroma-fe/pulls/158
This commit is contained in:
floatingghost 2022-09-06 19:25:03 +00:00
parent 6084cbbb00
commit 2da92fcd13
26 changed files with 616 additions and 19 deletions

View file

@ -132,6 +132,9 @@ const Attachment = {
...mapGetters(['mergedConfig'])
},
watch: {
'attachment.description' (newVal) {
this.localDescription = newVal
},
localDescription (newVal) {
this.onEdit(newVal)
}

View file

@ -1,6 +1,8 @@
import { reduce, filter, findIndex, clone, get } from 'lodash'
import Status from '../status/status.vue'
import ThreadTree from '../thread_tree/thread_tree.vue'
import { WSConnectionStatus } from '../../services/api/api.service.js'
import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -77,6 +79,9 @@ const conversation = {
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1
},
streamingEnabled () {
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
},
displayStyle () {
return this.$store.getters.mergedConfig.conversationDisplay
},
@ -339,7 +344,11 @@ const conversation = {
},
maybeHighlight () {
return this.isExpanded ? this.highlight : null
}
},
...mapGetters(['mergedConfig']),
...mapState({
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
})
},
components: {
Status,
@ -395,6 +404,11 @@ const conversation = {
setHighlight (id) {
if (!id) return
this.highlight = id
if (!this.streamingEnabled) {
this.$store.dispatch('fetchStatus', id)
}
this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id)
},

View file

@ -0,0 +1,75 @@
import PostStatusForm from '../post_status_form/post_status_form.vue'
import Modal from '../modal/modal.vue'
import statusPosterService from '../../services/status_poster/status_poster.service.js'
import get from 'lodash/get'
const EditStatusModal = {
components: {
PostStatusForm,
Modal
},
data () {
return {
resettingForm: false
}
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
modalActivated () {
return this.$store.state.editStatus.modalActivated
},
isFormVisible () {
return this.isLoggedIn && !this.resettingForm && this.modalActivated
},
params () {
return this.$store.state.editStatus.params || {}
}
},
watch: {
params (newVal, oldVal) {
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
this.resettingForm = true
this.$nextTick(() => {
this.resettingForm = false
})
}
},
isFormVisible (val) {
if (val) {
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
}
}
},
methods: {
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
const params = {
store: this.$store,
statusId: this.$store.state.editStatus.params.statusId,
status,
spoilerText,
sensitive,
poll,
media,
contentType
}
return statusPosterService.editStatus(params)
.then((data) => {
return data
})
.catch((err) => {
console.error('Error editing status', err)
return {
error: err.message
}
})
},
closeModal () {
this.$store.dispatch('closeEditStatusModal')
}
}
}
export default EditStatusModal

View file

@ -0,0 +1,48 @@
<template>
<Modal
v-if="isFormVisible"
class="edit-form-modal-view"
@backdropClicked="closeModal"
>
<div class="edit-form-modal-panel panel">
<div class="panel-heading">
{{ $t('post_status.edit_status') }}
</div>
<PostStatusForm
class="panel-body"
v-bind="params"
@posted="closeModal"
:disablePolls="true"
:disableVisibilitySelector="true"
:post-handler="doEditStatus"
/>
</div>
</Modal>
</template>
<script src="./edit_status_modal.js"></script>
<style lang="scss">
.modal-view.edit-form-modal-view {
align-items: flex-start;
}
.edit-form-modal-panel {
flex-shrink: 0;
margin-top: 25%;
margin-bottom: 2em;
width: 100%;
max-width: 700px;
@media (orientation: landscape) {
margin-top: 8%;
}
.form-bottom-left {
max-width: 6.5em;
.emoji-icon {
justify-content: right;
}
}
}
</style>

View file

@ -7,7 +7,8 @@ import {
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt
faExternalLinkAlt,
faHistory
} from '@fortawesome/free-solid-svg-icons'
import {
faBookmark as faBookmarkReg,
@ -22,7 +23,8 @@ library.add(
faThumbtack,
faShareAlt,
faExternalLinkAlt,
faFlag
faFlag,
faHistory
)
const ExtraButtons = {
@ -101,6 +103,25 @@ const ExtraButtons = {
},
reportStatus () {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
},
editStatus () {
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
.then(data => this.$store.dispatch('openEditStatusModal', {
statusId: this.status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: this.status.nsfw,
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
visibility: this.status.visibility,
statusContentType: data.content_type
}))
},
showStatusHistory () {
const originalStatus = { ...this.status }
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
stripFieldsList.forEach(p => delete originalStatus[p])
this.$store.dispatch('openStatusHistoryModal', originalStatus)
}
},
computed: {
@ -134,7 +155,11 @@ const ExtraButtons = {
},
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
}
},
isEdited () {
return this.status.edited_at !== null
},
editingAvailable () { return this.$store.state.instance.editingAvailable }
}
}

View file

@ -73,6 +73,28 @@
icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span>
</button>
<button
v-if="ownStatus && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="editStatus"
@click="close"
>
<FAIcon
fixed-width
icon="pen"
/><span>{{ $t("status.edit") }}</span>
</button>
<button
v-if="isEdited && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="showStatusHistory"
@click="close"
>
<FAIcon
fixed-width
icon="history"
/><span>{{ $t("status.edit_history") }}</span>
</button>
<button
v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon"

View file

@ -55,6 +55,14 @@ const pxStringToNumber = (str) => {
const PostStatusForm = {
props: [
'statusId',
'statusText',
'statusIsSensitive',
'statusPoll',
'statusFiles',
'statusMediaDescriptions',
'statusScope',
'statusContentType',
'replyTo',
'quoteId',
'repliedUser',
@ -63,6 +71,7 @@ const PostStatusForm = {
'subject',
'disableSubject',
'disableScopeSelector',
'disableVisibilitySelector',
'disableNotice',
'disableLockWarning',
'disablePolls',
@ -120,23 +129,40 @@ const PostStatusForm = {
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig
let statusParams = {
spoilerText: this.subject || '',
status: statusText,
sensitiveByDefault,
nsfw: !!sensitiveByDefault,
files: [],
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
contentType
}
if (this.statusId) {
const statusContentType = this.statusContentType || contentType
statusParams = {
spoilerText: this.subject || '',
status: this.statusText || '',
sensitiveIfSubject,
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
files: this.statusFiles || [],
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(),
contentType: statusContentType
}
}
return {
dropFiles: [],
uploadingFiles: false,
error: null,
posting: false,
highlighted: 0,
newStatus: {
spoilerText: this.subject || '',
status: statusText,
sensitiveIfSubject,
nsfw: !!sensitiveByDefault,
files: [],
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
contentType
},
newStatus: statusParams,
caret: 0,
pollFormVisible: false,
showDropIcon: 'hide',
@ -232,6 +258,9 @@ const PostStatusForm = {
uploadFileLimitReached () {
return this.newStatus.files.length >= this.fileLimit
},
isEdit () {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
},
...mapGetters(['mergedConfig']),
...mapState({
mobileLayout: state => state.interface.mobileLayout

View file

@ -66,6 +66,13 @@
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
</p>
<div
v-if="isEdit"
class="visibility-notice edit-warning"
>
<p>{{ $t('post_status.edit_remote_warning') }}</p>
<p>{{ $t('post_status.edit_unsupported_warning') }}</p>
</div>
<div
v-if="!disablePreview"
class="preview-heading faint"
@ -180,6 +187,7 @@
class="visibility-tray"
>
<scope-selector
v-if="!disableVisibilitySelector"
:show-all="showAllScopes"
:user-default="userDefaultScope"
:original-scope="copyMessageScope"
@ -420,6 +428,16 @@
align-items: baseline;
}
.visibility-notice.edit-warning {
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
.media-upload-icon, .poll-icon, .emoji-icon {
font-size: 1.85em;
line-height: 1.1;

View file

@ -437,6 +437,12 @@ const Status = {
},
visibilityLocalized () {
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
},
isEdited () {
return this.status.edited_at !== null
},
editingAvailable () {
return this.$store.state.instance.editingAvailable
}
},
methods: {

View file

@ -160,7 +160,8 @@
margin-right: 0.2em;
}
& .heading-reply-row {
& .heading-reply-row,
& .heading-edited-row {
position: relative;
align-content: baseline;
font-size: 0.85em;

View file

@ -329,6 +329,30 @@
class="mentions-line"
/>
</div>
<div
v-if="isEdited && editingAvailable && !isPreview"
class="heading-edited-row"
>
<i18n-t
keypath="status.edited_at"
tag="span"
>
<template #time>
<i18n-t
keypath="time.in_past"
tag="span"
>
<template>
<Timeago
:time="status.edited_at"
:auto-update="60"
:long-format="true"
/>
</template>
</i18n-t>
</template>
</i18n-t>
</div>
</div>
<StatusContent

View file

@ -0,0 +1,60 @@
import { get } from 'lodash'
import Modal from '../modal/modal.vue'
import Status from '../status/status.vue'
const StatusHistoryModal = {
components: {
Modal,
Status
},
data () {
return {
statuses: []
}
},
computed: {
modalActivated () {
return this.$store.state.statusHistory.modalActivated
},
params () {
return this.$store.state.statusHistory.params
},
statusId () {
return this.params.id
},
historyCount () {
return this.statuses.length
},
history () {
return this.statuses
}
},
watch: {
params (newVal, oldVal) {
const newStatusId = get(newVal, 'id') !== get(oldVal, 'id')
if (newStatusId) {
this.resetHistory()
}
if (newStatusId || get(newVal, 'edited_at') !== get(oldVal, 'edited_at')) {
this.fetchStatusHistory()
}
}
},
methods: {
resetHistory () {
this.statuses = []
},
fetchStatusHistory () {
this.$store.dispatch('fetchStatusHistory', this.params)
.then(data => {
this.statuses = data
})
},
closeModal () {
this.$store.dispatch('closeStatusHistoryModal')
}
}
}
export default StatusHistoryModal

View file

@ -0,0 +1,46 @@
<template>
<Modal
v-if="modalActivated"
class="status-history-modal-view"
@backdropClicked="closeModal"
>
<div class="status-history-modal-panel panel">
<div class="panel-heading">
{{ $tc('status.edit_history_modal_title', historyCount - 1, { historyCount: historyCount - 1 }) }}
</div>
<div class="panel-body">
<div
v-if="historyCount > 0"
class="history-body"
>
<status
v-for="status in history"
:key="status.id"
:statusoid="status"
:isPreview="true"
class="conversation-status status-fadein panel-body"
/>
</div>
</div>
</div>
</Modal>
</template>
<script src="./status_history_modal.js"></script>
<style lang="scss">
.modal-view.status-history-modal-view {
align-items: flex-start;
}
.status-history-modal-panel {
flex-shrink: 0;
margin-top: 25%;
margin-bottom: 2em;
width: 100%;
max-width: 700px;
@media (orientation: landscape) {
margin-top: 8%;
}
}
</style>