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

View File

@ -50,6 +50,11 @@ const GeneralTab = {
value: mode, value: mode,
label: this.$t(`settings.user_popover_avatar_action_${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: loopSilentAvailable:
// Firefox // Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||

View File

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

View File

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

View File

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

View File

@ -296,7 +296,12 @@
"private": "Followers-only - post to followers only", "private": "Followers-only - post to followers only",
"public": "Public - post to public timelines", "public": "Public - post to public timelines",
"unlisted": "Unlisted - do not 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": { "registration": {
"bio_optional": "Bio (optional)", "bio_optional": "Bio (optional)",
@ -467,6 +472,10 @@
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.", "avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker", "pad_emoji": "Pad emoji with spaces when adding from picker",
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available", "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_on_timeline": "Show emoji reactions on timeline",
"emoji_reactions_scale": "Reactions scale factor", "emoji_reactions_scale": "Reactions scale factor",
"export_theme": "Save preset", "export_theme": "Save preset",

View File

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

View File

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