diff --git a/changelog.d/drafts.add b/changelog.d/drafts.add new file mode 100644 index 00000000..1147016e --- /dev/null +++ b/changelog.d/drafts.add @@ -0,0 +1 @@ +Add draft management system diff --git a/src/App.scss b/src/App.scss index f52ba06b..2afc4390 100644 --- a/src/App.scss +++ b/src/App.scss @@ -748,6 +748,12 @@ option { margin-left: 0.7em; margin-top: -1em; } + + &.-neutral { + background-color: var(--badgeNeutral); + color: white; + color: var(--badgeNeutralText, white); + } } .alert { diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 65a15178..cf242092 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -368,6 +368,8 @@ const afterStoreSetup = async ({ store, i18n }) => { getInstanceConfig({ store }) ]).catch(e => Promise.reject(e)) + await store.dispatch('loadDrafts') + // Start fetching things that don't need to block the UI store.dispatch('fetchMutes') store.dispatch('startFetchingAnnouncements') diff --git a/src/boot/routes.js b/src/boot/routes.js index f87b2ec8..66d937c8 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -26,6 +26,7 @@ import ListsEdit from 'components/lists_edit/lists_edit.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue' import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' +import Drafts from 'components/drafts/drafts.vue' import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue' import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue' @@ -82,6 +83,7 @@ export default (store) => { { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, { name: 'about', path: '/about', component: About }, { name: 'announcements', path: '/announcements', component: AnnouncementsPage }, + { name: 'drafts', path: '/drafts', component: Drafts }, { name: 'user-profile', path: '/users/:name', component: UserProfile }, { name: 'legacy-user-profile', path: '/:name', component: UserProfile }, { name: 'lists', path: '/lists', component: Lists }, diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue index 6efe576b..57158aba 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -76,6 +76,7 @@ :disable-sensitivity-checkbox="true" :disable-submit="errorLoadingChat || !currentChat" :disable-preview="true" + :disable-draft="true" :optimistic-posting="true" :post-handler="sendMessage" :submit-on-enter="!mobileLayout" diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js new file mode 100644 index 00000000..cb07ec5c --- /dev/null +++ b/src/components/draft/draft.js @@ -0,0 +1,64 @@ +import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' +import EditStatusForm from 'src/components/edit_status_form/edit_status_form.vue' +import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' +import StatusContent from 'src/components/status_content/status_content.vue' + +const Draft = { + components: { + PostStatusForm, + EditStatusForm, + ConfirmModal, + StatusContent + }, + props: { + draft: { + type: Object, + required: true + } + }, + data () { + return { + editing: false, + showingConfirmDialog: false + } + }, + computed: { + relAttrs () { + if (this.draft.type === 'edit') { + return { statusId: this.draft.refId } + } else if (this.draft.type === 'reply') { + return { replyTo: this.draft.refId } + } else { + return {} + } + }, + postStatusFormProps () { + return { + draftId: this.draft.id, + ...this.relAttrs + } + }, + refStatus () { + return this.draft.refId ? this.$store.state.statuses.allStatusesObject[this.draft.refId] : undefined + } + }, + methods: { + toggleEditing () { + this.editing = !this.editing + }, + abandon () { + this.showingConfirmDialog = true + }, + doAbandon () { + this.$store.dispatch('abandonDraft', { id: this.draft.id }) + .then(() => { + this.hideConfirmDialog() + }) + }, + hideConfirmDialog () { + this.showingConfirmDialog = false + } + } +} + +export default Draft diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue new file mode 100644 index 00000000..d9d35612 --- /dev/null +++ b/src/components/draft/draft.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/src/components/draft_closer/draft_closer.js b/src/components/draft_closer/draft_closer.js new file mode 100644 index 00000000..e50ea05a --- /dev/null +++ b/src/components/draft_closer/draft_closer.js @@ -0,0 +1,52 @@ +import DialogModal from 'src/components/dialog_modal/dialog_modal.vue' + +const DraftCloser = { + data () { + return { + showing: false + } + }, + components: { + DialogModal + }, + emits: [ + 'save', + 'discard' + ], + computed: { + action () { + if (this.$store.getters.mergedConfig.autoSaveDraft) { + return 'save' + } else { + 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 diff --git a/src/components/draft_closer/draft_closer.vue b/src/components/draft_closer/draft_closer.vue new file mode 100644 index 00000000..1afb1f44 --- /dev/null +++ b/src/components/draft_closer/draft_closer.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/drafts/drafts.js b/src/components/drafts/drafts.js new file mode 100644 index 00000000..201417f6 --- /dev/null +++ b/src/components/drafts/drafts.js @@ -0,0 +1,16 @@ +import Draft from 'src/components/draft/draft.vue' +import List from 'src/components/list/list.vue' + +const Drafts = { + components: { + Draft, + List + }, + computed: { + drafts () { + return this.$store.getters.draftsArray + } + } +} + +export default Drafts diff --git a/src/components/drafts/drafts.vue b/src/components/drafts/drafts.vue new file mode 100644 index 00000000..e64a506e --- /dev/null +++ b/src/components/drafts/drafts.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/components/edit_status_form/edit_status_form.js b/src/components/edit_status_form/edit_status_form.js new file mode 100644 index 00000000..32376337 --- /dev/null +++ b/src/components/edit_status_form/edit_status_form.js @@ -0,0 +1,44 @@ +import PostStatusForm from '../post_status_form/post_status_form.vue' +import statusPosterService from '../../services/status_poster/status_poster.service.js' + +const EditStatusForm = { + components: { + PostStatusForm + }, + props: { + params: { + type: Object, + required: true + } + }, + methods: { + requestClose () { + this.$refs.postStatusForm.requestClose() + }, + doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) { + const params = { + store: this.$store, + statusId: this.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 + } + }) + } + } +} + +export default EditStatusForm diff --git a/src/components/edit_status_form/edit_status_form.vue b/src/components/edit_status_form/edit_status_form.vue new file mode 100644 index 00000000..0a7ec760 --- /dev/null +++ b/src/components/edit_status_form/edit_status_form.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js index 75adfea7..0de2e53c 100644 --- a/src/components/edit_status_modal/edit_status_modal.js +++ b/src/components/edit_status_modal/edit_status_modal.js @@ -1,11 +1,10 @@ -import PostStatusForm from '../post_status_form/post_status_form.vue' +import EditStatusForm from '../edit_status_form/edit_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, + EditStatusForm, Modal }, data () { @@ -43,30 +42,10 @@ const EditStatusModal = { } }, 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.$refs.editStatusForm.requestClose() + }, + doCloseModal () { this.$store.dispatch('closeEditStatusModal') } } diff --git a/src/components/edit_status_modal/edit_status_modal.vue b/src/components/edit_status_modal/edit_status_modal.vue index 5001bd46..42073d7d 100644 --- a/src/components/edit_status_modal/edit_status_modal.vue +++ b/src/components/edit_status_modal/edit_status_modal.vue @@ -10,13 +10,12 @@ {{ $t('post_status.edit_status') }} - diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 11863e97..bc962e32 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -20,7 +20,8 @@ import { faInfoCircle, faStream, faList, - faBullhorn + faBullhorn, + faFilePen } from '@fortawesome/free-solid-svg-icons' library.add( @@ -35,7 +36,8 @@ library.add( faInfoCircle, faStream, faList, - faBullhorn + faBullhorn, + faFilePen ) const NavPanel = { props: ['forceExpand', 'forceEditMode'], diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index 156cd8cb..46ed3f8c 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -62,6 +62,7 @@ export const ROOT_ITEMS = { route: 'chats', icon: 'comments', label: 'nav.chats', + badgeStyle: 'notification', badgeGetter: 'unreadChatCount', criteria: ['chats'] }, @@ -69,6 +70,7 @@ export const ROOT_ITEMS = { route: 'friend-requests', icon: 'user-plus', label: 'nav.friend_requests', + badgeStyle: 'notification', criteria: ['lockedUser'], badgeGetter: 'followRequestCount' }, @@ -82,8 +84,16 @@ export const ROOT_ITEMS = { route: 'announcements', icon: 'bullhorn', label: 'nav.announcements', + badgeStyle: 'notification', badgeGetter: 'unreadAnnouncementCount', criteria: ['announcements'] + }, + drafts: { + route: 'drafts', + icon: 'file-pen', + label: 'nav.drafts', + badgeStyle: 'neutral', + badgeGetter: 'draftCount' } } diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index 024ee314..4b6c8e29 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -48,7 +48,8 @@
{{ getters[item.badgeGetter] }}
diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue index decd1c04..37351b91 100644 --- a/src/components/navigation/navigation_pins.vue +++ b/src/components/navigation/navigation_pins.vue @@ -19,7 +19,8 @@ >{{ item.iconLetter }}
@@ -34,6 +35,14 @@ overflow: hidden; height: 100%; + &.alert-dot-notification { + background-color: var(--badgeNotification); + } + + &.alert-dot-neutral { + background-color: var(--badgeNeutral); + } + .pinned-item { position: relative; flex: 1 0 3em; diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js index a2070155..29ccbc4b 100644 --- a/src/components/poll/poll_form.js +++ b/src/components/poll/poll_form.js @@ -1,5 +1,5 @@ import * as DateUtils from 'src/services/date_utils/date_utils.js' -import { uniq } from 'lodash' +import { pollFallback } from 'src/services/poll/poll.service.js' import { library } from '@fortawesome/fontawesome-svg-core' import Select from '../select/select.vue' import { @@ -17,14 +17,33 @@ export default { Select }, name: 'PollForm', - props: ['visible'], - data: () => ({ - pollType: 'single', - options: ['', ''], - expiryAmount: 10, - expiryUnit: 'minutes' - }), + props: { + visible: {}, + params: { + type: Object, + required: true + } + }, computed: { + pollType: { + get () { return pollFallback(this.params, 'pollType') }, + set (newVal) { this.params.pollType = newVal } + }, + options () { + const hasOptions = !!this.params.options + if (!hasOptions) { + this.params.options = pollFallback(this.params, 'options') + } + return this.params.options + }, + expiryAmount: { + get () { return pollFallback(this.params, 'expiryAmount') }, + set (newVal) { this.params.expiryAmount = newVal } + }, + expiryUnit: { + get () { return pollFallback(this.params, 'expiryUnit') }, + set (newVal) { this.params.expiryUnit = newVal } + }, pollLimits () { return this.$store.state.instance.pollLimits }, @@ -89,7 +108,6 @@ export default { deleteOption (index, event) { if (this.options.length > 2) { this.options.splice(index, 1) - this.updatePollToParent() } }, convertExpiryToUnit (unit, amount) { @@ -104,24 +122,6 @@ export default { Math.max(this.minExpirationInCurrentUnit, this.expiryAmount) this.expiryAmount = Math.min(this.maxExpirationInCurrentUnit, this.expiryAmount) - this.updatePollToParent() - }, - updatePollToParent () { - const expiresIn = this.convertExpiryFromUnit( - this.expiryUnit, - this.expiryAmount - ) - - const options = uniq(this.options.filter(option => option !== '')) - if (options.length < 2) { - this.$emit('update-poll', { error: this.$t('polls.not_enough_options') }) - return - } - this.$emit('update-poll', { - options, - multiple: this.pollType === 'multiple', - expiresIn - }) } } } diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 563dfb96..1ae98549 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -10,11 +10,13 @@ import StatusContent from '../status_content/status_content.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js' +import { pollFormToMasto } from 'src/services/poll/poll.service.js' import { reject, map, uniqBy, debounce } from 'lodash' 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 { @@ -55,6 +57,18 @@ const pxStringToNumber = (str) => { return Number(str.substring(0, str.length - 2)) } +const typeAndRefId = ({ replyTo, profileMention, statusId }) => { + if (replyTo) { + return ['reply', replyTo] + } else if (profileMention) { + return ['mention', profileMention] + } else if (statusId) { + return ['edit', statusId] + } else { + return ['new', ''] + } +} + const PostStatusForm = { props: [ 'statusId', @@ -79,6 +93,7 @@ const PostStatusForm = { 'disableSensitivityCheckbox', 'disableSubmit', 'disablePreview', + 'disableDraft', 'placeholder', 'maxHeight', 'postHandler', @@ -88,13 +103,15 @@ const PostStatusForm = { 'submitOnEnter', 'emojiPickerPlacement', 'optimisticPosting', - 'profileMention' + 'profileMention', + 'draftId' ], emits: [ 'posted', 'resize', 'mediaplay', - 'mediapause' + 'mediapause', + 'can-close' ], components: { MediaUpload, @@ -105,7 +122,8 @@ const PostStatusForm = { Select, Attachment, StatusContent, - Gallery + Gallery, + DraftCloser }, mounted () { this.updateIdempotencyKey() @@ -126,41 +144,54 @@ const PostStatusForm = { const { scopeCopy } = this.$store.getters.mergedConfig - if (this.replyTo || this.profileMention) { - const currentUser = this.$store.state.users.currentUser - statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) - } + const [statusType, refId] = typeAndRefId({ replyTo: this.replyTo, profileMention: this.profileMention && this.repliedUser?.id, statusId: this.statusId }) - const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct') - ? this.copyMessageScope - : this.$store.state.users.currentUser.default_scope + // If we are starting a new post, do not associate it with old drafts + let statusParams = !this.disableDraft && (this.draftId || statusType !== 'new') ? this.getDraft(statusType, refId) : null - const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig + if (!statusParams) { + if (statusType === 'reply' || statusType === 'mention') { + const currentUser = this.$store.state.users.currentUser + statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) + } - let statusParams = { - spoilerText: this.subject || '', - status: statusText, - nsfw: !!sensitiveByDefault, - files: [], - poll: {}, - mediaDescriptions: {}, - visibility: scope, - contentType - } + const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct') + ? this.copyMessageScope + : this.$store.state.users.currentUser.default_scope + + const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig - if (this.statusId) { - const statusContentType = this.statusContentType || contentType statusParams = { + type: statusType, + refId, spoilerText: this.subject || '', - status: this.statusText || '', - nsfw: this.statusIsSensitive || !!sensitiveByDefault, - files: this.statusFiles || [], - poll: this.statusPoll || {}, - mediaDescriptions: this.statusMediaDescriptions || {}, - visibility: this.statusScope || scope, - contentType: statusContentType, + status: statusText, + nsfw: !!sensitiveByDefault, + files: [], + poll: {}, + hasPoll: false, + mediaDescriptions: {}, + visibility: scope, + contentType, quoting: false } + + if (statusType === 'edit') { + const statusContentType = this.statusContentType || contentType + statusParams = { + type: statusType, + refId, + spoilerText: this.subject || '', + status: this.statusText || '', + nsfw: this.statusIsSensitive || !!sensitiveByDefault, + files: this.statusFiles || [], + poll: this.statusPoll || {}, + hasPoll: false, + mediaDescriptions: this.statusMediaDescriptions || {}, + visibility: this.statusScope || scope, + contentType: statusContentType + } + } } return { @@ -172,13 +203,14 @@ const PostStatusForm = { highlighted: 0, newStatus: statusParams, caret: 0, - pollFormVisible: false, showDropIcon: 'hide', dropStopTimeout: null, preview: null, previewLoading: false, emojiInputShown: false, - idempotencyKey: '' + idempotencyKey: '', + saveInhibited: true, + savable: false } }, computed: { @@ -293,6 +325,24 @@ const PostStatusForm = { return false }, + debouncedMaybeAutoSaveDraft () { + return debounce(this.maybeAutoSaveDraft, 3000) + }, + pollFormVisible () { + return this.newStatus.hasPoll + }, + shouldAutoSaveDraft () { + return this.$store.getters.mergedConfig.autoSaveDraft + }, + autoSaveState () { + if (this.savable) { + return this.$t('post_status.auto_save_saving') + } else if (this.newStatus.id) { + return this.$t('post_status.auto_save_saved') + } else { + return this.$t('post_status.auto_save_nothing_new') + } + }, ...mapGetters(['mergedConfig']), ...mapState({ mobileLayout: state => state.interface.mobileLayout @@ -304,15 +354,32 @@ const PostStatusForm = { handler () { this.statusChanged() } + }, + savable (val) { + // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#usage_notes + // MDN says we'd better add the beforeunload event listener only when needed, and remove it when it's no longer needed + if (val) { + this.addBeforeUnloadListener() + } else { + this.removeBeforeUnloadListener() + } } }, + beforeUnmount () { + this.maybeAutoSaveDraft() + this.removeBeforeUnloadListener() + }, methods: { statusChanged () { this.autoPreview() this.updateIdempotencyKey() + this.debouncedMaybeAutoSaveDraft() + this.savable = true + this.saveInhibited = false }, clearStatus () { const newStatus = this.newStatus + this.saveInhibited = true this.newStatus = { status: '', spoilerText: '', @@ -320,10 +387,10 @@ const PostStatusForm = { visibility: newStatus.visibility, contentType: newStatus.contentType, poll: {}, + hasPoll: false, mediaDescriptions: {}, quoting: false } - this.pollFormVisible = false this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile() this.clearPollForm() if (this.preserveFocus) { @@ -336,6 +403,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 } @@ -353,7 +421,7 @@ const PostStatusForm = { return } - const poll = this.pollFormVisible ? this.newStatus.poll : {} + const poll = this.newStatus.hasPoll ? pollFormToMasto(this.newStatus.poll) : {} if (this.pollContentError) { this.error = this.pollContentError return @@ -388,6 +456,7 @@ const PostStatusForm = { postHandler(postingOptions).then((data) => { if (!data.error) { + this.abandonDraft() this.clearStatus() this.$emit('posted', data) } else { @@ -632,7 +701,7 @@ const PostStatusForm = { this.newStatus.visibility = visibility }, togglePollForm () { - this.pollFormVisible = !this.pollFormVisible + this.newStatus.hasPoll = !this.newStatus.hasPoll }, setPoll (poll) { this.newStatus.poll = poll @@ -665,6 +734,78 @@ const PostStatusForm = { }, propsToNative (props) { return propsToNative(props) + }, + saveDraft () { + if (!this.disableDraft && + !this.saveInhibited) { + if (this.newStatus.status || + this.newStatus.files?.length || + this.newStatus.hasPoll) { + return this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus }) + .then(id => { + if (this.newStatus.id !== id) { + this.newStatus.id = id + } + this.savable = false + }) + } else if (this.newStatus.id) { + // There is a draft, but there is nothing in it, clear it + return this.abandonDraft() + .then(() => { + this.savable = false + }) + } + } + return Promise.resolve() + }, + maybeAutoSaveDraft () { + if (this.shouldAutoSaveDraft) { + this.saveDraft() + } + }, + abandonDraft () { + return this.$store.dispatch('abandonDraft', { id: this.newStatus.id }) + }, + getDraft (statusType, refId) { + const maybeDraft = this.$store.state.drafts.drafts[this.draftId] + if (this.draftId && maybeDraft) { + return maybeDraft + } else { + const existingDrafts = this.$store.getters.draftsByTypeAndRefId(statusType, refId) + + if (existingDrafts.length) { + return existingDrafts[0] + } + } + // 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') + }) + }, + addBeforeUnloadListener () { + this._beforeUnloadListener ||= () => { + this.saveDraft() + } + window.addEventListener('beforeunload', this._beforeUnloadListener) + }, + removeBeforeUnloadListener () { + if (this._beforeUnloadListener) { + window.removeEventListener('beforeunload', this._beforeUnloadListener) + } } } } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index e76b52a1..b7a169c5 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -262,7 +262,7 @@ v-if="pollsAvailable" ref="pollForm" :visible="pollFormVisible" - @update-poll="setPoll" + :params="newStatus.poll" />
+ + {{ autoSaveState }} + +
+ @@ -610,5 +628,9 @@ border-radius: var(--roundness); border: 2px dashed var(--text); } + + .auto-save-status { + align-self: center; + } } diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index ba6b1faa..3ccf025c 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -45,6 +45,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') || diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 3cc83b54..cc530996 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -476,6 +476,22 @@ {{ $t('settings.autocomplete_select_first') }} +
  • + + {{ $t('settings.auto_save_draft') }} + +
  • +
  • + + {{ $t('settings.unsaved_post_action') }} + +
  • diff --git a/src/components/settings_modal/tabs/style_tab/virtual_directives_tab.vue b/src/components/settings_modal/tabs/style_tab/virtual_directives_tab.vue index f151d3a8..158f0a70 100644 --- a/src/components/settings_modal/tabs/style_tab/virtual_directives_tab.vue +++ b/src/components/settings_modal/tabs/style_tab/virtual_directives_tab.vue @@ -72,9 +72,9 @@ :compact="true" /> state.instance.pleromaChatMessagesAvailable, supportsAnnouncements: state => state.announcements.supportsAnnouncements }), - ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']) + ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount', 'draftCount']) }, methods: { toggleDrawer () { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 91b10ea0..7dd6ff28 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -255,6 +255,27 @@ +
  • + + {{ $t('nav.drafts') }} + + {{ draftCount }} + + +