Allow confirmation on closing reply form

This commit is contained in:
tusooa 2023-04-06 14:15:57 -04:00
parent aa59adc81f
commit f2bdc1c563
No known key found for this signature in database
GPG Key ID: 42AEC43D48433C51
11 changed files with 170 additions and 14 deletions

View File

@ -0,0 +1,48 @@
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
const DraftCloser = {
data () {
return {
showing: false
}
},
components: {
DialogModal
},
emits: [
'save',
'discard'
],
computed: {
action () {
return this.$store.getters.mergedConfig.unsavedPostAction
},
shouldConfirm () {
return this.action === 'confirm'
}
},
methods: {
requestClose () {
if (this.shouldConfirm) {
this.showing = true
} else if (this.action === 'save') {
this.save()
} else {
this.discard()
}
},
save () {
this.$emit('save')
this.showing = false
},
discard () {
this.$emit('discard')
this.showing = false
},
cancel () {
this.showing = false
}
}
}
export default DraftCloser

View File

@ -0,0 +1,43 @@
<template>
<teleport to="#modal">
<dialog-modal
v-if="showing"
v-body-scroll-lock="true"
class="confirm-modal"
:on-cancel="cancel"
>
<template #header>
<span>
{{ $t('post_status.close_confirm_title') }}
</span>
</template>
{{ $t('post_status.close_confirm') }}
<template #footer>
<button
class="btn button-default"
@click.prevent="save"
>
{{ $t('post_status.close_confirm_save_button') }}
</button>
<button
class="btn button-default"
@click.prevent="discard"
>
{{ $t('post_status.close_confirm_discard_button') }}
</button>
<button
class="btn button-default"
@click.prevent="cancel"
>
{{ $t('post_status.close_confirm_continue_composing_button') }}
</button>
</template>
</dialog-modal>
</teleport>
</template>
<script src="./draft_closer.js"></script>

View File

@ -15,6 +15,7 @@ import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue'
import Select from '../select/select.vue'
import DraftCloser from 'src/components/draft_closer/draft_closer.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -104,7 +105,8 @@ const PostStatusForm = {
'posted',
'resize',
'mediaplay',
'mediapause'
'mediapause',
'can-close'
],
components: {
MediaUpload,
@ -115,7 +117,8 @@ const PostStatusForm = {
Select,
Attachment,
StatusContent,
Gallery
Gallery,
DraftCloser
},
mounted () {
this.updateIdempotencyKey()
@ -199,7 +202,8 @@ const PostStatusForm = {
previewLoading: false,
emojiInputShown: false,
idempotencyKey: '',
saveInhibited: true
saveInhibited: true,
savable: false
}
},
computed: {
@ -290,9 +294,9 @@ const PostStatusForm = {
isEdit () {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
},
debouncedSaveDraft () {
return debounce(this.saveDraft, 3000)
},
// debouncedSaveDraft () {
// return debounce(this.saveDraft, 3000)
// },
pollFormVisible () {
return this.newStatus.hasPoll
},
@ -310,13 +314,14 @@ const PostStatusForm = {
}
},
beforeUnmount () {
this.saveDraft()
// this.saveDraft()
},
methods: {
statusChanged () {
this.autoPreview()
this.updateIdempotencyKey()
this.debouncedSaveDraft()
// this.debouncedSaveDraft()
this.savable = true
this.saveInhibited = false
},
clearStatus () {
@ -344,6 +349,7 @@ const PostStatusForm = {
el.style.height = undefined
this.error = null
if (this.preview) this.previewStatus()
this.savable = false
},
async postStatus (event, newStatus, opts = {}) {
if (this.posting && !this.optimisticPosting) { return }
@ -678,16 +684,18 @@ const PostStatusForm = {
this.newStatus.files?.length ||
this.newStatus.hasPoll
)) {
this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus })
return this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus })
.then(id => {
if (this.newStatus.id !== id) {
this.newStatus.id = id
this.savable = false
}
})
}
return Promise.resolve()
},
abandonDraft () {
this.$store.dispatch('abandonDraft', { id: this.newStatus.id })
return this.$store.dispatch('abandonDraft', { id: this.newStatus.id })
},
getDraft (statusType, refId) {
const maybeDraft = this.$store.state.drafts.drafts[this.draftId]
@ -701,6 +709,23 @@ const PostStatusForm = {
}
}
// No draft available, fall back
},
requestClose () {
if (!this.savable) {
this.$emit('can-close')
} else {
this.$refs.draftCloser.requestClose()
}
},
saveAndCloseDraft () {
this.saveDraft().then(() => {
this.$emit('can-close')
})
},
discardAndCloseDraft () {
this.abandonDraft().then(() => {
this.$emit('can-close')
})
}
}
}

View File

@ -339,6 +339,11 @@
</Checkbox>
</div>
</form>
<DraftCloser
ref="draftCloser"
@save="saveAndCloseDraft"
@discard="discardAndCloseDraft"
/>
</div>
</template>

View File

@ -50,6 +50,11 @@ const GeneralTab = {
value: mode,
label: this.$t(`settings.user_popover_avatar_action_${mode}`)
})),
unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.unsaved_post_action_${mode}`)
})),
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||

View File

@ -518,6 +518,15 @@
{{ $t('settings.autocomplete_select_first') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="unsavedPostAction"
path="unsavedPostAction"
:options="unsavedPostActionOptions"
>
{{ $t('settings.unsaved_post_action') }}
</ChoiceSetting>
</li>
</ul>
</div>
</div>

View File

@ -423,6 +423,13 @@ const Status = {
this.error = undefined
},
toggleReplying () {
if (this.replying) {
this.$refs.postStatusForm.requestClose()
} else {
this.doToggleReplying()
}
},
doToggleReplying () {
controlledOrUncontrolledToggle(this, 'replying')
},
gotoOriginal (id) {

View File

@ -496,13 +496,15 @@
class="status-container reply-form"
>
<PostStatusForm
ref="postStatusForm"
class="reply-body"
:reply-to="status.id"
:attentions="status.attentions"
:replied-user="status.user"
:copy-message-scope="status.visibility"
:subject="replySubject"
@posted="toggleReplying"
@posted="doToggleReplying"
@can-close="doToggleReplying"
/>
</div>
</template>

View File

@ -296,7 +296,12 @@
"private": "Followers-only - post to followers only",
"public": "Public - post to public timelines",
"unlisted": "Unlisted - do not post to public timelines"
}
},
"close_confirm_title": "Closing post form",
"close_confirm": "What do you want to do with your current writing?",
"close_confirm_save_button": "Save",
"close_confirm_discard_button": "Discard",
"close_confirm_continue_composing_button": "Continue composing"
},
"registration": {
"bio_optional": "Bio (optional)",
@ -467,6 +472,10 @@
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker",
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
"unsaved_post_action": "When you try to close an unsaved posting form",
"unsaved_post_action_save": "Save it to drafts",
"unsaved_post_action_discard": "Discard it",
"unsaved_post_action_confirm": "Ask every time",
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
"emoji_reactions_scale": "Reactions scale factor",
"export_theme": "Save preset",

View File

@ -18,7 +18,8 @@ export const multiChoiceProperties = [
'conversationDisplay', // tree | linear
'conversationOtherRepliesButton', // below | inside
'mentionLinkDisplay', // short | full_for_remote | full
'userPopoverAvatarAction' // close | zoom | open
'userPopoverAvatarAction', // close | zoom | open
'unsavedPostAction' // save | discard | confirm
]
export const defaultState = {
@ -117,7 +118,8 @@ export const defaultState = {
conversationOtherRepliesButton: undefined, // instance default
conversationTreeFadeAncestors: undefined, // instance default
maxDepthInThread: undefined, // instance default
autocompleteSelect: undefined // instance default
autocompleteSelect: undefined, // instance default
unsavedPostAction: undefined // instance default
}
// caching the instance default properties

View File

@ -105,6 +105,7 @@ const defaultState = {
conversationTreeFadeAncestors: false,
maxDepthInThread: 6,
autocompleteSelect: false,
unsavedPostAction: 'confirm',
// Nasty stuff
customEmoji: [],