From 2467e5655688fff618ee3be7c34595fff52212a1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 16:16:05 +0300 Subject: [PATCH 01/21] follow requests module leftovers --- src/components/notification/notification.js | 6 ++---- src/components/side_drawer/side_drawer.js | 4 ---- src/stores/users.js | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 3c82f455c..34251d42d 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -148,8 +148,7 @@ const Notification = { id: this.user.id, credentials: useOAuthStore().token, }) - // TODO Fix this - this.$store.dispatch('removeFollowRequest', this.user) + useFollowRequestsStore().remove(this.user.id) useNotificationsStore().markSingleNotificationAsSeen(this.notification.id) this.hideApproveConfirmDialog() }, @@ -166,8 +165,7 @@ const Notification = { credentials: useOAuthStore().token, }).then(() => { useNotificationsStore().dismissNotificationLocal(this.notification.id) - // TODO Fix this - this.$store.dispatch('removeFollowRequest', this.user) + useFollowRequestsStore().remove(this.user.id) }) this.hideDenyConfirmDialog() }, diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 40d922228..f2702f430 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -62,10 +62,6 @@ const SideDrawer = { GestureService.DIRECTION_LEFT, this.toggleDrawer, ) - - if (this.currentUser?.locked) { - this.$store.dispatch('startFetchingFollowRequests') - } }, components: { UserCard, diff --git a/src/stores/users.js b/src/stores/users.js index c9445a65d..9d95c6b5e 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -689,7 +689,6 @@ export const useUsersStore = defineStore('users', { useBookmarkFoldersStore().startFetching() if (user.locked) { - dispatch('startFetchingFollowRequests') useFollowRequestsStore().startFetching() } @@ -739,8 +738,6 @@ export const useUsersStore = defineStore('users', { useFollowRequestsStore().stopFetching() } - store?.dispatch('stopFetchingFollowRequests') - // NOTE: No need to verify the app still exists, because if it doesn't, // the token will be invalid too return oauth @@ -798,7 +795,7 @@ export const useUsersStore = defineStore('users', { useListsStore().startFetching() useBookmarkFoldersStore().startFetching() useChatsStore().startFetching() - store?.dispatch('startFetchingFollowRequests') + useFollowRequestsStore().startFetching() }) .finally(() => { useNotificationsStore().resume() From 9fe49c6db1173528f3c46b26df0901c313050eaa Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 16:18:14 +0300 Subject: [PATCH 02/21] more leftovers --- src/stores/instance_capabilities.js | 4 ---- src/stores/users.js | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/stores/instance_capabilities.js b/src/stores/instance_capabilities.js index 67c04a4ad..a794ea21b 100644 --- a/src/stores/instance_capabilities.js +++ b/src/stores/instance_capabilities.js @@ -38,10 +38,6 @@ export const useInstanceCapabilitiesStore = defineStore( } this[capability] = value - - if (capability === 'shoutAvailable') { - window.vuex.dispatch('initializeSocket') - } }, }, }, diff --git a/src/stores/users.js b/src/stores/users.js index 9d95c6b5e..5548a14df 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -670,7 +670,7 @@ export const useUsersStore = defineStore('users', { useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 }) /**/ - if (user.token) { + if (user.token && useInstanceCapabilitiesStore().shoutAvailable) { // Shoutbox useShoutStore().initializeSocket() useShoutStore().initializeShout() From c4442f595d336aed82351aec497966614038060d Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 16:18:56 +0300 Subject: [PATCH 03/21] oops --- src/components/settings_modal/helpers/setting.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js index d0dce5950..a3fc5931e 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -413,8 +413,7 @@ export default { hardReset() { switch (this.realSource) { case 'admin': - return this.$store - .dispatch('resetAdminSetting', { path: this.path }) + return useAdminSettingsStore().resetAdminSetting({ path: this.path }) .then(() => { this.draft = this.state }) From 148ce1eb990c55f4da64b335e0695b10d20bf2c0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 16:58:31 +0300 Subject: [PATCH 04/21] migrate drafts store to pinia as-is --- src/components/draft/draft.js | 7 +- src/components/drafts/drafts.js | 6 +- .../post_status_form/post_status_form.js | 19 ++-- src/components/side_drawer/side_drawer.js | 4 +- src/modules/drafts.js | 99 ------------------- src/modules/index.js | 6 +- src/stores/drafts.js | 75 ++++++++++++++ src/stores/users.js | 3 +- 8 files changed, 96 insertions(+), 123 deletions(-) delete mode 100644 src/modules/drafts.js create mode 100644 src/stores/drafts.js diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 1c7f419c6..582381ec6 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -7,6 +7,7 @@ import StatusContent from 'src/components/status_content/status_content.vue' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useStatusesStore } from 'src/stores/statuses.js' +import { useDraftsStore } from 'src/stores/drafts.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faPollH } from '@fortawesome/free-solid-svg-icons' @@ -77,9 +78,9 @@ const Draft = { editing(newVal) { if (newVal) return if (this.safeToSave) { - this.$store.dispatch('addOrSaveDraft', { draft: this.draft }) + useDraftsStore().addOrSaveDraft(this.draft) } else { - this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft }) + useDraftsStore().addOrSaveDraft(this.referenceDraft) } }, }, @@ -91,7 +92,7 @@ const Draft = { this.showingConfirmDialog = true }, doAbandon() { - this.$store.dispatch('abandonDraft', { id: this.draft.id }).then(() => { + useDraftsStore().abandonDraft(this.draft.id).then(() => { this.hideConfirmDialog() }) }, diff --git a/src/components/drafts/drafts.js b/src/components/drafts/drafts.js index 3d93edb14..c2cac4f13 100644 --- a/src/components/drafts/drafts.js +++ b/src/components/drafts/drafts.js @@ -1,4 +1,5 @@ import { defineAsyncComponent } from 'vue' +import { useDraftsStore } from 'src/stores/drafts.js' import Draft from 'src/components/draft/draft.vue' import List from 'src/components/list/list.vue' @@ -18,7 +19,7 @@ const Drafts = { }, computed: { drafts() { - return this.$store.getters.draftsArray + return useDraftsStore().draftsArray }, }, methods: { @@ -26,8 +27,7 @@ const Drafts = { this.showingConfirmDialog = true }, doAbandonAll() { - this.$store - .dispatch('abandonAllDrafts') + useDraftsStore().abandonAllDrafts() .then(() => this.hideConfirmDialog()) }, hideConfirmDialog() { diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 4fb6f4746..2e303a338 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -32,6 +32,7 @@ import { useMediaViewerStore } from 'src/stores/media_viewer.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUsersStore } from 'src/stores/users.js' +import { useDraftsStore } from 'src/stores/drafts.js' import { pollFormToMasto } from 'src/services/poll/poll.service.js' @@ -985,13 +986,11 @@ const PostStatusForm = { saveDraft() { if (!this.disableDraft && !this.saveInhibited) { if (this.safeToSaveDraft) { - return this.$store - .dispatch('addOrSaveDraft', { - draft: { - type: this.statusType, - refId: this.refId, - ...this.newStatus, - }, + return useDraftsStore() + .addOrSaveDraft({ + type: this.statusType, + refId: this.refId, + ...this.newStatus, }) .then((id) => { if (this.newStatus.id !== id) { @@ -1024,14 +1023,14 @@ const PostStatusForm = { } }, abandonDraft() { - return this.$store.dispatch('abandonDraft', { id: this.draftId }) + return useDraftsStore().abandonDraft(this.draftId) }, getDraft() { - const maybeDraft = this.$store.state.drafts.drafts[this.draftId] + const maybeDraft = useDraftsStore().drafts.get(this.draftId) if (this.draftId && maybeDraft) { return maybeDraft } else { - const existingDrafts = this.$store.getters.draftsByTypeAndRefId( + const existingDrafts = useDraftsStore().draftsByTypeAndRefId( this.statusType, this.refId, ) diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index f2702f430..5949649a0 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -1,5 +1,4 @@ import { mapActions, mapState } from 'pinia' -import { mapGetters } from 'vuex' import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' import UserCard from 'src/components/user_card/user_card.vue' @@ -15,6 +14,7 @@ import { useInterfaceStore } from 'src/stores/interface' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useShoutStore } from 'src/stores/shout' import { useUsersStore } from 'src/stores/users.js' +import { useDraftsStore } from 'src/stores/drafts.js' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -110,7 +110,7 @@ const SideDrawer = { hideSitename: (store) => store.instanceIdentity.hideSitename, }), ...mapState(useChatsStore, ['unreadChatsCount']), - ...mapGetters(['draftCount']), + ...mapState(useDraftsStore, ['draftCount']), }, methods: { toggleDrawer() { diff --git a/src/modules/drafts.js b/src/modules/drafts.js deleted file mode 100644 index 3cde4f574..000000000 --- a/src/modules/drafts.js +++ /dev/null @@ -1,99 +0,0 @@ -import { storage } from 'src/lib/storage.js' - -export const defaultState = { - drafts: {}, -} - -export const mutations = { - addOrSaveDraft(state, { draft }) { - state.drafts[draft.id] = draft - }, - abandonDraft(state, { id }) { - delete state.drafts[id] - }, - loadDrafts(state, data) { - state.drafts = data - }, -} - -const storageKey = 'pleroma-fe-drafts' - -/* - * Note: we do not use the persist state plugin because - * it is not impossible for a user to have two windows at - * the same time. The persist state plugin is just overriding - * everything with the current state. This isn't good because - * if a draft is created in one window and another draft is - * created in another, the draft in the first window will just - * be overriden. - * Here, we can't guarantee 100% atomicity unless one uses - * different keys, which will just pollute the whole storage. - * It is indeed best to have backend support for this. - */ -const getStorageData = async () => - (await storage.getItem(storageKey)) || - { - /* no-op */ - } - -const saveDraftToStorage = async (draft) => { - const currentData = await getStorageData() - currentData[draft.id] = JSON.parse(JSON.stringify(draft)) - await storage.setItem(storageKey, currentData) -} - -const deleteDraftFromStorage = async (ids) => { - const currentData = await getStorageData() - ids.forEach((id) => { - delete currentData[id] - }) - await storage.setItem(storageKey, currentData) -} - -export const actions = { - async addOrSaveDraft(store, { draft }) { - const id = draft.id || new Date().getTime().toString() - const draftWithId = { ...draft, id } - store.commit('addOrSaveDraft', { draft: draftWithId }) - await saveDraftToStorage(draftWithId) - return id - }, - async abandonDraft(store, { id }) { - store.commit('abandonDraft', { id }) - await deleteDraftFromStorage([id]) - }, - async abandonAllDrafts(store) { - const ids = Object.keys(store.state.drafts) - ids.forEach((id) => store.commit('abandonDraft', { id })) - await deleteDraftFromStorage(ids) - }, - async loadDrafts(store) { - const currentData = await getStorageData() - store.commit('loadDrafts', currentData) - }, -} - -export const getters = { - draftsByTypeAndRefId(state) { - return (type, refId) => { - return Object.values(state.drafts).filter( - (draft) => draft.type === type && draft.refId === refId, - ) - } - }, - draftsArray(state) { - return Object.values(state.drafts) - }, - draftCount(state) { - return Object.values(state.drafts).length - }, -} - -const drafts = { - state: defaultState, - mutations, - getters, - actions, -} - -export default drafts diff --git a/src/modules/index.js b/src/modules/index.js index 436d7e844..b1c6ea436 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -1,5 +1 @@ -import drafts from './drafts.js' - -export default { - drafts, -} +export default {} diff --git a/src/stores/drafts.js b/src/stores/drafts.js new file mode 100644 index 000000000..bca8ccac0 --- /dev/null +++ b/src/stores/drafts.js @@ -0,0 +1,75 @@ +import { defineStore } from 'pinia' +import { storage } from 'src/lib/storage.js' + +const storageKey = 'pleroma-fe-drafts' + +/* + * Note: we do not use the persist state plugin because + * it is not impossible for a user to have two windows at + * the same time. The persist state plugin is just overriding + * everything with the current state. This isn't good because + * if a draft is created in one window and another draft is + * created in another, the draft in the first window will just + * be overriden. + * Here, we can't guarantee 100% atomicity unless one uses + * different keys, which will just pollute the whole storage. + * It is indeed best to have backend support for this. + */ +const getStorageData = async () => await storage.getItem(storageKey) + +const saveDraftToStorage = async (draft) => { + const currentData = await getStorageData() ?? {} + currentData[draft.id] = JSON.parse(JSON.stringify(draft)) + await storage.setItem(storageKey, currentData) +} + +const deleteDraftFromStorage = async (ids) => { + const currentData = await getStorageData() ?? {} + ids.forEach((id) => { + delete currentData[id] + }) + await storage.setItem(storageKey, currentData) +} + +export const useDraftsStore = defineStore('drafts', { + state: () => ({ + drafts: new Map() + }), + getters: { + draftsByTypeAndRefId(state) { + return (type, refId) => { + return [...state.drafts.values()].filter( + (draft) => draft.type === type && draft.refId === refId, + ) + } + }, + draftsArray(state) { + return [...state.drafts.values()] + }, + draftsCount(state) { + return state.drafts.size + }, + }, + actions: { + async abandonDraft(id) { + this.drafts.delete(id) + await deleteDraftFromStorage([id]) + }, + async loadDrafts() { + const currentData = await getStorageData() + this.drafts = new Map(Object.entries(currentData)) + }, + async addOrSaveDraft(draft) { + const id = draft.id ?? new Date().getTime().toString() + const draftWithId = { ...draft, id } + this.drafts.set(draft.id, draftWithId) + await saveDraftToStorage(draftWithId) + return id + }, + async abandonAllDrafts(store) { + const ids = this.drafts.keys() + ids.forEach((id) => this.abandonDraft(id)) + await deleteDraftFromStorage(ids) + }, + } +}) diff --git a/src/stores/users.js b/src/stores/users.js index 5548a14df..7471b9ab5 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -21,6 +21,7 @@ import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useProfileConfigStore } from 'src/stores/profile_config.js' import { useTimelinesStore } from 'src/stores/timelines.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' +import { useDraftsStore } from 'src/stores/drafts.js' import { revokeToken } from 'src/api/oauth.js' import { @@ -700,7 +701,7 @@ export const useUsersStore = defineStore('users', { useAnnouncementsStore().startFetching() this.fetchMutes() - dispatch('loadDrafts') + useDraftsStore().loadDrafts() } catch (error) { console.error(error) From 5b9027f17ae55e4bea3ae893d25c6aa8d5129249 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 16:59:13 +0300 Subject: [PATCH 05/21] fix navigation --- src/components/navigation/navigation.js | 8 ++++---- src/components/navigation/navigation_entry.js | 15 +++++++++++++-- src/components/navigation/navigation_entry.vue | 4 ++-- src/components/navigation/navigation_pins.js | 9 +++++++-- src/components/navigation/navigation_pins.vue | 2 +- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index 7a2e29646..7f4208162 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -76,7 +76,7 @@ export const ROOT_ITEMS = { icon: 'comments', label: 'nav.chats', badgeStyle: 'notification', - badgeGetter: 'unreadChatsCount', + badgeGetter: 'unreadChats', criteria: ['chats'], }, friendRequests: { @@ -85,7 +85,7 @@ export const ROOT_ITEMS = { label: 'nav.friend_requests', badgeStyle: 'notification', criteria: ['lockedUser'], - badgeGetter: 'followRequestsCount', + badgeGetter: 'followRequests', }, about: { route: 'about', @@ -99,7 +99,7 @@ export const ROOT_ITEMS = { label: 'nav.announcements', store: 'announcements', badgeStyle: 'notification', - badgeGetter: 'unreadAnnouncementCount', + badgeGetter: 'unreadAnnouncements', criteria: ['announcements'], }, drafts: { @@ -107,7 +107,7 @@ export const ROOT_ITEMS = { icon: 'file-pen', label: 'nav.drafts', badgeStyle: 'neutral', - badgeGetter: 'draftCount', + badgeGetter: 'drafts', }, } diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index 31bc28eb5..daa6284ff 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -6,6 +6,9 @@ import OptionalRouterLink from 'src/components/optional_router_link/optional_rou import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUsersStore } from 'src/stores/users.js' +import { useDraftsStore } from 'src/stores/drafts.js' +import { useChatsStore } from 'src/stores/chats.js' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faThumbtack } from '@fortawesome/free-solid-svg-icons' @@ -40,11 +43,19 @@ const NavigationEntry = { routeTo() { return routeTo(this.item, this.currentUser) }, - getters() { - return this.$store.getters + badges() { + return { + drafts: this.draftsCount, + unreadAnnouncements: this.unreadAnnouncementCount, + followRequests: this.followRequestsCount, + unreadChats: this.unreadChatsCount, + } }, ...mapStores(useAnnouncementsStore), + ...mapState(useDraftsStore, ['draftsCount']), ...mapState(useUsersStore, ['currentUser']), + ...mapState(useChatsStore, ['unreadChatsCount']), + ...mapState(useFollowRequestsStore, ['followRequestsCount']), ...mapState(useSyncConfigStore, { pinnedItems: (store) => new Set(store.prefsStorage.collections.pinnedNavItems), diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index 3ab0355b1..0528a2a7b 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -47,11 +47,11 @@
- {{ getters[item.badgeGetter] }} + {{ badges[item.badgeGetter] }}
From 7ae22bbb75394c2fbce91bb141003e04514b8ede Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 16:59:58 +0300 Subject: [PATCH 06/21] remove references to vuex --- src/components/chat_new/chat_new.js | 1 - src/components/edit_status_form/edit_status_form.js | 1 - src/components/media_upload/media_upload.js | 3 +-- src/components/post_status_form/post_status_form.js | 6 +----- .../status_action_buttons/status_action_buttons.js | 3 --- src/components/sticker_picker/sticker_picker.js | 3 +-- src/components/user_card/user_card.js | 1 - src/main.js | 1 - src/stores/users.js | 8 -------- test/unit/specs/components/chat_view.spec.js | 5 ----- 10 files changed, 3 insertions(+), 29 deletions(-) diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js index 4f015db6a..03960a287 100644 --- a/src/components/chat_new/chat_new.js +++ b/src/components/chat_new/chat_new.js @@ -71,7 +71,6 @@ const chatNew = { this.loading = true this.userIds = [] - this.$store useSearchStore() .search({ q: query, resolve: true, type: 'accounts' }) .then((data) => { diff --git a/src/components/edit_status_form/edit_status_form.js b/src/components/edit_status_form/edit_status_form.js index f7fd3d04d..30bb98590 100644 --- a/src/components/edit_status_form/edit_status_form.js +++ b/src/components/edit_status_form/edit_status_form.js @@ -17,7 +17,6 @@ const EditStatusForm = { }, doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) { const params = { - store: this.$store, statusId: this.params.statusId, status, spoilerText, diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js index 2cb6a96e6..d3867b1d0 100644 --- a/src/components/media_upload/media_upload.js +++ b/src/components/media_upload/media_upload.js @@ -122,7 +122,6 @@ const mediaUpload = { }, async uploadFile(file) { const self = this - const store = this.$store if (file.size > useInstanceStore().uploadlimit) { const filesize = fileSizeFormatService.fileSizeFormat(file.size) const allowedsize = fileSizeFormatService.fileSizeFormat( @@ -145,7 +144,7 @@ const mediaUpload = { self.$emit('uploading') self.uploadCount++ - statusPosterService.uploadMedia({ store, formData }).then( + statusPosterService.uploadMedia({ formData }).then( (fileData) => { self.$emit('uploaded', fileData) self.decreaseUploadCount() diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 2e303a338..a261f9937 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -401,8 +401,6 @@ const PostStatusForm = { contentType: this.newStatus.contentType, poll, idempotencyKey: this.idempotencyKey, - - store: this.$store, } }, @@ -413,7 +411,6 @@ const PostStatusForm = { ...useEmojiStore().standardEmojiList, ...useEmojiStore().customEmoji, ], - store: this.$store, }) }, emojiSuggestor() { @@ -575,7 +572,7 @@ const PostStatusForm = { ...mapState(useUsersStore, ['currentUser']), ...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useInterfaceStore, { - mobileLayout: (store) => store.mobileLayout, + mobileLayout: (state) => state.mobileLayout, }), }, watch: { @@ -752,7 +749,6 @@ const PostStatusForm = { const description = this.newStatus.mediaDescriptions[id] if (!description || description.trim() === '') return return statusPoster.setMediaDescription({ - store: this.$store, id, description, }) diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js index bbbdb79e5..9f2720162 100644 --- a/src/components/status_action_buttons/status_action_buttons.js +++ b/src/components/status_action_buttons/status_action_buttons.js @@ -97,9 +97,6 @@ const StatusActionButtons = { replying: this.replying, emojiPickerShown: this.emojiPickerShown, emit: this.$emit, - dispatch: this.$store.dispatch, - state: this.$store.state, - getters: this.$store.getters, router: this.$router, currentUser: this.currentUser, loggedIn: !!this.currentUser, diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js index 482aacb81..4d7d1d698 100644 --- a/src/components/sticker_picker/sticker_picker.js +++ b/src/components/sticker_picker/sticker_picker.js @@ -29,14 +29,13 @@ const StickerPicker = { } }, pick(sticker, name) { - const store = this.$store // TODO remove this workaround by finding a way to bypass reuploads fetch(sticker).then((res) => { res.blob().then((blob) => { const file = new File([blob], name, { mimetype: 'image/png' }) const formData = new FormData() formData.append('file', file) - statusPosterService.uploadMedia({ store, formData }).then( + statusPosterService.uploadMedia({ formData }).then( (fileData) => { this.$emit('uploaded', fileData) this.clear() diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 739c1579b..40fdd7413 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -418,7 +418,6 @@ export default { ...useEmojiStore().standardEmojiList, ...useEmojiStore().customEmoji, ], - store: this.$store, }) }, emojiSuggestor() { diff --git a/src/main.js b/src/main.js index 735725250..bb76a1cf0 100644 --- a/src/main.js +++ b/src/main.js @@ -102,7 +102,6 @@ const persistedStateOptions = { strict: false, // Socket modifies itself, let's ignore this for now. // strict: process.env.NODE_ENV !== 'production' }) - window.vuex = store // Temporarily passing pinia and vuex stores along with storageError result until migration is fully complete. return await afterStoreSetup({ pinia, store, storageError, i18n }) } catch (e) { diff --git a/src/stores/users.js b/src/stores/users.js index 7471b9ab5..e713bb4c6 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -612,13 +612,6 @@ export const useUsersStore = defineStore('users', { // Login/Logout async loginUser(accessToken) { - const store = window.vuex - const dispatch = - store?.dispatch ?? - (() => { - /* no-op */ - }) // for tests - this.loggingIn = true try { @@ -723,7 +716,6 @@ export const useUsersStore = defineStore('users', { } }, logout() { - const store = window.vuex const oauth = useOAuthStore() // Pause fetching diff --git a/test/unit/specs/components/chat_view.spec.js b/test/unit/specs/components/chat_view.spec.js index ba06b6b4c..dd95b1fd3 100644 --- a/test/unit/specs/components/chat_view.spec.js +++ b/test/unit/specs/components/chat_view.spec.js @@ -29,11 +29,6 @@ const message3 = { const global = { mocks: { - $store: { - state: { - api: {}, - }, - }, $route: { params: { recipient_id: 2, From 365e7c9235d532a923f3996254eca1461d93319c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 17:06:59 +0300 Subject: [PATCH 07/21] remove vuex --- package.json | 3 +- src/lib/persisted_state.js | 67 ------------------------------------- src/main.js | 27 ++------------- test/fixtures/mock_store.js | 22 ------------ yarn.lock | 9 +---- 5 files changed, 5 insertions(+), 123 deletions(-) delete mode 100644 test/fixtures/mock_store.js diff --git a/package.json b/package.json index c10f86b84..60f65b129 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,7 @@ "vue": "3.5.22", "vue-i18n": "11", "vue-router": "4.6.4", - "vue-virtual-scroller": "^2.0.0-beta.7", - "vuex": "4.1.0" + "vue-virtual-scroller": "^2.0.0-beta.7" }, "devDependencies": { "@babel/core": "7.28.5", diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index a4dfe7d00..d4e0b1de2 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -27,73 +27,6 @@ const defaultStorage = (() => { return storage })() -export default function createPersistedState({ - key = 'vuex-lz', - paths = [], - getState = (key, storage) => { - const value = storage.getItem(key) - return value - }, - setState = (key, state, storage) => { - if (!loaded) { - console.info('waiting for old state to be loaded...') - return Promise.resolve() - } else { - return storage.setItem(key, state) - } - }, - reducer = defaultReducer, - storage = defaultStorage, - subscriber = (store) => (handler) => store.subscribe(handler), -} = {}) { - return getState(key, storage).then((savedState) => { - return (store) => { - try { - if (savedState !== null && typeof savedState === 'object') { - // build user cache - const usersState = savedState.users || {} - usersState.usersObject = {} - const users = usersState.users || [] - each(users, (user) => { - usersState.usersObject[user.id] = user - }) - savedState.users = usersState - - store.replaceState(merge({}, store.state, savedState)) - } - loaded = true - } catch (e) { - console.error("Couldn't load state") - console.error(e) - loaded = true - } - subscriber(store)((mutation, state) => { - try { - if (saveImmedeatelyActions.includes(mutation.type)) { - setState(key, reducer(cloneDeep(state), paths), storage).then( - (success) => { - if (success !== undefined) { - if (mutation.type === 'setOption') { - useInterfaceStore().settingsSaved({ success }) - } - } - }, - (error) => { - if (mutation.type === 'setOption') { - useInterfaceStore().settingsSaved({ error }) - } - }, - ) - } - } catch (e) { - console.error("Couldn't persist state:") - console.error(e) - } - }) - } - }) -} - /** * This persists state for pinia, which falls back to read from the vuex state * if pinia persisted state does not exist. diff --git a/src/main.js b/src/main.js index bb76a1cf0..f3615d955 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,6 @@ /* global process */ import { createPinia } from 'pinia' -import { createStore } from 'vuex' import 'custom-event-polyfill' import './lib/event_target_polyfill.js' @@ -17,11 +16,8 @@ import { createI18n } from 'vue-i18n' import afterStoreSetup from './boot/after_store.js' import messages from './i18n/messages.js' -import createPersistedState, { - piniaPersistPlugin, -} from './lib/persisted_state.js' +import { piniaPersistPlugin } from './lib/persisted_state.js' import { piniaPushNotificationsPlugin } from './lib/push_notifications_plugin.js' -import vuexModules from './modules/index.js' import { piniaLanguagePlugin } from 'src/lib/language.js' import { piniaStylePlugin } from 'src/lib/style.js' @@ -69,20 +65,12 @@ const persistedStateOptions = { try { let storageError - const plugins = [] const pinia = createPinia() pinia.use(piniaPersistPlugin()) pinia.use(piniaLanguagePlugin) pinia.use(piniaStylePlugin) pinia.use(piniaPushNotificationsPlugin) - try { - const persistedState = await createPersistedState(persistedStateOptions) - plugins.push(persistedState) - } catch (e) { - console.error('Storage error', e) - storageError = e - } document.querySelector('#splash').classList.remove('initial-hidden') document.querySelector('#mascot').src = `/static/pleromatan_apology${isFox}_small.webp` @@ -93,17 +81,8 @@ const persistedStateOptions = { 'update.art_by', { linkToArtist: 'pipivovott' }, ) - const store = createStore({ - modules: vuexModules, - plugins, - options: { - devtools: process.env.NODE_ENV !== 'production', - }, - strict: false, // Socket modifies itself, let's ignore this for now. - // strict: process.env.NODE_ENV !== 'production' - }) - // Temporarily passing pinia and vuex stores along with storageError result until migration is fully complete. - return await afterStoreSetup({ pinia, store, storageError, i18n }) + // Temporarily passing pinia stores along with storageError result until migration is fully complete. + return await afterStoreSetup({ pinia, storageError, i18n }) } catch (e) { splashError(i18n, e) } diff --git a/test/fixtures/mock_store.js b/test/fixtures/mock_store.js deleted file mode 100644 index b3380834c..000000000 --- a/test/fixtures/mock_store.js +++ /dev/null @@ -1,22 +0,0 @@ -import { cloneDeep } from 'lodash' -import { createStore } from 'vuex' - -import vuexModules from 'src/modules/index.js' - -const tweakModules = (modules) => { - const res = {} - Object.entries(modules).forEach(([name, module]) => { - const m = { ...module } - m.state = cloneDeep(module.state) - res[name] = m - }) - return res -} - -const makeMockStore = () => { - return createStore({ - modules: tweakModules(vuexModules), - }) -} - -export default makeMockStore diff --git a/yarn.lock b/yarn.lock index 4913bcfb7..364949865 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2877,7 +2877,7 @@ "@vue/compiler-dom" "3.5.35" "@vue/shared" "3.5.35" -"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.4": +"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.4": version "6.6.4" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== @@ -9190,13 +9190,6 @@ vue@^3.5.35: "@vue/server-renderer" "3.5.35" "@vue/shared" "3.5.35" -vuex@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.1.0.tgz#aa1b3ea5c7385812b074c86faeeec2217872e36c" - integrity sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ== - dependencies: - "@vue/devtools-api" "^6.0.0-beta.11" - w3c-xmlserializer@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" From 1aff1ac4e7553246554868e7b571a98562d2fd35 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 17:09:41 +0300 Subject: [PATCH 08/21] doc updated --- docs/HACKING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/HACKING.md b/docs/HACKING.md index 88760b77a..8d3c09a0c 100644 --- a/docs/HACKING.md +++ b/docs/HACKING.md @@ -81,7 +81,7 @@ In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/src/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation. -For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters. +For most part, PleromaFE tries to store all the info it can get in global pinia store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters. PleromaFE also tries to persist this store, however only stable data is stored, such as user authentication and preferences, user highlights. Persistence is performed by saving and loading chunk of vuex store in browser's LocalStorage/IndexedDB. From c611788e7773eb5ed97191e08ca311fb9215d82f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 17:26:08 +0300 Subject: [PATCH 09/21] cleanup --- src/api/user.js | 7 +- src/components/draft/draft.js | 10 ++- src/components/drafts/drafts.js | 6 +- src/components/navigation/navigation_entry.js | 6 +- .../post_status_form/post_status_form.js | 2 +- .../settings_modal/helpers/setting.js | 5 +- .../settings_modal/tabs/composing_tab.js | 2 +- src/components/side_drawer/side_drawer.js | 2 +- src/lib/persisted_state.js | 15 +--- src/main.js | 4 - src/stores/drafts.js | 9 ++- src/stores/profile_config.js | 4 +- src/stores/users.js | 4 +- test/fixtures/setup_test.js | 18 +---- test/unit/specs/components/draft.spec.js | 2 +- .../specs/components/post_status_form.spec.js | 2 +- .../specs/components/rich_content.spec.js | 2 +- test/unit/specs/stores/sync_config.spec.js | 80 ++++--------------- vite.config.js | 1 + 19 files changed, 59 insertions(+), 122 deletions(-) diff --git a/src/api/user.js b/src/api/user.js index 430b2c895..d3a2a4a70 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -17,8 +17,11 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' const MOVE_ACCOUNT_URL = '/api/pleroma/move_account' const ALIASES_URL = '/api/pleroma/aliases' -const NOTIFICATION_SETTINGS_URL = ({ blockFromStrangers, hideNotificationContents }) => - `/api/pleroma/notification_settings${paramsString({ blockFromStrangers, hideNotificationContents })}` +const NOTIFICATION_SETTINGS_URL = ({ + blockFromStrangers, + hideNotificationContents, +}) => + `/api/pleroma/notification_settings${paramsString({ blockFromStrangers, hideNotificationContents })}` export const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 582381ec6..5f17738d0 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -5,9 +5,9 @@ import Gallery from 'src/components/gallery/gallery.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import StatusContent from 'src/components/status_content/status_content.vue' +import { useDraftsStore } from 'src/stores/drafts.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useStatusesStore } from 'src/stores/statuses.js' -import { useDraftsStore } from 'src/stores/drafts.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faPollH } from '@fortawesome/free-solid-svg-icons' @@ -92,9 +92,11 @@ const Draft = { this.showingConfirmDialog = true }, doAbandon() { - useDraftsStore().abandonDraft(this.draft.id).then(() => { - this.hideConfirmDialog() - }) + useDraftsStore() + .abandonDraft(this.draft.id) + .then(() => { + this.hideConfirmDialog() + }) }, hideConfirmDialog() { this.showingConfirmDialog = false diff --git a/src/components/drafts/drafts.js b/src/components/drafts/drafts.js index c2cac4f13..3a37c0862 100644 --- a/src/components/drafts/drafts.js +++ b/src/components/drafts/drafts.js @@ -1,9 +1,10 @@ import { defineAsyncComponent } from 'vue' -import { useDraftsStore } from 'src/stores/drafts.js' import Draft from 'src/components/draft/draft.vue' import List from 'src/components/list/list.vue' +import { useDraftsStore } from 'src/stores/drafts.js' + const Drafts = { components: { Draft, @@ -27,7 +28,8 @@ const Drafts = { this.showingConfirmDialog = true }, doAbandonAll() { - useDraftsStore().abandonAllDrafts() + useDraftsStore() + .abandonAllDrafts() .then(() => this.hideConfirmDialog()) }, hideConfirmDialog() { diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index daa6284ff..49392b37a 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -4,11 +4,11 @@ import { routeTo } from 'src/components/navigation/navigation.js' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' import { useAnnouncementsStore } from 'src/stores/announcements.js' +import { useChatsStore } from 'src/stores/chats.js' +import { useDraftsStore } from 'src/stores/drafts.js' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUsersStore } from 'src/stores/users.js' -import { useDraftsStore } from 'src/stores/drafts.js' -import { useChatsStore } from 'src/stores/chats.js' -import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faThumbtack } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index a261f9937..b708d3f68 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -24,6 +24,7 @@ import { findOffset } from '../../services/offset_finder/offset_finder.service.j import genRandomSeed from '../../services/random_seed/random_seed.service.js' import statusPoster from '../../services/status_poster/status_poster.service.js' +import { useDraftsStore } from 'src/stores/drafts.js' import { useEmojiStore } from 'src/stores/emoji.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' @@ -32,7 +33,6 @@ import { useMediaViewerStore } from 'src/stores/media_viewer.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUsersStore } from 'src/stores/users.js' -import { useDraftsStore } from 'src/stores/drafts.js' import { pollFormToMasto } from 'src/services/poll/poll.service.js' diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js index a3fc5931e..31afc9068 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -8,8 +8,8 @@ import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useLocalConfigStore } from 'src/stores/local_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useProfileConfigStore } from 'src/stores/profile_config.js' +import { useSyncConfigStore } from 'src/stores/sync_config.js' export default { components: { @@ -413,7 +413,8 @@ export default { hardReset() { switch (this.realSource) { case 'admin': - return useAdminSettingsStore().resetAdminSetting({ path: this.path }) + return useAdminSettingsStore() + .resetAdminSetting({ path: this.path }) .then(() => { this.draft = this.state }) diff --git a/src/components/settings_modal/tabs/composing_tab.js b/src/components/settings_modal/tabs/composing_tab.js index 74f01a2e1..7701d1673 100644 --- a/src/components/settings_modal/tabs/composing_tab.js +++ b/src/components/settings_modal/tabs/composing_tab.js @@ -16,8 +16,8 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useOAuthStore } from 'src/stores/oauth.js' -import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useProfileConfigStore } from 'src/stores/profile_config.js' +import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUsersStore } from 'src/stores/users.js' import { updateProfile } from 'src/api/user.js' diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 5949649a0..fb0152ece 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -7,6 +7,7 @@ import { unseenNotifications } from '../../services/notification_utils/notificat import { useAnnouncementsStore } from 'src/stores/announcements' import { useChatsStore } from 'src/stores/chats.js' +import { useDraftsStore } from 'src/stores/drafts.js' import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' @@ -14,7 +15,6 @@ import { useInterfaceStore } from 'src/stores/interface' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useShoutStore } from 'src/stores/shout' import { useUsersStore } from 'src/stores/users.js' -import { useDraftsStore } from 'src/stores/drafts.js' import { library } from '@fortawesome/fontawesome-svg-core' import { diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index d4e0b1de2..5a698ac8e 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -1,11 +1,7 @@ -import { cloneDeep, each, get, merge, set } from 'lodash' +import { cloneDeep, get, set } from 'lodash' import { storage } from './storage.js' -import { useInterfaceStore } from 'src/stores/interface' - -let loaded = false - const defaultReducer = (state, paths) => paths.length === 0 ? state @@ -14,15 +10,6 @@ const defaultReducer = (state, paths) => return substate }, {}) -const saveImmedeatelyActions = [ - 'markNotificationsAsSeen', - 'setHighlight', - 'setOption', - 'setClientData', - 'setToken', - 'clearToken', -] - const defaultStorage = (() => { return storage })() diff --git a/src/main.js b/src/main.js index f3615d955..953346427 100644 --- a/src/main.js +++ b/src/main.js @@ -33,10 +33,6 @@ const i18n = createI18n({ messages.setLanguage(i18n.global, currentLocale) -const persistedStateOptions = { - paths: ['oauth', 'config'], -} - ;(async () => { const isFox = Math.floor(Math.random() * 2) > 0 ? '_fox' : '' diff --git a/src/stores/drafts.js b/src/stores/drafts.js index bca8ccac0..8325fba5b 100644 --- a/src/stores/drafts.js +++ b/src/stores/drafts.js @@ -1,4 +1,5 @@ import { defineStore } from 'pinia' + import { storage } from 'src/lib/storage.js' const storageKey = 'pleroma-fe-drafts' @@ -18,13 +19,13 @@ const storageKey = 'pleroma-fe-drafts' const getStorageData = async () => await storage.getItem(storageKey) const saveDraftToStorage = async (draft) => { - const currentData = await getStorageData() ?? {} + const currentData = (await getStorageData()) ?? {} currentData[draft.id] = JSON.parse(JSON.stringify(draft)) await storage.setItem(storageKey, currentData) } const deleteDraftFromStorage = async (ids) => { - const currentData = await getStorageData() ?? {} + const currentData = (await getStorageData()) ?? {} ids.forEach((id) => { delete currentData[id] }) @@ -33,7 +34,7 @@ const deleteDraftFromStorage = async (ids) => { export const useDraftsStore = defineStore('drafts', { state: () => ({ - drafts: new Map() + drafts: new Map(), }), getters: { draftsByTypeAndRefId(state) { @@ -71,5 +72,5 @@ export const useDraftsStore = defineStore('drafts', { ids.forEach((id) => this.abandonDraft(id)) await deleteDraftFromStorage(ids) }, - } + }, }) diff --git a/src/stores/profile_config.js b/src/stores/profile_config.js index 7533575d0..5d43ea446 100644 --- a/src/stores/profile_config.js +++ b/src/stores/profile_config.js @@ -97,7 +97,9 @@ export const settingsMap = { } export const defaultState = () => ({ - config: Object.fromEntries(Object.keys(settingsMap).map((key) => [key, null])) + config: Object.fromEntries( + Object.keys(settingsMap).map((key) => [key, null]), + ), }) export const useProfileConfigStore = defineStore('profileConfig', { diff --git a/src/stores/users.js b/src/stores/users.js index e713bb4c6..7093e9e03 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -5,6 +5,7 @@ import { defineStore } from 'pinia' import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' import { useChatsStore } from 'src/stores/chats.js' +import { useDraftsStore } from 'src/stores/drafts.js' import { useEmojiStore } from 'src/stores/emoji.js' import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' @@ -14,14 +15,13 @@ import { useListsStore } from 'src/stores/lists.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useNotificationsStore } from 'src/stores/notifications.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { useProfileConfigStore } from 'src/stores/profile_config.js' import { useShoutStore } from 'src/stores/shout.js' import { useStatusesStore } from 'src/stores/statuses.js' import { useStreamingStore } from 'src/stores/streaming.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' -import { useProfileConfigStore } from 'src/stores/profile_config.js' import { useTimelinesStore } from 'src/stores/timelines.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' -import { useDraftsStore } from 'src/stores/drafts.js' import { revokeToken } from 'src/api/oauth.js' import { diff --git a/test/fixtures/setup_test.js b/test/fixtures/setup_test.js index 3da0231a4..24acbdc4c 100644 --- a/test/fixtures/setup_test.js +++ b/test/fixtures/setup_test.js @@ -5,26 +5,15 @@ import VueVirtualScroller from 'vue-virtual-scroller' import RichContent from 'src/components/rich_content/rich_content.jsx' import Status from 'src/components/status/status.vue' import StillImage from 'src/components/still-image/still-image.vue' -import makeMockStore from './mock_store' import routes from 'src/boot/routes' export const $t = (msg) => msg const $i18n = { t: (msg) => msg } -const applyAfterStore = (store, afterStore) => { - afterStore(store) - return store -} - -const getDefaultOpts = ({ - afterStore = () => { - /* no-op */ - }, -} = {}) => ({ +const getDefaultOpts = () => ({ global: { plugins: [ - applyAfterStore(makeMockStore(), afterStore), VueVirtualScroller, createRouter({ history: createMemoryHistory(), @@ -87,9 +76,8 @@ const customBehaviors = () => { config.plugins.VueWrapper.install(customBehaviors) -export const mountOpts = (allOpts = {}) => { - const { afterStore, ...opts } = allOpts - const defaultOpts = getDefaultOpts({ afterStore }) +export const mountOpts = (opts = {}) => { + const defaultOpts = getDefaultOpts() const mergedOpts = { ...opts, global: { diff --git a/test/unit/specs/components/draft.spec.js b/test/unit/specs/components/draft.spec.js index 40d63419b..9e26e36b3 100644 --- a/test/unit/specs/components/draft.spec.js +++ b/test/unit/specs/components/draft.spec.js @@ -1,10 +1,10 @@ import { createTestingPinia } from '@pinia/testing' import { flushPromises, mount } from '@vue/test-utils' import { setActivePinia } from 'pinia' +import { $t, mountOpts, waitForEvent } from 'test/fixtures/setup_test.js' import { nextTick } from 'vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' -import { $t, mountOpts, waitForEvent } from '../../../fixtures/setup_test' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useUsersStore } from 'src/stores/users.js' diff --git a/test/unit/specs/components/post_status_form.spec.js b/test/unit/specs/components/post_status_form.spec.js index 981a6d020..1fddf4a95 100644 --- a/test/unit/specs/components/post_status_form.spec.js +++ b/test/unit/specs/components/post_status_form.spec.js @@ -1,10 +1,10 @@ import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' import { setActivePinia } from 'pinia' +import { mountOpts } from 'test/fixtures/setup_test.js' import { vi } from 'vitest' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' -import { mountOpts } from '../../../fixtures/setup_test' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js index 19bfdc035..c20e4fe43 100644 --- a/test/unit/specs/components/rich_content.spec.js +++ b/test/unit/specs/components/rich_content.spec.js @@ -1,9 +1,9 @@ import { createTestingPinia } from '@pinia/testing' import { mount, shallowMount } from '@vue/test-utils' import { setActivePinia } from 'pinia' +import { mountOpts } from 'test/fixtures/setup_test.js' import RichContent from 'src/components/rich_content/rich_content.jsx' -import { mountOpts } from '../../../fixtures/setup_test' const attentions = [] diff --git a/test/unit/specs/stores/sync_config.spec.js b/test/unit/specs/stores/sync_config.spec.js index 3618068f7..cd1cc8904 100644 --- a/test/unit/specs/stores/sync_config.spec.js +++ b/test/unit/specs/stores/sync_config.spec.js @@ -37,10 +37,7 @@ describe('The SyncConfig store', () => { it('should initialize storage if none present', async () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig({ ...user }) expect(store.cache._version).to.eql(VERSION) expect(store.cache._timestamp).to.be.a('number') @@ -50,10 +47,7 @@ describe('The SyncConfig store', () => { it('should initialize storage with proper flags for new users if none present', async () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig({ ...user, created_at: new Date() }) expect(store.cache._version).to.eql(VERSION) expect(store.cache._timestamp).to.be.a('number') @@ -63,10 +57,7 @@ describe('The SyncConfig store', () => { it('should merge flags even if remote timestamp is older', async () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() store.cache = { _timestamp: Date.now(), _version: VERSION, @@ -96,10 +87,7 @@ describe('The SyncConfig store', () => { it('should trim journal to 500 entries', async () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() store.cache = { _timestamp: Date.now(), _version: VERSION, @@ -138,10 +126,7 @@ describe('The SyncConfig store', () => { it('should reset local timestamp to remote if contents are the same', async () => { const store = useSyncConfigStore() store.cache = null - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig({ ...user, @@ -161,10 +146,7 @@ describe('The SyncConfig store', () => { it('should use remote version if local missing', async () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig(store, user) expect(store.cache._version).to.eql(VERSION) expect(store.cache._timestamp).to.be.a('number') @@ -208,9 +190,7 @@ describe('The SyncConfig store', () => { }) vi.spyOn(storage, 'setItem').mockResolvedValue() const store = useSyncConfigStore() - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig({ ...user, @@ -240,9 +220,7 @@ describe('The SyncConfig store', () => { }) vi.spyOn(storage, 'setItem').mockResolvedValue() const store = useSyncConfigStore() - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig({ ...user, @@ -282,9 +260,7 @@ describe('The SyncConfig store', () => { vi.spyOn(storage, 'setItem').mockResolvedValue() const store = useSyncConfigStore() const setPreference = vi.spyOn(store, 'setPreference') - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig({ ...user, @@ -318,9 +294,7 @@ describe('The SyncConfig store', () => { }) vi.spyOn(storage, 'setItem').mockResolvedValue() const store = useSyncConfigStore() - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig({ ...user, @@ -357,9 +331,7 @@ describe('The SyncConfig store', () => { const localStore = useLocalConfigStore() localStore.set({ path: 'fontInterface', value: 'Current interface' }) const store = useSyncConfigStore() - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() await store.initSyncConfig({ ...user }) @@ -372,10 +344,7 @@ describe('The SyncConfig store', () => { describe('setPreference', () => { it('should set preference and update journal log accordingly', () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() store.setPreference({ path: 'simple.palette', value: '1' }) expect(store.prefsStorage.simple.palette).to.eql('1') expect(store.prefsStorage._journal).to.have.length(1) @@ -390,10 +359,7 @@ describe('The SyncConfig store', () => { it('should keep journal to a minimum', () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() store.setPreference({ path: 'simple.palette', value: 1 }) store.setPreference({ path: 'simple.palette', value: 2 }) store.addCollectionPreference({ path: 'collections.palette', value: 2 }) @@ -423,10 +389,7 @@ describe('The SyncConfig store', () => { it('should remove duplicate entries from journal', () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() store.setPreference({ path: 'simple.palette', value: 1 }) store.setPreference({ path: 'simple.palette', value: 1 }) store.addCollectionPreference({ path: 'collections.palette', value: 2 }) @@ -440,10 +403,7 @@ describe('The SyncConfig store', () => { // TODO We need a proper test for object-based stores it.skip('should remove depth = 3 set/unset entries from journal', () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() store.setPreference({ path: 'simple.fontInput', value: 'test' }) store.unsetPreference({ path: 'simple.fontInput' }) store.updateCache(store, { username: 'test' }) @@ -455,10 +415,7 @@ describe('The SyncConfig store', () => { it('should not allow unsetting depth <= 2', () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() store.setPreference({ path: 'simple.object.foo', value: 1 }) expect(() => store.unsetPreference({ path: 'simple' })).to.throw() expect(() => @@ -468,10 +425,7 @@ describe('The SyncConfig store', () => { it('should not allow (un)setting depth > 3', () => { const store = useSyncConfigStore() - // PushSyncConfig is very simple but uses vuex to push data - store.pushSyncConfig = () => { - /* no-op */ - } + store.pushSyncConfig = vi.fn() store.setPreference({ path: 'simple.object', value: {} }) expect(() => store.setPreference({ path: 'simple.object.lv3', value: 1 }), diff --git a/vite.config.js b/vite.config.js index 7dee680ec..9ebd3a3a5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -120,6 +120,7 @@ export default defineConfig(async ({ mode, command }) => { const swDest = 'sw-pleroma.js' const alias = { src: '/src', + test: '/test', components: '/src/components', ...(mode === 'test' ? { vue: 'vue/dist/vue.esm-bundler.js' } : {}), } From d5382d4aa27b2d86087fba27b075a98b37c85b87 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 17:27:06 +0300 Subject: [PATCH 10/21] fix --- src/stores/profile_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/profile_config.js b/src/stores/profile_config.js index 5d43ea446..516b3845e 100644 --- a/src/stores/profile_config.js +++ b/src/stores/profile_config.js @@ -125,7 +125,7 @@ export const useProfileConfigStore = defineStore('profileConfig', { return } - useUsersStore().addNewUsers(result) + const [user] = useUsersStore().addNewUsers(result) this.update(user) } catch (e) { console.warn('Error setting server-side option:', e) From 201e2fb910f48679eddc2b924b476a536c73bb90 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 17:27:30 +0300 Subject: [PATCH 11/21] fix --- src/components/notification/notification.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 34251d42d..51b60fb29 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -13,6 +13,7 @@ import { highlightStyle, } from '../../services/user_highlighter/user_highlighter.js' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useNotificationsStore } from 'src/stores/notifications.js' From e9b70fcb0e2c120d87a1b7f23da3a3e23806db7b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 17:49:27 +0300 Subject: [PATCH 12/21] tests moved to a proper place --- src/components/draft/draft.js | 2 + src/stores/drafts.js | 1 + test/unit/specs/components/draft.spec.js | 193 ----- .../specs/components/post_status_form.spec.js | 759 +++++++++++------- 4 files changed, 483 insertions(+), 472 deletions(-) delete mode 100644 test/unit/specs/components/draft.spec.js diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 5f17738d0..12df0d651 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -76,7 +76,9 @@ const Draft = { }, watch: { editing(newVal) { + console.log('SAVE?', newVal) if (newVal) return + console.log('SAVE', newVal) if (this.safeToSave) { useDraftsStore().addOrSaveDraft(this.draft) } else { diff --git a/src/stores/drafts.js b/src/stores/drafts.js index 8325fba5b..0f3fae527 100644 --- a/src/stores/drafts.js +++ b/src/stores/drafts.js @@ -62,6 +62,7 @@ export const useDraftsStore = defineStore('drafts', { }, async addOrSaveDraft(draft) { const id = draft.id ?? new Date().getTime().toString() + console.log('SAVE', id) const draftWithId = { ...draft, id } this.drafts.set(draft.id, draftWithId) await saveDraftToStorage(draftWithId) diff --git a/test/unit/specs/components/draft.spec.js b/test/unit/specs/components/draft.spec.js deleted file mode 100644 index 9e26e36b3..000000000 --- a/test/unit/specs/components/draft.spec.js +++ /dev/null @@ -1,193 +0,0 @@ -import { createTestingPinia } from '@pinia/testing' -import { flushPromises, mount } from '@vue/test-utils' -import { setActivePinia } from 'pinia' -import { $t, mountOpts, waitForEvent } from 'test/fixtures/setup_test.js' -import { nextTick } from 'vue' - -import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' - -import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useUsersStore } from 'src/stores/users.js' - -const autoSaveOrNot = (caseFn, caseTitle, runFn) => { - caseFn(`${caseTitle} with auto-save`, function () { - return runFn.bind(this)(true) - }) - - caseFn(`${caseTitle} with no auto-save`, function () { - return runFn.bind(this)(false) - }) -} - -const saveManually = async (wrapper) => { - const morePostActions = wrapper.findByText( - 'button', - $t('post_status.more_post_actions'), - ) - await morePostActions.trigger('click') - - const btn = wrapper.findByText( - 'button', - $t('post_status.save_to_drafts_button'), - ) - await btn.trigger('click') -} - -const waitSaveTime = 4000 - -const currentUser = { - id: 'current-user', - default_scope: 'public', - locked: false, -} - -describe('Draft saving', () => { - beforeEach(() => { - setActivePinia(createTestingPinia()) - useUsersStore().currentUser = currentUser - }) - - afterEach(() => { - vi.useRealTimers() - }) - - autoSaveOrNot( - it, - 'should save when the button is clicked', - async (autoSave) => { - const wrapper = mount(PostStatusForm, mountOpts()) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: autoSave, - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - - await saveManually(wrapper) - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal( - 'mew mew', - ) - }, - ) - - it('should auto-save if it is enabled', async function () { - vi.useFakeTimers() - const wrapper = mount(PostStatusForm, mountOpts()) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: true, - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - await vi.advanceTimersByTimeAsync(waitSaveTime) - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew') - }) - - it('should auto-save when close if auto-save is on', async () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - closeable: true, - }, - }), - ) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: true, - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - wrapper.vm.requestClose() - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - await waitForEvent(wrapper, 'close-accepted') - }) - - it('should save when close if auto-save is off, and unsavedPostAction is save', async () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - closeable: true, - }, - }), - ) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: false, - unsavedPostAction: 'save', - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - wrapper.vm.requestClose() - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - await waitForEvent(wrapper, 'close-accepted') - }) - - it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - closeable: true, - }, - }), - ) - const store = useMergedConfigStore() - store.mergedConfig = { - autoSaveDraft: false, - unsavedPostAction: 'discard', - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - wrapper.vm.requestClose() - await waitForEvent(wrapper, 'close-accepted') - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - }) - - it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - closeable: true, - }, - }), - ) - const store = useMergedConfigStore(createTestingPinia()) - store.mergedConfig = { - autoSaveDraft: false, - unsavedPostAction: 'confirm', - } - expect(wrapper.vm.$store.getters.draftCount).to.equal(0) - const textarea = wrapper.get('textarea') - await textarea.setValue('mew mew') - wrapper.vm.requestClose() - await nextTick() - await flushPromises() - const saveButton = await vi.waitFor(() => { - const button = wrapper.findByText( - 'button', - $t('post_status.close_confirm_save_button'), - ) - if (!button) throw new Error('Save button not present') - return button - }) - expect(saveButton).to.be.ok - await saveButton.trigger('click') - console.info('clicked') - expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - await flushPromises() - await waitForEvent(wrapper, 'close-accepted') - }) -}) diff --git a/test/unit/specs/components/post_status_form.spec.js b/test/unit/specs/components/post_status_form.spec.js index 1fddf4a95..45a6bea22 100644 --- a/test/unit/specs/components/post_status_form.spec.js +++ b/test/unit/specs/components/post_status_form.spec.js @@ -1,16 +1,44 @@ import { createTestingPinia } from '@pinia/testing' -import { mount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' import { setActivePinia } from 'pinia' -import { mountOpts } from 'test/fixtures/setup_test.js' +import { $t, mountOpts, waitForEvent } from 'test/fixtures/setup_test.js' import { vi } from 'vitest' +import { nextTick } from 'vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' +import { useDraftsStore } from 'src/stores/drafts.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useStatusesStore } from 'src/stores/statuses.js' import { useUsersStore } from 'src/stores/users.js' +const autoSaveOrNot = (caseFn, caseTitle, runFn) => { + caseFn(`${caseTitle} with auto-save`, function () { + return runFn.bind(this)(true) + }) + + caseFn(`${caseTitle} with no auto-save`, function () { + return runFn.bind(this)(false) + }) +} + +const saveManually = async (wrapper) => { + const morePostActions = wrapper.findByText( + 'button', + $t('post_status.more_post_actions'), + ) + await morePostActions.trigger('click') + + const btn = wrapper.findByText( + 'button', + $t('post_status.save_to_drafts_button'), + ) + await btn.trigger('click') +} + +const waitSaveTime = 4000 + const currentUser = { id: 'current-user', default_scope: 'public', @@ -36,303 +64,476 @@ const repliedStatus2 = { } describe('PostStatusForm', () => { - beforeEach(() => { - vi.useFakeTimers() - setActivePinia(createTestingPinia()) - useUsersStore().currentUser = currentUser - useStatusesStore().allStatuses = new Map([ - [repliedStatus.id, repliedStatus], - ]) - }) + describe('Basic functionality', () => { + beforeEach(() => { + vi.useFakeTimers() + setActivePinia(createTestingPinia()) + useUsersStore().currentUser = currentUser + useStatusesStore().allStatuses = new Map([ + [repliedStatus.id, repliedStatus], + ]) + }) - it('Clean empty initial state', () => { - const wrapper = mount(PostStatusForm, mountOpts()) + afterEach(() => { + vi.useRealTimers() + }) - expect(wrapper.vm.statusType).to.equal('new') - expect(wrapper.vm.newStatus.spoilerText).to.eql('') - expect(wrapper.vm.newStatus.mentions).to.eql('') - expect(wrapper.vm.newStatus.status).to.eql('') - }) + it('Clean empty initial state', () => { + const wrapper = mount(PostStatusForm, mountOpts()) - it('Reset cleans form to pristine state equal to state form was when created', () => { - const wrapper = mount(PostStatusForm, mountOpts()) + expect(wrapper.vm.statusType).to.equal('new') + expect(wrapper.vm.newStatus.spoilerText).to.eql('') + expect(wrapper.vm.newStatus.mentions).to.eql('') + expect(wrapper.vm.newStatus.status).to.eql('') + }) - const initial = { ...wrapper.vm.newStatus } - wrapper.vm.clearStatus() + it('Reset cleans form to pristine state equal to state form was when created', () => { + const wrapper = mount(PostStatusForm, mountOpts()) - expect(wrapper.vm.newStatus).to.eql(initial) - }) + const initial = { ...wrapper.vm.newStatus } + wrapper.vm.clearStatus() - it('Initializes a reply form', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ + expect(wrapper.vm.newStatus).to.eql(initial) + }) + + it('Initializes a reply form', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus, + }, + }), + ) + + useInstanceCapabilitiesStore().quotingAvailable = true + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + expect(wrapper.vm.refId).to.equal('status-1') + expect(wrapper.vm.quotable).to.equal(true) + expect(wrapper.vm.inReplyToStatusId).to.equal('status-1') + expect(wrapper.vm.newStatus.quote).to.be.null + expect(wrapper.vm.newStatus.poll).to.be.null + expect(wrapper.vm.newStatus.spoilerText).to.eql('') + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('@replied ') + expect(wrapper.vm.newStatus.visibility).to.eql('public') + }) + + it('Copies scope and subject line, disables quoting for locked posts', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + }, + }), + ) + + useInstanceCapabilitiesStore().quotingAvailable = true + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + expect(wrapper.vm.quotable).to.equal(false) + expect(wrapper.vm.newStatus.quote).to.be.null + expect(wrapper.vm.newStatus.poll).to.be.null + expect(wrapper.vm.newStatus.spoilerText).to.eql('re: subject') + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('@replied ') + expect(wrapper.vm.newStatus.visibility).to.eql('private') + + expect(wrapper.vm.postingOptions.status).to.eql('@replied ') + expect(wrapper.vm.postingOptions.spoilerText).to.eql('re: subject') + expect(wrapper.vm.postingOptions.visibility).to.eql('private') + expect(wrapper.vm.postingOptions.sensitive).to.eql(false) + expect(wrapper.vm.postingOptions.media).to.eql([]) + expect(wrapper.vm.postingOptions.inReplyToStatusId).to.eql('status-2') + expect(wrapper.vm.postingOptions.quoteId).to.be.null + expect(wrapper.vm.postingOptions.contentType).to.eql('text/plain') + expect(wrapper.vm.postingOptions.poll).to.be.null + }) + + it('Forces direct mode when replying to a DM, mastodon style subject handling', () => { + // We need to initialize pinia first which is happening here... + const options = mountOpts({ props: { - repliedStatus: repliedStatus, + repliedStatus: { ...repliedStatus2, visibility: 'direct' }, }, - }), - ) + }) - useInstanceCapabilitiesStore().quotingAvailable = true + // ...set our settings... + useMergedConfigStore().mergedConfig = { + ...useMergedConfigStore().mergedConfig, + subjectLineBehavior: 'masto', + } - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - expect(wrapper.vm.refId).to.equal('status-1') - expect(wrapper.vm.quotable).to.equal(true) - expect(wrapper.vm.inReplyToStatusId).to.equal('status-1') - expect(wrapper.vm.newStatus.quote).to.be.null - expect(wrapper.vm.newStatus.poll).to.be.null - expect(wrapper.vm.newStatus.spoilerText).to.eql('') - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('@replied ') - expect(wrapper.vm.newStatus.visibility).to.eql('public') + // ...and only then mount our component + const wrapper = mount(PostStatusForm, options) + + // Otherwise we get multiple instances of pinia that don't talk to each other + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + expect(wrapper.vm.quotable).to.equal(false) + expect(wrapper.vm.newStatus.quote).to.be.null + expect(wrapper.vm.newStatus.poll).to.be.null + expect(wrapper.vm.newStatus.spoilerText).to.eql('subject') + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('@replied ') + expect(wrapper.vm.newStatus.visibility).to.eql('direct') + }) + + it('Sets status to statusText without mentions if mentions line is enabled', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + statusText: 'testing', + mentionsLine: true, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('testing') + }) + + it('Sets mention when asked for it', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + profileMention: repliedUser, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('mention') + expect(wrapper.vm.isReply).to.equal(false) + expect(wrapper.vm.newStatus.mentions).to.eql('@replied') + expect(wrapper.vm.newStatus.status).to.eql('@replied ') + }) + + it('Initializes quote when reply/quote toggled to quote', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + + wrapper.vm.quoteThreadToggled = true + + expect(wrapper.vm.newStatus.quote).to.eql({ + thread: true, + id: 'status-2', + }) + }) + + it('Resets quote when reply/quote toggled to reply', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + + wrapper.vm.quoteThreadToggled = true + wrapper.vm.quoteThreadToggled = false + + expect(wrapper.vm.newStatus.quote).to.be.null + }) + + it('Initializes and reset quote when toggling quote attachment', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + repliedStatus: repliedStatus2, + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('reply') + expect(wrapper.vm.isReply).to.equal(true) + + wrapper.vm.toggleQuoteForm() + expect(wrapper.vm.newStatus.quote).to.eql({ + thread: false, + id: null, + url: '', + }) + wrapper.vm.toggleQuoteForm() + expect(wrapper.vm.newStatus.quote).to.be.null + }) + + it('Status editing', () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + statusId: 'edited', + statusText: 'text', + statusSubject: 'heading', + statusIsSensitive: true, + statusPoll: {}, + statusQuote: {}, + statusFiles: [], + statusMediaDescriptions: {}, + statusVisibility: 'unlisted', + statusContentType: 'text/markdown', + }, + }), + ) + + expect(wrapper.vm.statusType).to.equal('edit') + expect(wrapper.vm.isReply).to.equal(false) // edits don't support changing reply-to so it's pretty much ignored + expect(wrapper.vm.isEdit).to.equal(true) + expect(wrapper.vm.newStatus.quote).to.eql({}) + expect(wrapper.vm.newStatus.poll).to.eql({}) + expect(wrapper.vm.newStatus.spoilerText).to.eql('heading') + expect(wrapper.vm.newStatus.mentions).to.eql('') + expect(wrapper.vm.newStatus.status).to.eql('text') + expect(wrapper.vm.newStatus.visibility).to.eql('unlisted') + expect(wrapper.vm.newStatus.contentType).to.eql('text/markdown') + expect(wrapper.vm.newStatus.nsfw).to.equal(true) + expect(wrapper.vm.newStatus.files).to.eql([]) + }) + + it('Posting should reset idempotency key', async () => { + vi.setSystemTime(new Date(2027, 1, 1, 13)) + const wrapper = mount(PostStatusForm, mountOpts()) + const oldIdempotency = wrapper.vm.idempotencyKey + + vi.setSystemTime(new Date(2028, 1, 1, 13)) + + wrapper.vm.newStatus.status = 'Testing' + await wrapper.vm.postStatus() + + expect(wrapper.vm.idempotencyKey).to.not.eql(oldIdempotency) + }) }) - it('Copies scope and subject line, disables quoting for locked posts', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - }, - }), - ) + describe('Attachments', () => { + beforeEach(() => { + vi.useFakeTimers() + setActivePinia(createTestingPinia()) + useUsersStore().currentUser = currentUser + useStatusesStore().allStatuses = new Map([ + [repliedStatus.id, repliedStatus], + ]) + }) - useInstanceCapabilitiesStore().quotingAvailable = true + afterEach(() => { + vi.useRealTimers() + }) - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - expect(wrapper.vm.quotable).to.equal(false) - expect(wrapper.vm.newStatus.quote).to.be.null - expect(wrapper.vm.newStatus.poll).to.be.null - expect(wrapper.vm.newStatus.spoilerText).to.eql('re: subject') - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('@replied ') - expect(wrapper.vm.newStatus.visibility).to.eql('private') + // TODO Probably better to separate attachment upload/manipulation into its own component? + // we need to upload-on-submit for compression setting anyway + it('Attachments manipulations (moving, adding, removing)', () => { + const wrapper = mount(PostStatusForm, mountOpts()) - expect(wrapper.vm.postingOptions.status).to.eql('@replied ') - expect(wrapper.vm.postingOptions.spoilerText).to.eql('re: subject') - expect(wrapper.vm.postingOptions.visibility).to.eql('private') - expect(wrapper.vm.postingOptions.sensitive).to.eql(false) - expect(wrapper.vm.postingOptions.media).to.eql([]) - expect(wrapper.vm.postingOptions.inReplyToStatusId).to.eql('status-2') - expect(wrapper.vm.postingOptions.quoteId).to.be.null - expect(wrapper.vm.postingOptions.contentType).to.eql('text/plain') - expect(wrapper.vm.postingOptions.poll).to.be.null + const i1 = { id: '1', url: 'a' } + const i2 = { id: '2', url: 'b' } + const i3 = { id: '3', url: 'c' } + const i4 = { id: '4', url: 'd' } + const iX = { id: 'x', url: 'x' } + + wrapper.vm.newStatus.files = [i3, i1, iX, i2] + + wrapper.vm.removeMediaFile(iX) + expect(wrapper.vm.newStatus.files).to.eql([i3, i1, i2]) + + wrapper.vm.shiftUpMediaFile(i1) + expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2]) + + wrapper.vm.shiftUpMediaFile(i1) // should ignore + expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2]) + + wrapper.vm.shiftDnMediaFile(i3) + expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3]) + + wrapper.vm.shiftDnMediaFile(i3) // should ignore + expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3]) + + wrapper.vm.addMediaFile(i4) + expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3, i4]) + }) + + it('Attachment descriptions', () => { + const wrapper = mount(PostStatusForm, mountOpts()) + + const i1 = { id: '1', url: 'a' } + + wrapper.vm.addMediaFile(i1) + expect(wrapper.vm.newStatus.files).to.eql([i1]) + + wrapper.vm.editAttachment(i1, 'description') + expect(wrapper.vm.newStatus.mediaDescriptions['1']).to.eql('description') + }) }) - it('Forces direct mode when replying to a DM, mastodon style subject handling', () => { - // We need to initialize pinia first which is happening here... - const options = mountOpts({ - props: { - repliedStatus: { ...repliedStatus2, visibility: 'direct' }, + describe('Draft saving', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + useUsersStore().currentUser = currentUser + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + autoSaveOrNot( + it, + 'should save when the button is clicked', + async (autoSave) => { + const wrapper = mount(PostStatusForm, mountOpts()) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: autoSave, + } + expect(useDraftsStore().draftsCount).to.equal(0) + + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + + await saveManually(wrapper) + expect(useDraftsStore().draftsCount).to.equal(1) + expect(useDraftsStore().draftsArray[0].status).to.equal('mew mew') }, + ) + + it('should auto-save if it is enabled', async function () { + vi.useFakeTimers() + const wrapper = mount(PostStatusForm, mountOpts()) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: true, + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + + expect(useDraftsStore().draftsCount).to.equal(0) + await vi.advanceTimersByTimeAsync(waitSaveTime) + expect(useDraftsStore().draftsCount).to.equal(1) + expect(useDraftsStore().draftsArray[0].status).to.equal('mew mew') }) - // ...set our settings... - useMergedConfigStore().mergedConfig = { - ...useMergedConfigStore().mergedConfig, - subjectLineBehavior: 'masto', - } - - // ...and only then mount our component - const wrapper = mount(PostStatusForm, options) - - // Otherwise we get multiple instances of pinia that don't talk to each other - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - expect(wrapper.vm.quotable).to.equal(false) - expect(wrapper.vm.newStatus.quote).to.be.null - expect(wrapper.vm.newStatus.poll).to.be.null - expect(wrapper.vm.newStatus.spoilerText).to.eql('subject') - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('@replied ') - expect(wrapper.vm.newStatus.visibility).to.eql('direct') - }) - - it('Sets status to statusText without mentions if mentions line is enabled', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - statusText: 'testing', - mentionsLine: true, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('testing') - }) - - it('Sets mention when asked for it', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - profileMention: repliedUser, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('mention') - expect(wrapper.vm.isReply).to.equal(false) - expect(wrapper.vm.newStatus.mentions).to.eql('@replied') - expect(wrapper.vm.newStatus.status).to.eql('@replied ') - }) - - it('Initializes quote when reply/quote toggled to quote', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - - wrapper.vm.quoteThreadToggled = true - - expect(wrapper.vm.newStatus.quote).to.eql({ thread: true, id: 'status-2' }) - }) - - it('Resets quote when reply/quote toggled to reply', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - - wrapper.vm.quoteThreadToggled = true - wrapper.vm.quoteThreadToggled = false - - expect(wrapper.vm.newStatus.quote).to.be.null - }) - - it('Initializes and reset quote when toggling quote attachment', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - repliedStatus: repliedStatus2, - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('reply') - expect(wrapper.vm.isReply).to.equal(true) - - wrapper.vm.toggleQuoteForm() - expect(wrapper.vm.newStatus.quote).to.eql({ - thread: false, - id: null, - url: '', + it('should auto-save when close if auto-save is on', async () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + closeable: true, + }, + }), + ) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: true, + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + wrapper.vm.requestClose() + expect(useDraftsStore().draftsCount).to.equal(1) + await waitForEvent(wrapper, 'close-accepted') + }) + + it('should save when close if auto-save is off, and unsavedPostAction is save', async () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + closeable: true, + }, + }), + ) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: false, + unsavedPostAction: 'save', + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + wrapper.vm.requestClose() + expect(useDraftsStore().draftsCount).to.equal(1) + await waitForEvent(wrapper, 'close-accepted') + }) + + it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => { + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + closeable: true, + }, + }), + ) + const store = useMergedConfigStore() + store.mergedConfig = { + autoSaveDraft: false, + unsavedPostAction: 'discard', + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + wrapper.vm.requestClose() + await waitForEvent(wrapper, 'close-accepted') + expect(useDraftsStore().draftsCount).to.equal(0) + }) + + it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => { + const store = useMergedConfigStore() + const wrapper = mount( + PostStatusForm, + mountOpts({ + props: { + closeable: true, + }, + }), + ) + store.mergedConfig = { + autoSaveDraft: false, + unsavedPostAction: 'confirm', + } + expect(useDraftsStore().draftsCount).to.equal(0) + const textarea = wrapper.get('textarea') + await textarea.setValue('mew mew') + wrapper.vm.requestClose() + await nextTick() + await flushPromises() + const saveButton = await vi.waitFor(() => { + const button = wrapper.findByText( + 'button', + $t('post_status.close_confirm_save_button'), + ) + if (!button) throw new Error('Save button not present') + return button + }) + expect(saveButton).to.be.ok + await saveButton.trigger('click') + console.info('clicked') + expect(useDraftsStore().draftsCount).to.equal(1) + await flushPromises() + await waitForEvent(wrapper, 'close-accepted') }) - wrapper.vm.toggleQuoteForm() - expect(wrapper.vm.newStatus.quote).to.be.null }) - - it('Status editing', () => { - const wrapper = mount( - PostStatusForm, - mountOpts({ - props: { - statusId: 'edited', - statusText: 'text', - statusSubject: 'heading', - statusIsSensitive: true, - statusPoll: {}, - statusQuote: {}, - statusFiles: [], - statusMediaDescriptions: {}, - statusVisibility: 'unlisted', - statusContentType: 'text/markdown', - }, - }), - ) - - expect(wrapper.vm.statusType).to.equal('edit') - expect(wrapper.vm.isReply).to.equal(false) // edits don't support changing reply-to so it's pretty much ignored - expect(wrapper.vm.isEdit).to.equal(true) - expect(wrapper.vm.newStatus.quote).to.eql({}) - expect(wrapper.vm.newStatus.poll).to.eql({}) - expect(wrapper.vm.newStatus.spoilerText).to.eql('heading') - expect(wrapper.vm.newStatus.mentions).to.eql('') - expect(wrapper.vm.newStatus.status).to.eql('text') - expect(wrapper.vm.newStatus.visibility).to.eql('unlisted') - expect(wrapper.vm.newStatus.contentType).to.eql('text/markdown') - expect(wrapper.vm.newStatus.nsfw).to.equal(true) - expect(wrapper.vm.newStatus.files).to.eql([]) - }) - - it('Posting should reset idempotency key', async () => { - vi.setSystemTime(new Date(2027, 1, 1, 13)) - const wrapper = mount(PostStatusForm, mountOpts()) - const oldIdempotency = wrapper.vm.idempotencyKey - - vi.setSystemTime(new Date(2028, 1, 1, 13)) - - wrapper.vm.newStatus.status = 'Testing' - await wrapper.vm.postStatus() - - expect(wrapper.vm.idempotencyKey).to.not.eql(oldIdempotency) - }) - - // TODO Probably better to separate attachment upload/manipulation into its own component? - // we need to upload-on-submit for compression setting anyway - it('Attachments manipulations (moving, adding, removing)', () => { - const wrapper = mount(PostStatusForm, mountOpts()) - - const i1 = { id: '1', url: 'a' } - const i2 = { id: '2', url: 'b' } - const i3 = { id: '3', url: 'c' } - const i4 = { id: '4', url: 'd' } - const iX = { id: 'x', url: 'x' } - - wrapper.vm.newStatus.files = [i3, i1, iX, i2] - - wrapper.vm.removeMediaFile(iX) - expect(wrapper.vm.newStatus.files).to.eql([i3, i1, i2]) - - wrapper.vm.shiftUpMediaFile(i1) - expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2]) - - wrapper.vm.shiftUpMediaFile(i1) // should ignore - expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2]) - - wrapper.vm.shiftDnMediaFile(i3) - expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3]) - - wrapper.vm.shiftDnMediaFile(i3) // should ignore - expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3]) - - wrapper.vm.addMediaFile(i4) - expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3, i4]) - }) - - it('Attachment descriptions', () => { - const wrapper = mount(PostStatusForm, mountOpts()) - - const i1 = { id: '1', url: 'a' } - - wrapper.vm.addMediaFile(i1) - expect(wrapper.vm.newStatus.files).to.eql([i1]) - - wrapper.vm.editAttachment(i1, 'description') - expect(wrapper.vm.newStatus.mediaDescriptions['1']).to.eql('description') - }) - // TODO: Drafts (needs vuex to pinia migration) }) From 266e71178643ff2f42e4c1558d074f49e88b9ea0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 19:01:56 +0300 Subject: [PATCH 13/21] fix draft editing --- src/components/draft/draft.js | 12 ++++-------- src/components/draft/draft.vue | 4 +++- src/components/post_status_form/post_status_form.js | 2 ++ src/stores/drafts.js | 5 ++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 12df0d651..90497139b 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -36,6 +36,7 @@ const Draft = { return { referenceDraft: cloneDeep(this.draft), editing: false, + showingForm: false, showingConfirmDialog: false, } }, @@ -52,7 +53,7 @@ const Draft = { } }, safeToSave() { - return ( + return Boolean( this.draft.status || this.draft.files?.length || this.draft.hasPoll || @@ -76,14 +77,9 @@ const Draft = { }, watch: { editing(newVal) { - console.log('SAVE?', newVal) + this.showingForm = true if (newVal) return - console.log('SAVE', newVal) - if (this.safeToSave) { - useDraftsStore().addOrSaveDraft(this.draft) - } else { - useDraftsStore().addOrSaveDraft(this.referenceDraft) - } + this.showingForm = false }, }, methods: { diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue index e610f627b..1fc0294e6 100644 --- a/src/components/draft/draft.vue +++ b/src/components/draft/draft.vue @@ -64,14 +64,16 @@
-
+
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index b708d3f68..ed48de2d7 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -987,6 +987,8 @@ const PostStatusForm = { type: this.statusType, refId: this.refId, ...this.newStatus, + // Draft ID overwrites status ID (which is undefined for fresh statuses) + id: this.draftId, }) .then((id) => { if (this.newStatus.id !== id) { diff --git a/src/stores/drafts.js b/src/stores/drafts.js index 0f3fae527..bf707cbe4 100644 --- a/src/stores/drafts.js +++ b/src/stores/drafts.js @@ -62,14 +62,13 @@ export const useDraftsStore = defineStore('drafts', { }, async addOrSaveDraft(draft) { const id = draft.id ?? new Date().getTime().toString() - console.log('SAVE', id) const draftWithId = { ...draft, id } - this.drafts.set(draft.id, draftWithId) + this.drafts.set(id, draftWithId) await saveDraftToStorage(draftWithId) return id }, async abandonAllDrafts(store) { - const ids = this.drafts.keys() + const ids = [...this.drafts.keys()] ids.forEach((id) => this.abandonDraft(id)) await deleteDraftFromStorage(ids) }, From 53a0ab9af247ca339096856d088888960f14f4ae Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 19:14:48 +0300 Subject: [PATCH 14/21] cleanup --- src/components/draft/draft.js | 17 ++--------------- src/components/draft/draft.vue | 6 +++--- .../edit_status_form/edit_status_form.js | 3 +++ 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 90497139b..22a10b2e6 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -34,9 +34,7 @@ const Draft = { }, data() { return { - referenceDraft: cloneDeep(this.draft), editing: false, - showingForm: false, showingConfirmDialog: false, } }, @@ -52,14 +50,6 @@ const Draft = { return {} } }, - safeToSave() { - return Boolean( - this.draft.status || - this.draft.files?.length || - this.draft.hasPoll || - this.draft.hasQuote - ) - }, postStatusFormProps() { return { draftId: this.draft.id, @@ -71,15 +61,12 @@ const Draft = { ? useStatusesStore().allStatuses.get(this.draft.refId) : undefined }, - localCollapseSubjectDefault() { - return useMergedConfigStore().mergedConfig.collapseMessageWithSubject - }, }, watch: { editing(newVal) { - this.showingForm = true if (newVal) return - this.showingForm = false + // (Post|Edit)StatusForm handles draft saving + this.$refs.form.saveDraft() }, }, methods: { diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue index 1fc0294e6..5302a3ab3 100644 --- a/src/components/draft/draft.vue +++ b/src/components/draft/draft.vue @@ -64,16 +64,16 @@
-
+
diff --git a/src/components/edit_status_form/edit_status_form.js b/src/components/edit_status_form/edit_status_form.js index 30bb98590..0121d4597 100644 --- a/src/components/edit_status_form/edit_status_form.js +++ b/src/components/edit_status_form/edit_status_form.js @@ -15,6 +15,9 @@ const EditStatusForm = { requestClose() { this.$refs.postStatusForm.requestClose() }, + saveDraft() { + this.$refs.postStatusForm.saveDraft() + }, doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) { const params = { statusId: this.params.statusId, From 964e016bfb0a2ff2d5c51ce389fce84ff6719a7f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 19:18:11 +0300 Subject: [PATCH 15/21] consistency, lint, cleanup --- src/components/draft/draft.js | 2 -- src/components/extra_notifications/extra_notifications.js | 4 ++-- src/components/extra_notifications/extra_notifications.vue | 2 +- src/components/mobile_nav/mobile_nav.js | 4 ++-- src/components/mobile_nav/mobile_nav.vue | 2 +- src/components/nav_panel/nav_panel.js | 2 +- src/components/navigation/navigation_entry.js | 2 +- src/components/navigation/navigation_pins.js | 2 +- src/components/notifications/notifications.js | 6 +++--- src/components/side_drawer/side_drawer.js | 2 +- src/components/side_drawer/side_drawer.vue | 4 ++-- src/stores/announcements.js | 2 +- 12 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js index 22a10b2e6..6c7f38287 100644 --- a/src/components/draft/draft.js +++ b/src/components/draft/draft.js @@ -1,4 +1,3 @@ -import { cloneDeep } from 'lodash' import { defineAsyncComponent } from 'vue' import Gallery from 'src/components/gallery/gallery.vue' @@ -6,7 +5,6 @@ import PostStatusForm from 'src/components/post_status_form/post_status_form.vue import StatusContent from 'src/components/status_content/status_content.vue' import { useDraftsStore } from 'src/stores/drafts.js' -import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useStatusesStore } from 'src/stores/statuses.js' import { library } from '@fortawesome/fontawesome-svg-core' diff --git a/src/components/extra_notifications/extra_notifications.js b/src/components/extra_notifications/extra_notifications.js index d20cbb67e..70583bd2f 100644 --- a/src/components/extra_notifications/extra_notifications.js +++ b/src/components/extra_notifications/extra_notifications.js @@ -30,7 +30,7 @@ const ExtraNotifications = { return ( this.mergedConfig.showExtraNotifications && this.mergedConfig.showAnnouncementsInExtraNotifications && - this.unreadAnnouncementCount + this.unreadAnnouncementsCount ) }, shouldShowFollowRequests() { @@ -56,7 +56,7 @@ const ExtraNotifications = { return useUsersStore().currentUser }, ...mapState(useAnnouncementsStore, { - unreadAnnouncementCount: 'unreadAnnouncementCount', + unreadAnnouncementsCount: 'unreadAnnouncementsCount', }), ...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useChatsStore, ['unreadChatsCount']), diff --git a/src/components/extra_notifications/extra_notifications.vue b/src/components/extra_notifications/extra_notifications.vue index 329c5c1c3..6e21efcb2 100644 --- a/src/components/extra_notifications/extra_notifications.vue +++ b/src/components/extra_notifications/extra_notifications.vue @@ -31,7 +31,7 @@ class="fa-scale-110 icon" icon="bullhorn" /> - {{ $t('notifications.unread_announcements', { num: unreadAnnouncementCount }, unreadAnnouncementCount) }} + {{ $t('notifications.unread_announcements', { num: unreadAnnouncementsCount }, unreadAnnouncementsCount) }}
new Set(store.prefsStorage.collections.pinnedNavItems).has('chats'), diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index 8cdb70a1a..336ad60a6 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -19,7 +19,7 @@ icon="bars" />
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index f5de8bb2c..e03a0c9d7 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -112,7 +112,7 @@ const NavPanel = { }, computed: { ...mapState(useAnnouncementsStore, { - unreadAnnouncementCount: 'unreadAnnouncementCount', + unreadAnnouncementsCount: 'unreadAnnouncementsCount', supportsAnnouncements: (store) => store.supportsAnnouncements, }), ...mapState(useInstanceCapabilitiesStore, [ diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index 49392b37a..6186899e6 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -46,7 +46,7 @@ const NavigationEntry = { badges() { return { drafts: this.draftsCount, - unreadAnnouncements: this.unreadAnnouncementCount, + unreadAnnouncements: this.unreadAnnouncementsCount, followRequests: this.followRequestsCount, unreadChats: this.unreadChatsCount, } diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index c4773d833..d9a3e3a4a 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -59,7 +59,7 @@ const NavPanel = { badges() { return { drafts: this.draftsCount, - unreadAnnouncements: this.unreadAnnouncementCount, + unreadAnnouncements: this.unreadAnnouncementsCount, followRequests: this.followRequestsCount, unreadChats: this.unreadChatsCount, } diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 734671521..b1a1468ab 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -110,7 +110,7 @@ const Notifications = { return countExtraNotifications( useMergedConfigStore().mergedConfig, useChatsStore().unreadChatsCount, - useAnnouncementsStore().unreadAnnouncementCount, + useAnnouncementsStore().unreadAnnouncementsCount, useFollowRequestsStore().followRequestsCount, ) }, @@ -118,7 +118,7 @@ const Notifications = { return ( this.unseenNotifications.length + this.unreadChatsCount + - this.unreadAnnouncementCount + this.unreadAnnouncementsCount ) }, loading() { @@ -157,7 +157,7 @@ const Notifications = { showExtraNotifications() { return !this.noExtra }, - ...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), + ...mapState(useAnnouncementsStore, ['unreadAnnouncementsCount']), ...mapState(useChatsStore, ['unreadChatsCount']), ...mapState(useInterfaceStore, ['layoutType']), }, diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index fb0152ece..6b3fad7fa 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -97,7 +97,7 @@ const SideDrawer = { ...mapState(useFollowRequestsStore, ['followRequestsCount']), ...mapState(useAnnouncementsStore, [ 'supportsAnnouncements', - 'unreadAnnouncementCount', + 'unreadAnnouncementsCount', ]), ...mapState(useInstanceCapabilitiesStore, [ 'pleromaChatMessagesAvailable', diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index a42287b3c..f7b0563ba 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -248,10 +248,10 @@ icon="bullhorn" /> {{ $t("nav.announcements") }} - {{ unreadAnnouncementCount }} + {{ unreadAnnouncementsCount }} diff --git a/src/stores/announcements.js b/src/stores/announcements.js index c78e79853..e06602dfe 100644 --- a/src/stores/announcements.js +++ b/src/stores/announcements.js @@ -16,7 +16,7 @@ export const useAnnouncementsStore = defineStore('announcements', { userActions: {}, }), getters: { - unreadAnnouncementCount() { + unreadAnnouncementsCount() { if (!useUsersStore().currentUser) { return 0 } From fd701a468f6379d2daf743cd86bb2dc926cb1ba2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 19:25:00 +0300 Subject: [PATCH 16/21] fix navigation badges --- src/components/navigation/navigation_entry.js | 4 ++-- src/components/navigation/navigation_entry.vue | 2 +- src/components/navigation/navigation_pins.js | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index 6186899e6..1a7832afe 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -1,4 +1,4 @@ -import { mapState, mapStores } from 'pinia' +import { mapState } from 'pinia' import { routeTo } from 'src/components/navigation/navigation.js' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' @@ -51,7 +51,7 @@ const NavigationEntry = { unreadChats: this.unreadChatsCount, } }, - ...mapStores(useAnnouncementsStore), + ...mapState(useAnnouncementsStore, ['unreadAnnouncementsCount']), ...mapState(useDraftsStore, ['draftsCount']), ...mapState(useUsersStore, ['currentUser']), ...mapState(useChatsStore, ['unreadChatsCount']), diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index 0528a2a7b..b3e14d073 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -54,7 +54,7 @@ {{ badges[item.badgeGetter] }}
{{ this[`${item.store}Store`][item.badgeGetter] }} diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index d9a3e3a4a..b77518cc8 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -13,6 +13,8 @@ import { import { useAnnouncementsStore } from 'src/stores/announcements' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' +import { useChatsStore } from 'src/stores/chats.js' +import { useDraftsStore } from 'src/stores/drafts.js' import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' @@ -69,10 +71,14 @@ const NavPanel = { }), ...mapState(useAnnouncementsStore, { supportsAnnouncements: (store) => store.supportsAnnouncements, + unreadAnnouncementsCount: 'unreadAnnouncementsCount', }), + ...mapState(useDraftsStore, ['draftsCount']), + ...mapState(useFollowRequestsStore, ['followRequestsCount']), ...mapState(useBookmarkFoldersStore, { bookmarks: getBookmarkFolderEntries, }), + ...mapState(useChatsStore, ['unreadChatsCount']), ...mapState(useSyncConfigStore, { pinnedItems: (store) => new Set(store.prefsStorage.collections.pinnedNavItems), @@ -83,7 +89,6 @@ const NavPanel = { 'localBubble', ]), ...mapState(useUsersStore, ['currentUser']), - ...mapState(useFollowRequestsStore, ['followRequestsCount']), pinnedList() { if (!this.currentUser) { return filterNavigation( From 84d52798ed2e29085e3f04c0e14a476d9b9ffd2e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 19:31:44 +0300 Subject: [PATCH 17/21] remove/move unused dependencies --- package.json | 3 +-- yarn.lock | 15 +-------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 60f65b129..cba260ba3 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "click-outside-vue3": "4.0.1", "cropperjs": "2.0.1", "escape-html": "1.0.3", - "globals": "^16.0.0", "hash-sum": "^2.0.0", "js-cookie": "3.0.5", "localforage": "1.10.0", @@ -48,7 +47,6 @@ "punycode.js": "2.3.1", "qrcode": "1.5.4", "querystring-es3": "0.2.1", - "url": "0.11.4", "utf8": "3.0.0", "uuid": "11.1.0", "vue": "3.5.22", @@ -79,6 +77,7 @@ "autoprefixer": "10.4.21", "chai": "5.3.3", "chalk": "5.6.2", + "globals": "^16.0.0", "chromedriver": "135.0.4", "connect-history-api-fallback": "2.0.0", "cross-spawn": "7.0.6", diff --git a/yarn.lock b/yarn.lock index 364949865..b6a2c6be1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7513,11 +7513,6 @@ punycode.js@2.3.1: resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -7532,7 +7527,7 @@ qrcode@1.5.4: pngjs "^5.0.0" yargs "^15.3.1" -qs@^6.12.3, qs@^6.14.0: +qs@^6.14.0: version "6.14.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== @@ -9008,14 +9003,6 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" -url@0.11.4: - version "0.11.4" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c" - integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg== - dependencies: - punycode "^1.4.1" - qs "^6.12.3" - utf8@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" From 514e80888a0864c066750982c9bef7d13b938cc8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 19:39:03 +0300 Subject: [PATCH 18/21] fix error --- src/components/navigation/navigation_entry.vue | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index b3e14d073..cbebbaf47 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -53,12 +53,6 @@ > {{ badges[item.badgeGetter] }}
-
- {{ this[`${item.store}Store`][item.badgeGetter] }} -