From 1aa0901471beb3f77feced1f2ac72726cb8aebfc Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 10 Mar 2023 11:20:06 -0500 Subject: [PATCH 01/34] Add basic draft saving --- .../post_status_form/post_status_form.js | 54 +++++++++++++++++-- src/main.js | 2 + src/modules/drafts.js | 35 ++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/modules/drafts.js diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 563dfb96..ccb1e2cc 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -55,6 +55,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', @@ -88,7 +100,8 @@ const PostStatusForm = { 'submitOnEnter', 'emojiPickerPlacement', 'optimisticPosting', - 'profileMention' + 'profileMention', + 'draftId' ], emits: [ 'posted', @@ -126,7 +139,9 @@ const PostStatusForm = { const { scopeCopy } = this.$store.getters.mergedConfig - if (this.replyTo || this.profileMention) { + const [statusType, refId] = typeAndRefId({ replyTo: this.replyTo, profileMention: this.profileMention, statusId: this.statusId }) + + if (statusType === 'reply' || statusType === 'mention') { const currentUser = this.$store.state.users.currentUser statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) } @@ -138,6 +153,8 @@ const PostStatusForm = { const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig let statusParams = { + type: statusType, + refId, spoilerText: this.subject || '', status: statusText, nsfw: !!sensitiveByDefault, @@ -148,9 +165,11 @@ const PostStatusForm = { contentType } - if (this.statusId) { + if (statusType === 'edit') { const statusContentType = this.statusContentType || contentType statusParams = { + type: statusType, + refId, spoilerText: this.subject || '', status: this.statusText || '', nsfw: this.statusIsSensitive || !!sensitiveByDefault, @@ -163,6 +182,21 @@ const PostStatusForm = { } } + console.debug('type and ref:', [statusType, refId]) + + const maybeDraft = this.$store.state.drafts.drafts[this.draftId] + if (this.draftId && maybeDraft) { + console.debug('current draft:', maybeDraft) + statusParams = maybeDraft + } else { + const existingDrafts = this.$store.getters.draftsByTypeAndRefId(statusType, refId) + + console.debug('existing drafts:', existingDrafts) + if (existingDrafts.length) { + statusParams = existingDrafts[0] + } + } + return { randomSeed: genRandomSeed(), dropFiles: [], @@ -293,6 +327,19 @@ const PostStatusForm = { return false }, + saveDraft () { + return debounce(() => { + if (this.newStatus.status) { + console.debug('Saving status', this.newStatus) + this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus }) + .then(id => { + if (this.newStatus.id !== id) { + this.newStatus.id = id + } + }) + } + }, 3000) + }, ...mapGetters(['mergedConfig']), ...mapState({ mobileLayout: state => state.interface.mobileLayout @@ -310,6 +357,7 @@ const PostStatusForm = { statusChanged () { this.autoPreview() this.updateIdempotencyKey() + this.saveDraft() }, clearStatus () { const newStatus = this.newStatus diff --git a/src/main.js b/src/main.js index 85eb1f4c..a2af7b19 100644 --- a/src/main.js +++ b/src/main.js @@ -24,6 +24,7 @@ import pollsModule from './modules/polls.js' import postStatusModule from './modules/postStatus.js' import editStatusModule from './modules/editStatus.js' import statusHistoryModule from './modules/statusHistory.js' +import draftsModule from './modules/drafts.js' import chatsModule from './modules/chats.js' import announcementsModule from './modules/announcements.js' @@ -96,6 +97,7 @@ const persistedStateOptions = { postStatus: postStatusModule, editStatus: editStatusModule, statusHistory: statusHistoryModule, + drafts: draftsModule, chats: chatsModule, announcements: announcementsModule }, diff --git a/src/modules/drafts.js b/src/modules/drafts.js new file mode 100644 index 00000000..465c9aad --- /dev/null +++ b/src/modules/drafts.js @@ -0,0 +1,35 @@ + +export const defaultState = { + drafts: {} +} + +export const mutations = { + addOrSaveDraft (state, { draft }) { + state.drafts[draft.id] = draft + } +} + +export const actions = { + addOrSaveDraft (store, { draft }) { + const id = draft.id || (new Date().getTime()).toString() + store.commit('addOrSaveDraft', { draft: { ...draft, id } }) + return id + } +} + +export const getters = { + draftsByTypeAndRefId (state) { + return (type, refId) => { + return Object.values(state.drafts).filter(draft => draft.type === type && draft.refId === refId) + } + } +} + +const drafts = { + state: defaultState, + mutations, + getters, + actions +} + +export default drafts From 1edada7e9dc05e2ae837acb9ea196e2f7e351e3a Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 10 Mar 2023 11:24:18 -0500 Subject: [PATCH 02/34] Save draft immediately before unmount --- .../post_status_form/post_status_form.js | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index ccb1e2cc..f8b8deb7 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -327,18 +327,8 @@ const PostStatusForm = { return false }, - saveDraft () { - return debounce(() => { - if (this.newStatus.status) { - console.debug('Saving status', this.newStatus) - this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus }) - .then(id => { - if (this.newStatus.id !== id) { - this.newStatus.id = id - } - }) - } - }, 3000) + debouncedSaveDraft () { + return debounce(this.saveDraft, 3000) }, ...mapGetters(['mergedConfig']), ...mapState({ @@ -353,11 +343,14 @@ const PostStatusForm = { } } }, + beforeUnmount () { + this.saveDraft() + }, methods: { statusChanged () { this.autoPreview() this.updateIdempotencyKey() - this.saveDraft() + this.debouncedSaveDraft() }, clearStatus () { const newStatus = this.newStatus @@ -713,6 +706,17 @@ const PostStatusForm = { }, propsToNative (props) { return propsToNative(props) + }, + saveDraft () { + if (this.newStatus.status) { + console.debug('Saving status', this.newStatus) + this.$store.dispatch('addOrSaveDraft', { draft: this.newStatus }) + .then(id => { + if (this.newStatus.id !== id) { + this.newStatus.id = id + } + }) + } } } } From 02e2e6b1bf635c8f39af2b40904bc64cc7166bf9 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 10 Mar 2023 12:10:39 -0500 Subject: [PATCH 03/34] Add minimal draft management tool --- src/boot/routes.js | 2 + src/components/draft/draft.js | 42 +++++++++ src/components/draft/draft.vue | 35 ++++++++ src/components/drafts/drafts.js | 17 ++++ src/components/drafts/drafts.vue | 24 +++++ src/components/nav_panel/nav_panel.js | 6 +- src/components/navigation/navigation.js | 5 ++ .../post_status_form/post_status_form.js | 90 ++++++++++--------- src/i18n/en.json | 7 +- src/modules/drafts.js | 3 + 10 files changed, 186 insertions(+), 45 deletions(-) create mode 100644 src/components/draft/draft.js create mode 100644 src/components/draft/draft.vue create mode 100644 src/components/drafts/drafts.js create mode 100644 src/components/drafts/drafts.vue diff --git a/src/boot/routes.js b/src/boot/routes.js index 31e3dbb0..fe076172 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' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { @@ -80,6 +81,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/draft/draft.js b/src/components/draft/draft.js new file mode 100644 index 00000000..1914024f --- /dev/null +++ b/src/components/draft/draft.js @@ -0,0 +1,42 @@ +import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' + +const Draft = { + components: { + PostStatusForm + }, + props: { + draft: { + type: Object, + required: true + } + }, + data () { + return { + editing: 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 + } + } + }, + methods: { + toggleEditing () { + this.editing = !this.editing + } + } +} + +export default Draft diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue new file mode 100644 index 00000000..5114da70 --- /dev/null +++ b/src/components/draft/draft.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/components/drafts/drafts.js b/src/components/drafts/drafts.js new file mode 100644 index 00000000..6703a48e --- /dev/null +++ b/src/components/drafts/drafts.js @@ -0,0 +1,17 @@ +import Draft from 'src/components/draft/draft.vue' +import List from 'src/components/list/list.vue' + +const Drafts = { + components: { + Draft, + List + }, + computed: { + drafts () { + console.debug('available drafts:', this.$store.getters.draftsArray) + 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/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 8c9c3b11..f8d551c7 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -19,7 +19,8 @@ import { faInfoCircle, faStream, faList, - faBullhorn + faBullhorn, + faFilePen } from '@fortawesome/free-solid-svg-icons' library.add( @@ -34,7 +35,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 face430e..41685cda 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -78,6 +78,11 @@ export const ROOT_ITEMS = { label: 'nav.announcements', badgeGetter: 'unreadAnnouncementCount', criteria: ['announcements'] + }, + drafts: { + route: 'drafts', + icon: 'file-pen', + label: 'nav.drafts' } } diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index f8b8deb7..f6e20066 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -141,59 +141,48 @@ const PostStatusForm = { const [statusType, refId] = typeAndRefId({ replyTo: this.replyTo, profileMention: this.profileMention, statusId: this.statusId }) - if (statusType === 'reply' || statusType === 'mention') { - const currentUser = this.$store.state.users.currentUser - statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) - } + let statusParams = this.getDraft(statusType, refId) - const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct') - ? this.copyMessageScope - : this.$store.state.users.currentUser.default_scope + if (!statusParams) { + if (statusType === 'reply' || statusType === 'mention') { + const currentUser = this.$store.state.users.currentUser + statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) + } - const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig + const scope = ((this.copyMessageScope && scopeCopy) || this.copyMessageScope === 'direct') + ? this.copyMessageScope + : this.$store.state.users.currentUser.default_scope - let statusParams = { - type: statusType, - refId, - spoilerText: this.subject || '', - status: statusText, - nsfw: !!sensitiveByDefault, - files: [], - poll: {}, - mediaDescriptions: {}, - visibility: scope, - contentType - } + const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig - 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 || {}, - mediaDescriptions: this.statusMediaDescriptions || {}, - visibility: this.statusScope || scope, - contentType: statusContentType, + status: statusText, + nsfw: !!sensitiveByDefault, + files: [], + poll: {}, + mediaDescriptions: {}, + visibility: scope, + contentType, quoting: false } - } - console.debug('type and ref:', [statusType, refId]) - - const maybeDraft = this.$store.state.drafts.drafts[this.draftId] - if (this.draftId && maybeDraft) { - console.debug('current draft:', maybeDraft) - statusParams = maybeDraft - } else { - const existingDrafts = this.$store.getters.draftsByTypeAndRefId(statusType, refId) - - console.debug('existing drafts:', existingDrafts) - if (existingDrafts.length) { - statusParams = existingDrafts[0] + 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 || {}, + mediaDescriptions: this.statusMediaDescriptions || {}, + visibility: this.statusScope || scope, + contentType: statusContentType + } } } @@ -717,6 +706,23 @@ const PostStatusForm = { } }) } + }, + getDraft (statusType, refId) { + console.debug('type and ref:', [statusType, refId]) + + const maybeDraft = this.$store.state.drafts.drafts[this.draftId] + if (this.draftId && maybeDraft) { + console.debug('current draft:', maybeDraft) + return maybeDraft + } else { + const existingDrafts = this.$store.getters.draftsByTypeAndRefId(statusType, refId) + + console.debug('existing drafts:', existingDrafts) + if (existingDrafts.length) { + return existingDrafts[0] + } + } + // No draft available, fall back } } } diff --git a/src/i18n/en.json b/src/i18n/en.json index 3f7ea282..71059df1 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -190,7 +190,8 @@ "mobile_notifications_close": "Close notifications", "mobile_notifications_mark_as_seen": "Mark all as seen", "announcements": "Announcements", - "quotes": "Quotes" + "quotes": "Quotes", + "drafts": "Drafts" }, "notifications": { "broken_favorite": "Unknown status, searching for it…", @@ -1401,5 +1402,9 @@ }, "unicode_domain_indicator": { "tooltip": "This domain contains non-ascii characters." + }, + "drafts": { + "drafts": "Drafts", + "continue": "Continue editing" } } diff --git a/src/modules/drafts.js b/src/modules/drafts.js index 465c9aad..81199c62 100644 --- a/src/modules/drafts.js +++ b/src/modules/drafts.js @@ -22,6 +22,9 @@ export const getters = { return (type, refId) => { return Object.values(state.drafts).filter(draft => draft.type === type && draft.refId === refId) } + }, + draftsArray (state) { + return Object.values(state.drafts) } } From a245379f43cf91d36cb4e5360a858f0cedb688a7 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 10 Mar 2023 12:39:08 -0500 Subject: [PATCH 04/34] Make it possible to abandon draft --- src/components/draft/draft.js | 24 +++++++++++++-- src/components/draft/draft.vue | 53 +++++++++++++++++++++++++++------- src/i18n/en.json | 7 ++++- src/modules/drafts.js | 6 ++++ 4 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 1914024f..8c4b662b 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -1,8 +1,10 @@ import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' +import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' const Draft = { components: { - PostStatusForm + PostStatusForm, + ConfirmModal }, props: { draft: { @@ -12,7 +14,8 @@ const Draft = { }, data () { return { - editing: false + editing: false, + showingConfirmDialog: false } }, computed: { @@ -35,6 +38,23 @@ const Draft = { methods: { toggleEditing () { this.editing = !this.editing + }, + abandon () { + this.showingConfirmDialog = true + }, + doAbandon () { + console.debug('abandoning') + this.$store.dispatch('abandonDraft', { id: this.draft.id }) + .then(() => { + this.hideConfirmDialog() + }) + }, + hideConfirmDialog () { + this.showingConfirmDialog = false + }, + handlePosted () { + console.debug('posted') + this.doAbandon() } } } diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue index 5114da70..dbea442b 100644 --- a/src/components/draft/draft.vue +++ b/src/components/draft/draft.vue @@ -1,28 +1,48 @@ @@ -31,5 +51,18 @@ diff --git a/src/i18n/en.json b/src/i18n/en.json index 71059df1..502be45f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1405,6 +1405,11 @@ }, "drafts": { "drafts": "Drafts", - "continue": "Continue editing" + "continue": "Continue editing", + "abandon": "Abandon draft", + "abandon_confirm_title": "Abandon confirmation", + "abandon_confirm": "Do you really want to abandon this draft?", + "abandon_confirm_accept_button": "Abandon", + "abandon_confirm_cancel_button": "Keep" } } diff --git a/src/modules/drafts.js b/src/modules/drafts.js index 81199c62..808e6837 100644 --- a/src/modules/drafts.js +++ b/src/modules/drafts.js @@ -6,6 +6,9 @@ export const defaultState = { export const mutations = { addOrSaveDraft (state, { draft }) { state.drafts[draft.id] = draft + }, + abandonDraft (state, { id }) { + delete state.drafts[id] } } @@ -14,6 +17,9 @@ export const actions = { const id = draft.id || (new Date().getTime()).toString() store.commit('addOrSaveDraft', { draft: { ...draft, id } }) return id + }, + abandonDraft (store, { id }) { + store.commit('abandonDraft', { id }) } } From 8a7f17ac9e9c58ef4b5a17bd26a5a762a06a8814 Mon Sep 17 00:00:00 2001 From: tusooa Date: Fri, 10 Mar 2023 13:22:44 -0500 Subject: [PATCH 05/34] Display information about replied-to/edited status --- src/components/draft/draft.js | 7 +++++- src/components/draft/draft.vue | 46 ++++++++++++++++++++++++++++------ src/i18n/en.json | 4 ++- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 8c4b662b..9b052606 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -1,10 +1,12 @@ import PostStatusForm from 'src/components/post_status_form/post_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, - ConfirmModal + ConfirmModal, + StatusContent }, props: { draft: { @@ -33,6 +35,9 @@ const Draft = { draftId: this.draft.id, ...this.relAttrs } + }, + refStatus () { + return this.draft.refId ? this.$store.state.statuses.allStatusesObject[this.draft.refId] : undefined } }, methods: { diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue index dbea442b..6e1b65bd 100644 --- a/src/components/draft/draft.vue +++ b/src/components/draft/draft.vue @@ -1,8 +1,5 @@