From b721cd99512d7b335945dee72eb4d06f82422408 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 31 Aug 2026 18:05:45 +0300 Subject: [PATCH 01/39] simplify lodash tree-shaking (no real benefit though, but build should be simpler --- .babelrc | 2 +- package.json | 3 +- yarn.lock | 79 ++++------------------------------------------------ 3 files changed, 7 insertions(+), 77 deletions(-) diff --git a/.babelrc b/.babelrc index 4ec104161..48f99d6ca 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { "presets": ["@babel/preset-env"], - "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"], + "plugins": ["@babel/plugin-transform-runtime", "@vue/babel-plugin-jsx"], "comments": true } diff --git a/package.json b/package.json index 50138e6a2..c10f86b84 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,6 @@ "@vue/compiler-sfc": "3.5.22", "@vue/test-utils": "2.4.6", "autoprefixer": "10.4.21", - "babel-plugin-lodash": "3.3.4", "chai": "5.3.3", "chalk": "5.6.2", "chromedriver": "135.0.4", @@ -97,7 +96,7 @@ "function-bind": "1.1.2", "http-proxy-middleware": "3.0.5", "iso-639-1": "3.1.5", - "lodash": "4.17.21", + "lodash-es": "4.17.21", "msw": "2.14.6", "nightwatch": "3.12.2", "oxc": "^1.0.1", diff --git a/yarn.lock b/yarn.lock index 8644d2d53..4ca508961 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,7 +20,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.26.2": +"@babel/code-frame@^7.0.0": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -113,17 +113,6 @@ eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.0.tgz#764382b5392e5b9aff93cadb190d0745866cbc2c" - integrity sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw== - dependencies: - "@babel/parser" "^7.27.0" - "@babel/types" "^7.27.0" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^3.0.2" - "@babel/generator@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" @@ -281,14 +270,6 @@ "@babel/traverse" "^7.29.7" "@babel/types" "^7.29.7" -"@babel/helper-module-imports@^7.0.0-beta.49": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/helper-module-imports@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" @@ -479,13 +460,6 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" - integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== - dependencies: - "@babel/types" "^7.27.0" - "@babel/parser@^7.27.2", "@babel/parser@^7.28.0", "@babel/parser@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" @@ -1131,15 +1105,6 @@ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== -"@babel/template@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" - integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== - dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/parser" "^7.27.0" - "@babel/types" "^7.27.0" - "@babel/template@^7.27.1", "@babel/template@^7.27.2": version "7.27.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" @@ -1158,19 +1123,6 @@ "@babel/parser" "^7.29.7" "@babel/types" "^7.29.7" -"@babel/traverse@^7.25.9": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.0.tgz#11d7e644779e166c0442f9a07274d02cd91d4a70" - integrity sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA== - dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/generator" "^7.27.0" - "@babel/parser" "^7.27.0" - "@babel/template" "^7.27.0" - "@babel/types" "^7.27.0" - debug "^4.3.1" - globals "^11.1.0" - "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.3.tgz#6911a10795d2cce43ec6a28cffc440cca2593434" @@ -1223,7 +1175,7 @@ "@babel/types" "^7.29.7" debug "^4.3.1" -"@babel/types@^7.0.0-beta.49", "@babel/types@^7.25.9", "@babel/types@^7.27.0", "@babel/types@^7.4.4": +"@babel/types@^7.25.9", "@babel/types@^7.4.4": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== @@ -1804,7 +1756,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": version "0.3.30" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99" integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q== @@ -3393,17 +3345,6 @@ axios@^1.7.4: form-data "^4.0.0" proxy-from-env "^1.1.0" -babel-plugin-lodash@3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz#4f6844358a1340baed182adbeffa8df9967bc196" - integrity sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg== - dependencies: - "@babel/helper-module-imports" "^7.0.0-beta.49" - "@babel/types" "^7.0.0-beta.49" - glob "^7.1.1" - lodash "^4.17.10" - require-package-name "^2.0.1" - babel-plugin-polyfill-corejs2@^0.4.14: version "0.4.14" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz#8101b82b769c568835611542488d463395c2ef8f" @@ -5404,7 +5345,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@7.2.3, glob@^7.1.1, glob@^7.1.4, glob@^7.2.3: +glob@7.2.3, glob@^7.1.4, glob@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5455,11 +5396,6 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - globals@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" @@ -6488,7 +6424,7 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== -lodash@4.17.21, lodash@^4.17.10, lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7763,11 +7699,6 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -require-package-name@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9" - integrity sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q== - requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" From d8825bae3355bebf43c6a954f8e12aa9faf51d06 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 31 Aug 2026 18:44:30 +0300 Subject: [PATCH 02/39] yarn lock --- yarn.lock | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 4ca508961..4913bcfb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6379,6 +6379,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -6424,7 +6429,7 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== -lodash@4.17.21, lodash@^4.17.21: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== From 8a743c648c77378a92993b230d4c4f587c243fee Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 31 Aug 2026 19:05:26 +0300 Subject: [PATCH 03/39] migrate follow requests to separate pinia store --- .../extra_notifications.js | 6 +-- .../extra_notifications.vue | 2 +- .../follow_request_card.js | 18 ++++---- .../follow_requests/follow_requests.js | 4 +- src/components/mobile_nav/mobile_nav.js | 3 +- src/components/nav_panel/nav_panel.js | 6 +-- src/components/navigation/navigation.js | 2 +- src/components/navigation/navigation_pins.js | 6 +-- src/components/notifications/notifications.js | 3 +- src/components/side_drawer/side_drawer.js | 5 +-- src/components/side_drawer/side_drawer.vue | 4 +- src/modules/api.js | 26 ----------- .../follow_request_fetcher.service.js | 33 -------------- .../notification_utils/notification_utils.js | 10 ++--- src/stores/fetchers/follow_requests.js | 44 +++++++++++++++++++ src/stores/follow_requests.js | 37 ++++++++++++++++ src/stores/users.js | 11 +++-- 17 files changed, 121 insertions(+), 99 deletions(-) delete mode 100644 src/services/follow_request_fetcher/follow_request_fetcher.service.js create mode 100644 src/stores/fetchers/follow_requests.js create mode 100644 src/stores/follow_requests.js diff --git a/src/components/extra_notifications/extra_notifications.js b/src/components/extra_notifications/extra_notifications.js index 851656ae5..d20cbb67e 100644 --- a/src/components/extra_notifications/extra_notifications.js +++ b/src/components/extra_notifications/extra_notifications.js @@ -1,8 +1,8 @@ import { mapState } from 'pinia' -import { mapGetters } from 'vuex' import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useChatsStore } from 'src/stores/chats.js' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' @@ -37,7 +37,7 @@ const ExtraNotifications = { return ( this.mergedConfig.showExtraNotifications && this.mergedConfig.showFollowRequestsInExtraNotifications && - this.followRequestCount + this.followRequestsCount ) }, hasAnythingToShow() { @@ -55,12 +55,12 @@ const ExtraNotifications = { currentUser() { return useUsersStore().currentUser }, - ...mapGetters(['followRequestCount']), ...mapState(useAnnouncementsStore, { unreadAnnouncementCount: 'unreadAnnouncementCount', }), ...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useChatsStore, ['unreadChatsCount']), + ...mapState(useFollowRequestsStore, ['followRequestsCount']), }, methods: { openNotificationSettings() { diff --git a/src/components/extra_notifications/extra_notifications.vue b/src/components/extra_notifications/extra_notifications.vue index 388435950..329c5c1c3 100644 --- a/src/components/extra_notifications/extra_notifications.vue +++ b/src/components/extra_notifications/extra_notifications.vue @@ -48,7 +48,7 @@ class="fa-scale-110 icon" icon="user-plus" /> - {{ $t('notifications.unread_follow_requests', { num: followRequestCount }, followRequestCount) }} + {{ $t('notifications.unread_follow_requests', { num: followRequestsCount }, followRequestsCount) }} { + const notifId = this.findFollowRequestNotificationId() + useFollowRequestsStore().remove(this.user.id) + notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId) }) - // TODO fix - this.$store.dispatch('removeFollowRequest', this.user) - - const notifId = this.findFollowRequestNotificationId() - useNotificationsStore().markSingleNotificationAsSeen(notifId) this.hideApproveConfirmDialog() }, denyUser() { @@ -70,15 +70,13 @@ const FollowRequestCard = { } }, doDeny() { - const notifId = this.findFollowRequestNotificationId() - denyUser({ id: this.user.id, credentials: useOAuthStore().token, }).then(() => { - useNotificationsStore().dismissNotificationLocal(notifId) - // TODO fix - this.$store.dispatch('removeFollowRequest', this.user) + const notifId = this.findFollowRequestNotificationId() + useFollowRequestsStore().remove(this.user.id) + notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId) }) this.hideDenyConfirmDialog() }, diff --git a/src/components/follow_requests/follow_requests.js b/src/components/follow_requests/follow_requests.js index 513298afc..5936e2303 100644 --- a/src/components/follow_requests/follow_requests.js +++ b/src/components/follow_requests/follow_requests.js @@ -1,3 +1,5 @@ +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' + import FollowRequestCard from 'src/components/follow_request_card/follow_request_card.vue' const FollowRequests = { @@ -6,7 +8,7 @@ const FollowRequests = { }, computed: { requests() { - return this.$store.state.api.followRequests + return useFollowRequestsStore().requests.values() }, }, } diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 97645ca73..e7fe404e0 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -10,6 +10,7 @@ import { import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useChatsStore } from 'src/stores/chats.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' @@ -67,10 +68,10 @@ const MobileNav = { return ( this.unseenNotifications.length + countExtraNotifications( - this.$store, useMergedConfigStore().mergedConfig, useChatsStore().unreadChatsCount, useAnnouncementsStore().unreadAnnouncementCount, + useFollowRequestsStore().followRequestsCount, ) ) }, diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index c3e760efb..f5de8bb2c 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -1,5 +1,4 @@ import { mapState } from 'pinia' -import { mapState as mapVuexState } from 'vuex' import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' @@ -11,6 +10,7 @@ import NavigationPins from 'src/components/navigation/navigation_pins.vue' import { useAnnouncementsStore } from 'src/stores/announcements' import { useChatsStore } from 'src/stores/chats.js' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' @@ -130,9 +130,7 @@ const NavPanel = { new Set(store.prefsStorage.collections.pinnedNavItems), }), ...mapState(useUsersStore, ['currentUser']), - ...mapVuexState({ - followRequestCount: (state) => state.api.followRequests.length, - }), + ...mapState(useFollowRequestsStore, ['followRequestsCount']), ...mapState(useChatsStore, ['unreadChatsCount']), timelinesItems() { return filterNavigation( diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index 39fa2c993..7a2e29646 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -85,7 +85,7 @@ export const ROOT_ITEMS = { label: 'nav.friend_requests', badgeStyle: 'notification', criteria: ['lockedUser'], - badgeGetter: 'followRequestCount', + badgeGetter: 'followRequestsCount', }, about: { route: 'about', diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index 2efa94ee4..f16336ef3 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -1,5 +1,4 @@ import { mapState } from 'pinia' -import { mapState as mapVuexState } from 'vuex' import { filterNavigation, @@ -14,6 +13,7 @@ import { import { useAnnouncementsStore } from 'src/stores/announcements' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useListsStore } from 'src/stores/lists' @@ -78,9 +78,7 @@ const NavPanel = { 'localBubble', ]), ...mapState(useUsersStore, ['currentUser']), - ...mapVuexState({ - followRequestCount: (state) => state.api.followRequests.length, - }), + ...mapState(useFollowRequestsStore, ['followRequestsCount']), pinnedList() { if (!this.currentUser) { return filterNavigation( diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 2272c78af..734671521 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -14,6 +14,7 @@ import NotificationFilters from './notification_filters.vue' import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useChatsStore } from 'src/stores/chats.js' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useNotificationsStore } from 'src/stores/notifications.js' @@ -107,10 +108,10 @@ const Notifications = { }, extraNotificationsCount() { return countExtraNotifications( - this.$store, useMergedConfigStore().mergedConfig, useChatsStore().unreadChatsCount, useAnnouncementsStore().unreadAnnouncementCount, + useFollowRequestsStore().followRequestsCount, ) }, unseenCountTitle() { diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 2d2886d58..40d922228 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -8,6 +8,7 @@ import { unseenNotifications } from '../../services/notification_utils/notificat import { useAnnouncementsStore } from 'src/stores/announcements' import { useChatsStore } from 'src/stores/chats.js' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface' @@ -85,9 +86,6 @@ const SideDrawer = { unseenNotificationsCount() { return this.unseenNotifications.length }, - followRequestCount() { - return this.$store.state.api.followRequests.length - }, timelinesRoute() { let name if (useInterfaceStore().lastTimeline) { @@ -100,6 +98,7 @@ const SideDrawer = { return { name } } }, + ...mapState(useFollowRequestsStore, ['followRequestsCount']), ...mapState(useAnnouncementsStore, [ 'supportsAnnouncements', 'unreadAnnouncementCount', diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index c810d93a0..a42287b3c 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -141,10 +141,10 @@ icon="user-plus" /> {{ $t("nav.friend_requests") }} - {{ followRequestCount }} + {{ followRequestsCount }} diff --git a/src/modules/api.js b/src/modules/api.js index 290fbfc27..1395752e0 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -1,19 +1,12 @@ import { Socket } from 'phoenix' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' -import { useOAuthStore } from 'src/stores/oauth.js' import { useShoutStore } from 'src/stores/shout.js' -import followRequestFetcher from 'src/services/follow_request_fetcher/follow_request_fetcher.service' - const api = { state: { fetchers: {}, socket: null, - followRequests: [], - }, - getters: { - followRequestCount: (state) => state.followRequests.length, }, mutations: { addFetcher(state, { fetcherName, fetcher }) { @@ -29,27 +22,8 @@ const api = { setSocket(state, socket) { state.socket = socket }, - setFollowRequests(state, value) { - state.followRequests = value - }, }, actions: { - // Follow requests - startFetchingFollowRequests(store) { - if (store.state.fetchers.followRequests) return - const fetcher = followRequestFetcher.startFetching({ - store, - credentials: useOAuthStore().token, - }) - - store.commit('addFetcher', { fetcherName: 'followRequests', fetcher }) - }, - stopFetchingFollowRequests(store) { - const fetcher = store.state.fetchers.followRequests - if (!fetcher) return - store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher }) - }, - // Pleroma websocket setWsToken(store, token) { store.commit('setWsToken', token) diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js deleted file mode 100644 index 492c4e648..000000000 --- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js +++ /dev/null @@ -1,33 +0,0 @@ -import { useUsersStore } from 'src/stores/users.js' - -import { fetchFollowRequests } from 'src/api/user.js' -import { promiseInterval } from 'src/services/promise_interval/promise_interval.js' - -const fetchAndUpdate = ({ store, credentials }) => { - return fetchFollowRequests({ credentials }) - .then( - (result) => { - const { data: requests } = result - store.commit('setFollowRequests', requests) - useUsersStore().addNewUsers(result) - }, - (rej) => { - console.error(rej) - }, - ) - .catch((e) => { - console.error(e) - }) -} - -const startFetching = ({ credentials, store }) => { - const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) - boundFetchAndUpdate() - return promiseInterval(boundFetchAndUpdate, 10000) -} - -const followRequestFetcher = { - startFetching, -} - -export default followRequestFetcher diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 1fdb7bec1..803a56322 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -98,13 +98,11 @@ export const unseenNotifications = ( } export const countExtraNotifications = ( - store, mergedConfig, unreadChatsCount, - unreadAnnouncementCount, + unreadAnnouncementsCount, + followRequestsCount, ) => { - const rootGetters = store.rootGetters || store.getters - if (!mergedConfig.showExtraNotifications) { return 0 } @@ -112,10 +110,10 @@ export const countExtraNotifications = ( return [ mergedConfig.showChatsInExtraNotifications ? unreadChatsCount : 0, mergedConfig.showAnnouncementsInExtraNotifications - ? unreadAnnouncementCount + ? unreadAnnouncementsCount : 0, mergedConfig.showFollowRequestsInExtraNotifications - ? rootGetters.followRequestCount + ? followRequestsCount : 0, ].reduce((a, c) => a + c, 0) } diff --git a/src/stores/fetchers/follow_requests.js b/src/stores/fetchers/follow_requests.js new file mode 100644 index 000000000..8794ab07e --- /dev/null +++ b/src/stores/fetchers/follow_requests.js @@ -0,0 +1,44 @@ +import { ref } from 'vue' + +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' +import { useUsersStore } from 'src/stores/users.js' + +import { fetchFollowRequests } from 'src/api/user.js' +import { promiseInterval } from 'src/services/promise_interval/promise_interval.js' + +const followRequestFetcher = ({ credentials }) => { + const interval = ref(null) + + const fetchAndUpdate = () => { + return fetchFollowRequests({ credentials }) + .then((result) => { + const { data: requests } = result + useFollowRequestsStore().setFollowRequests(requests) + useUsersStore().addNewUsers(result) + }) + .catch((e) => { + console.error(e) + }) + } + + const startFetching = () => { + if (interval.value) throw new Error('Interval already exists!') + + fetchAndUpdate() + + interval.value = promiseInterval(fetchAndUpdate, 10000) + } + + const stopFetching = () => { + interval.value.stop() + interval.value = null + } + + return { + fetchAndUpdate, + startFetching, + stopFetching, + } +} + +export default followRequestFetcher diff --git a/src/stores/follow_requests.js b/src/stores/follow_requests.js new file mode 100644 index 000000000..064d57253 --- /dev/null +++ b/src/stores/follow_requests.js @@ -0,0 +1,37 @@ +import { defineStore } from 'pinia' + +import followRequestFetcher from 'src/stores/fetchers/follow_requests.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +export const useFollowRequestsStore = defineStore('followRequests', { + state: () => ({ + fetcher: null, + requests: new Map(), + }), + getters: { + followRequestsCount(state) { + return state.requests.size + }, + }, + actions: { + startFetching() { + if (this.fetcher) throw 'Fetcher already exists!' + + this.fetcher = followRequestFetcher({ + credentials: useOAuthStore().token, + }) + + this.fetcher.startFetching() + }, + stopFetching() { + if (!this.fetcher) throw "Fetcher doesn't exists!" + this.fetcher.stopFetching(), (this.fetcher = null) + }, + setFollowRequests(requests) { + this.requests = new Map(requests.map((user) => [user.id, user])) + }, + remove(id) { + this.requests.delete(id) + } + }, +}) diff --git a/src/stores/users.js b/src/stores/users.js index 90c87c0f3..71a9ddc94 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -6,6 +6,7 @@ import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' import { useChatsStore } from 'src/stores/chats.js' import { useEmojiStore } from 'src/stores/emoji.js' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' @@ -678,9 +679,10 @@ export const useUsersStore = defineStore('users', { useListsStore().startFetching() useBookmarkFoldersStore().startFetching() - if (user.locked) { - dispatch('startFetchingFollowRequests') - } + // if (user.locked) { + dispatch('startFetchingFollowRequests') + useFollowRequestsStore().startFetching() + // } if (useMergedConfigStore().mergedConfig.useStreamingApi) { useStreamingStore().initSocket(true) @@ -724,6 +726,9 @@ export const useUsersStore = defineStore('users', { useListsStore().stopFetching() useBookmarkFoldersStore().stopFetching() useChatsStore().stopFetching() + // if (this.currentUser.locked) { + useFollowRequestsStore().stopFetching() + // } store?.dispatch('stopFetchingFollowRequests') From 36cb7c1aa440df979ccaedce0263896bceb1b8da Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 31 Aug 2026 19:15:55 +0300 Subject: [PATCH 04/39] undo debug --- src/stores/users.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/stores/users.js b/src/stores/users.js index 71a9ddc94..78527b4c5 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -679,10 +679,10 @@ export const useUsersStore = defineStore('users', { useListsStore().startFetching() useBookmarkFoldersStore().startFetching() - // if (user.locked) { - dispatch('startFetchingFollowRequests') - useFollowRequestsStore().startFetching() - // } + if (user.locked) { + dispatch('startFetchingFollowRequests') + useFollowRequestsStore().startFetching() + } if (useMergedConfigStore().mergedConfig.useStreamingApi) { useStreamingStore().initSocket(true) @@ -726,9 +726,9 @@ export const useUsersStore = defineStore('users', { useListsStore().stopFetching() useBookmarkFoldersStore().stopFetching() useChatsStore().stopFetching() - // if (this.currentUser.locked) { - useFollowRequestsStore().stopFetching() - // } + if (this.currentUser.locked) { + useFollowRequestsStore().stopFetching() + } store?.dispatch('stopFetchingFollowRequests') From d6c6c44e894e0b1746a3d0d592f8a9458269cf4b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 31 Aug 2026 19:22:12 +0300 Subject: [PATCH 05/39] move phoenix socket stuff into shoutstore, removing api vuex module --- src/modules/api.js | 53 -------------------------------------------- src/modules/index.js | 2 -- src/stores/shout.js | 29 +++++++++++++++++++++--- src/stores/users.js | 6 +++-- 4 files changed, 30 insertions(+), 60 deletions(-) delete mode 100644 src/modules/api.js diff --git a/src/modules/api.js b/src/modules/api.js deleted file mode 100644 index 1395752e0..000000000 --- a/src/modules/api.js +++ /dev/null @@ -1,53 +0,0 @@ -import { Socket } from 'phoenix' - -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' -import { useShoutStore } from 'src/stores/shout.js' - -const api = { - state: { - fetchers: {}, - socket: null, - }, - mutations: { - addFetcher(state, { fetcherName, fetcher }) { - state.fetchers[fetcherName] = fetcher - }, - removeFetcher(state, { fetcherName }) { - state.fetchers[fetcherName].stop() - delete state.fetchers[fetcherName] - }, - setWsToken(state, token) { - state.wsToken = token - }, - setSocket(state, socket) { - state.socket = socket - }, - }, - actions: { - // Pleroma websocket - setWsToken(store, token) { - store.commit('setWsToken', token) - }, - initializeSocket({ commit, state, rootState }) { - // Set up websocket connection - const token = state.wsToken - if ( - useInstanceCapabilitiesStore().shoutAvailable && - token !== undefined && - state.socket === null - ) { - const socket = new Socket('/socket', { params: { token } }) - socket.connect() - - commit('setSocket', socket) - useShoutStore().initializeShout(socket) - } - }, - disconnectFromSocket({ commit, state }) { - state.socket?.disconnect() - commit('setSocket', null) - }, - }, -} - -export default api diff --git a/src/modules/index.js b/src/modules/index.js index 6aa236257..61b7fd996 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -1,9 +1,7 @@ -import api from './api.js' import drafts from './drafts.js' import profileConfig from './profileConfig.js' export default { - api, profileConfig, drafts, } diff --git a/src/stores/shout.js b/src/stores/shout.js index 79268bd57..f76b37f5e 100644 --- a/src/stores/shout.js +++ b/src/stores/shout.js @@ -1,14 +1,33 @@ -import { defineStore } from 'pinia' +import { Socket } from 'phoenix' +import { defineStore } from 'pinia' +import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' +import { useUsersStore } from 'src/stores/users.js' + +// Maybe rename it to PhoenixSocket if we ever utilize this socket more export const useShoutStore = defineStore('shout', { state: () => ({ messages: [], channel: { state: '' }, joined: false, + token: null, + socket: null, }), + getters: { + token: () => useUsersStore().currentUser?.token + }, actions: { - initializeShout(socket) { - const channel = socket.channel('chat:public') + initializeSocket() { + if (this.token === null) return + if (!useInstanceCapabilitiesStore().shoutAvailable) return + if (this.socket !== null) throw new Error('Shout socket already exist!') + + this.socket = new Socket('/socket', { params: { token: this.token } }) + this.socket.connect() + }, + initializeShout() { + const channel = this.socket.channel('chat:public') + channel.joinPush.receive('ok', () => { this.joined = true }) @@ -28,5 +47,9 @@ export const useShoutStore = defineStore('shout', { channel.join() this.channel = channel }, + disconnectSocket() { + this.socket?.disconnect() + this.socket = null + } }, }) diff --git a/src/stores/users.js b/src/stores/users.js index 78527b4c5..305d0fc5b 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -11,6 +11,7 @@ import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useListsStore } from 'src/stores/lists.js' +import { useShoutStore } from 'src/stores/shout.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useNotificationsStore } from 'src/stores/notifications.js' import { useOAuthStore } from 'src/stores/oauth.js' @@ -663,8 +664,8 @@ export const useUsersStore = defineStore('users', { if (user.token) { // Shoutbox - dispatch('setWsToken', user.token) - dispatch('initializeSocket') + useShoutStore().initializeSocket() + useShoutStore().initializeShout() } // DMs and Home @@ -747,6 +748,7 @@ export const useUsersStore = defineStore('users', { }) .then(() => { oauth.clearToken() + useShoutStore().disconnectSocket() this.currentUser = null From 316bf4c226faea994e6da703eefe3c5d95a8b22c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 1 Sep 2026 18:02:38 +0300 Subject: [PATCH 06/39] migrate and fix profileConfig --- src/api/user.js | 6 +- .../follow_requests/follow_requests.js | 4 +- .../settings_modal/helpers/setting.js | 5 +- .../settings_modal/tabs/composing_tab.js | 6 +- .../settings_modal/tabs/composing_tab.vue | 6 +- src/modules/index.js | 2 - src/stores/follow_requests.js | 2 +- .../profile_config.js} | 113 ++++++++++-------- src/stores/shout.js | 6 +- src/stores/users.js | 6 +- 10 files changed, 85 insertions(+), 71 deletions(-) rename src/{modules/profileConfig.js => stores/profile_config.js} (62%) diff --git a/src/api/user.js b/src/api/user.js index ec1763bb4..430b2c895 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -17,7 +17,8 @@ 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 = '/api/pleroma/notification_settings' +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' @@ -432,10 +433,9 @@ export const exportFriends = ({ id, credentials }) => { // #Profile settings export const updateNotificationSettings = ({ credentials, settings }) => { return promisedRequest({ - url: NOTIFICATION_SETTINGS_URL, + url: NOTIFICATION_SETTINGS_URL(settings), credentials, method: 'PUT', - payload: settings, }) } diff --git a/src/components/follow_requests/follow_requests.js b/src/components/follow_requests/follow_requests.js index 5936e2303..181d5605e 100644 --- a/src/components/follow_requests/follow_requests.js +++ b/src/components/follow_requests/follow_requests.js @@ -1,7 +1,7 @@ -import { useFollowRequestsStore } from 'src/stores/follow_requests.js' - import FollowRequestCard from 'src/components/follow_request_card/follow_request_card.vue' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' + const FollowRequests = { components: { FollowRequestCard, diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js index a1a946fca..d0dce5950 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -9,6 +9,7 @@ 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' export default { components: { @@ -236,7 +237,7 @@ export default { configSource() { switch (this.realSource) { case 'profile': - return this.$store.state.profileConfig + return useProfileConfigStore().config case 'admin': return useAdminSettingsStore().config default: @@ -253,7 +254,7 @@ export default { switch (this.realSource) { case 'profile': return (k, v) => - this.$store.dispatch('setProfileOption', { name: k, value: v }) + useProfileConfigStore().setProfileOption({ name: k, value: v }) case 'admin': return (k, v) => useAdminSettingsStore().pushAdminSetting({ path: k, value: v }) diff --git a/src/components/settings_modal/tabs/composing_tab.js b/src/components/settings_modal/tabs/composing_tab.js index 3e734e6e9..74f01a2e1 100644 --- a/src/components/settings_modal/tabs/composing_tab.js +++ b/src/components/settings_modal/tabs/composing_tab.js @@ -17,6 +17,7 @@ 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 { useUsersStore } from 'src/stores/users.js' import { updateProfile } from 'src/api/user.js' @@ -109,6 +110,9 @@ const ComposingTab = { FontControl, }, computed: { + defaultScope() { + return useProfileConfigStore().config.defaultScope + }, postFormats() { return useInstanceCapabilitiesStore().postFormats }, @@ -135,7 +139,7 @@ const ComposingTab = { }, methods: { changeDefaultScope(value) { - this.$store.dispatch('setProfileOption', { name: 'defaultScope', value }) + useProfileConfigStore().setProfileOption({ name: 'defaultScope', value }) }, clearCache(key) { clearCache(key) diff --git a/src/components/settings_modal/tabs/composing_tab.vue b/src/components/settings_modal/tabs/composing_tab.vue index b8c156bd3..c110981e6 100644 --- a/src/components/settings_modal/tabs/composing_tab.vue +++ b/src/components/settings_modal/tabs/composing_tab.vue @@ -11,10 +11,10 @@ diff --git a/src/modules/index.js b/src/modules/index.js index 61b7fd996..436d7e844 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -1,7 +1,5 @@ import drafts from './drafts.js' -import profileConfig from './profileConfig.js' export default { - profileConfig, drafts, } diff --git a/src/stores/follow_requests.js b/src/stores/follow_requests.js index 064d57253..27f8cbc00 100644 --- a/src/stores/follow_requests.js +++ b/src/stores/follow_requests.js @@ -32,6 +32,6 @@ export const useFollowRequestsStore = defineStore('followRequests', { }, remove(id) { this.requests.delete(id) - } + }, }, }) diff --git a/src/modules/profileConfig.js b/src/stores/profile_config.js similarity index 62% rename from src/modules/profileConfig.js rename to src/stores/profile_config.js index 8b9c77425..7533575d0 100644 --- a/src/modules/profileConfig.js +++ b/src/stores/profile_config.js @@ -1,34 +1,36 @@ import { get, set } from 'lodash' +import { defineStore } from 'pinia' import { useOAuthStore } from 'src/stores/oauth.js' import { useUsersStore } from 'src/stores/users.js' -import { updateNotificationSettings, updateProfile } from 'src/api/user.js' +import { updateNotificationSettings, updateProfileJSON } from 'src/api/user.js' -const defaultApi = ({ rootState, commit }, { path, value }) => { +const defaultApi = async ({ path, value }) => { const params = {} set(params, path, value) - return updateProfile({ + + return await updateProfileJSON({ params, credentials: useOAuthStore().token, - }).then((result) => { - useUsersStore().addNewUsers(result) }) } -const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => { +const notificationsApi = async ({ path, value, oldValue }) => { const settings = {} set(settings, path, value) - return updateNotificationSettings({ + + const result = await updateNotificationSettings({ settings, credentials: useOAuthStore().token, - }).then(({ data: result }) => { - if (result.status === 'success') { - commit('confirmProfileOption', { name, value }) - } else { - commit('confirmProfileOption', { name, value: oldValue }) - } }) + + if (result.data.status === 'success') { + // a bit of a hack + return { ...result, success: true } + } else { + throw new Error('Failed updating notification settings', result) + } } /** @@ -84,60 +86,65 @@ export const settingsMap = { // NotificationSettingsAPIs webPushHideContents: { get: 'pleroma.notification_settings.hide_notification_contents', - set: 'hide_notification_contents', + set: 'hideNotificationContents', api: notificationsApi, }, blockNotificationsFromStrangers: { get: 'pleroma.notification_settings.block_from_strangers', - set: 'block_from_strangers', + set: 'blockFromStrangers', api: notificationsApi, }, } -export const defaultState = Object.fromEntries( - Object.keys(settingsMap).map((key) => [key, null]), -) +export const defaultState = () => ({ + config: Object.fromEntries(Object.keys(settingsMap).map((key) => [key, null])) +}) -const profileConfig = { - state: { ...defaultState }, - mutations: { - confirmProfileOption(state, { name, value }) { - set(state, name, value) - }, - wipeProfileOption(state, { name }) { - set(state, name, null) - }, - wipeAllProfileOptions(state) { - Object.keys(settingsMap).forEach((key) => { - set(state, key, null) - }) +export const useProfileConfigStore = defineStore('profileConfig', { + state: defaultState, + actions: { + confirmProfileOption({ name, value }) { + set(this.config, name, value) }, // Set the settings based on their path location - setCurrentUser(state, user) { + async setProfileOption({ name, value }) { + const oldValue = get(this, name) + const map = settingsMap[name] + + if (!map) throw new Error('Invalid server-side setting') + const { set: path = map, api = defaultApi } = map + set(this.config, name, null) + + try { + const result = await api({ path, value, oldValue }) + const { success } = result + if (success) { + set(this.config, name, value) + return + } + + useUsersStore().addNewUsers(result) + this.update(user) + } catch (e) { + console.warn('Error setting server-side option:', e) + + set(this.config, name, oldValue) + } + }, + update(user) { Object.entries(settingsMap).forEach((map) => { const [name, value] = map const { get: path = value } = value - set(state, name, get(user._original, path)) + set(this.config, name, get(user._original, path)) + }) + }, + onLogin(user) { + this.update(user) + }, + onLogout() { + Object.keys(settingsMap).forEach((key) => { + set(this.config, key, null) }) }, }, - actions: { - setProfileOption({ rootState, state, commit }, { name, value }) { - const oldValue = get(state, name) - const map = settingsMap[name] - if (!map) throw new Error('Invalid server-side setting') - const { set: path = map, api = defaultApi } = map - commit('wipeProfileOption', { name }) - - api({ rootState, commit }, { path, value, oldValue }).catch((e) => { - console.warn('Error setting server-side option:', e) - commit('confirmProfileOption', { name, value: oldValue }) - }) - }, - logout({ commit }) { - commit('wipeAllProfileOptions') - }, - }, -} - -export default profileConfig +}) diff --git a/src/stores/shout.js b/src/stores/shout.js index f76b37f5e..487b04c4d 100644 --- a/src/stores/shout.js +++ b/src/stores/shout.js @@ -1,6 +1,6 @@ import { Socket } from 'phoenix' - import { defineStore } from 'pinia' + import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useUsersStore } from 'src/stores/users.js' @@ -14,7 +14,7 @@ export const useShoutStore = defineStore('shout', { socket: null, }), getters: { - token: () => useUsersStore().currentUser?.token + token: () => useUsersStore().currentUser?.token, }, actions: { initializeSocket() { @@ -50,6 +50,6 @@ export const useShoutStore = defineStore('shout', { disconnectSocket() { this.socket?.disconnect() this.socket = null - } + }, }, }) diff --git a/src/stores/users.js b/src/stores/users.js index 87b177dfa..da35dc3a0 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -11,13 +11,14 @@ import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useListsStore } from 'src/stores/lists.js' -import { useShoutStore } from 'src/stores/shout.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useNotificationsStore } from 'src/stores/notifications.js' import { useOAuthStore } from 'src/stores/oauth.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' @@ -142,6 +143,7 @@ export const useUsersStore = defineStore('users', { if (user.id === this.currentUser?.id) { this.currentUser = reactive + useProfileConfigStore().update(reactive) } // Initialize some stuff @@ -648,6 +650,7 @@ export const useUsersStore = defineStore('users', { console.error('Error setting theme', e) }) }) + useProfileConfigStore().onLogin(user) useUserHighlightStore().initUserHighlight(user) @@ -780,6 +783,7 @@ export const useUsersStore = defineStore('users', { Cookies.remove('__Host-pleroma_key', { path: '/' }) useInterfaceStore().onLogout() + useProfileConfigStore().onLogout() }) .catch((e) => { useInterfaceStore().pushGlobalNotice({ From 2467e5655688fff618ee3be7c34595fff52212a1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 16:16:05 +0300 Subject: [PATCH 07/39] 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 08/39] 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 09/39] 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 10/39] 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 11/39] 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 12/39] 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 13/39] 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 14/39] 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 15/39] 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 16/39] 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 17/39] 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 18/39] 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 19/39] 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 20/39] 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 21/39] 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 22/39] 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 71a9851881734802fea26796f86275ba5e8e76b3 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 2 Sep 2026 22:34:28 +0300 Subject: [PATCH 23/39] fix --- 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] }} -
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 65acfed5c..cff7f655f 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -1,5 +1,4 @@ -import { mapState } from 'pinia' -import { defineAsyncComponent } from 'vue' +import { mapActions, mapState } from 'pinia' import Report from 'src/components/report/report.vue' import StatusContent from 'src/components/status_content/status_content.vue' @@ -16,13 +15,10 @@ import { 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' -import { useOAuthStore } from 'src/stores/oauth.js' import { useStatusesStore } from 'src/stores/statuses.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUsersStore } from 'src/stores/users.js' -import { approveUser, denyUser } from 'src/api/user.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { library } from '@fortawesome/fontawesome-svg-core' @@ -58,8 +54,6 @@ const Notification = { selecting: false, statusExpanded: false, unmuted: false, - showingApproveConfirmDialog: false, - showingDenyConfirmDialog: false, } }, props: ['notification'], @@ -73,9 +67,6 @@ const Notification = { UserPopover, UserLink, - ConfirmModal: defineAsyncComponent( - () => import('src/components/confirm_modal/confirm_modal.vue'), - ), }, mounted() { document.addEventListener('selectionchange', this.onContentSelect) @@ -125,53 +116,7 @@ const Notification = { toggleMute() { this.unmuted = !this.unmuted }, - showApproveConfirmDialog() { - this.showingApproveConfirmDialog = true - }, - hideApproveConfirmDialog() { - this.showingApproveConfirmDialog = false - }, - showDenyConfirmDialog() { - this.showingDenyConfirmDialog = true - }, - hideDenyConfirmDialog() { - this.showingDenyConfirmDialog = false - }, - approveUser() { - if (this.shouldConfirmApprove) { - this.showApproveConfirmDialog() - } else { - this.doApprove() - } - }, - doApprove() { - approveUser({ - id: this.user.id, - credentials: useOAuthStore().token, - }) - useFollowRequestsStore().remove(this.user.id) - useNotificationsStore().markSingleNotificationAsSeen(this.notification.id) - this.hideApproveConfirmDialog() - }, - denyUser() { - if (this.shouldConfirmDeny) { - this.showDenyConfirmDialog() - } else { - this.doDeny() - } - }, - doDeny() { - denyUser({ - id: this.user.id, - credentials: useOAuthStore().token, - }).then(() => { - useNotificationsStore().markSingleNotificationAsSeen( - this.notification.id, - ) - useFollowRequestsStore().remove(this.user.id) - }) - this.hideDenyConfirmDialog() - }, + ...mapActions(useFollowRequestsStore, ['approve', 'deny']), }, computed: { status() { @@ -222,12 +167,6 @@ const Notification = { scaleMfm() { return this.mergedConfig.scaleMfm }, - shouldConfirmApprove() { - return this.mergedConfig.modalOnApproveFollow - }, - shouldConfirmDeny() { - return this.mergedConfig.modalOnDenyFollow - }, ...mapState(useUsersStore, ['currentUser']), }, } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 601e574db..156c9ac11 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -226,7 +226,7 @@
- - - {{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }} - - - {{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }} - - diff --git a/src/stores/fetchers/follow_requests.js b/src/stores/fetchers/follow_requests.js index 8794ab07e..c48b289ef 100644 --- a/src/stores/fetchers/follow_requests.js +++ b/src/stores/fetchers/follow_requests.js @@ -24,8 +24,6 @@ const followRequestFetcher = ({ credentials }) => { const startFetching = () => { if (interval.value) throw new Error('Interval already exists!') - fetchAndUpdate() - interval.value = promiseInterval(fetchAndUpdate, 10000) } diff --git a/src/stores/follow_requests.js b/src/stores/follow_requests.js index e30d7d4ca..647821782 100644 --- a/src/stores/follow_requests.js +++ b/src/stores/follow_requests.js @@ -1,12 +1,19 @@ import { defineStore } from 'pinia' import followRequestFetcher from 'src/stores/fetchers/follow_requests.js' +import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useNotificationsStore } from 'src/stores/notifications.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { approveUser, denyUser } from 'src/api/user.js' + export const useFollowRequestsStore = defineStore('followRequests', { state: () => ({ fetcher: null, requests: new Map(), + showingApproveConfirmDialog: false, + showingDenyConfirmDialog: false, + tempId: null, }), getters: { followRequestsCount(state) { @@ -14,6 +21,7 @@ export const useFollowRequestsStore = defineStore('followRequests', { }, }, actions: { + // Fetcher stuff startFetching() { if (this.fetcher) throw new Error('Fetcher already exists!') @@ -31,8 +39,76 @@ export const useFollowRequestsStore = defineStore('followRequests', { setFollowRequests(requests) { this.requests = new Map(requests.map((user) => [user.id, user])) }, - remove(id) { + + // Confirm dialogs + showApproveConfirmDialog(id) { + this.showingApproveConfirmDialog = true + this.tempId = id + }, + showDenyConfirmDialog(id) { + this.showingDenyConfirmDialog = true + this.tempId = id + }, + hideApproveConfirmDialog() { + this.showingApproveConfirmDialog = false + this.tempId = null + }, + hideDenyConfirmDialog() { + this.showingDenyConfirmDialog = false + this.tempId = null + }, + + // Dialog/Instant fork + approve(id) { + if (useMergedConfigStore().mergedConfig.modalOnApproveFollow) { + this.showApproveConfirmDialog(id) + } else { + this.doApprove(id) + } + }, + deny(id) { + if (useMergedConfigStore().mergedConfig.modalOnDenyFollow) { + this.showDenyConfirmDialog(id) + } else { + this.doDeny(id) + } + }, + + // Actual calls + async doApprove(userId) { + const id = userId ?? this.tempId + this.hideApproveConfirmDialog() + + await approveUser({ + id, + credentials: useOAuthStore().token, + }) + + const notifId = this.findFollowRequestNotificationId(id) + notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId) this.requests.delete(id) }, + async doDeny(userId) { + const id = userId ?? this.tempId + this.hideDenyConfirmDialog() + + await denyUser({ + id, + credentials: useOAuthStore().token, + }) + + const notifId = this.findFollowRequestNotificationId(id) + notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId) + this.requests.delete(id) + }, + + // Utility + findFollowRequestNotificationId(userId) { + const notif = useNotificationsStore().data.find( + (notif) => + notif.from_profile.id === userId && notif.type === 'follow_request', + ) + return notif?.id + }, }, }) diff --git a/test/unit/specs/stores/drafts.spec.js b/test/unit/specs/stores/drafts.spec.js index e9b7b9614..9beb4587f 100644 --- a/test/unit/specs/stores/drafts.spec.js +++ b/test/unit/specs/stores/drafts.spec.js @@ -49,15 +49,39 @@ describe('Drafts store', () => { it('draftsByTypeAndRefId', async () => { const store = useDraftsStore() - await store.addOrSaveDraft({ id: 1, type: 'edit', refId: 'e1', status: 'draft' }) - await store.addOrSaveDraft({ id: 2, type: 'reply', refId: 'r1', status: 'draft' }) + await store.addOrSaveDraft({ + id: 1, + type: 'edit', + refId: 'e1', + status: 'draft', + }) + await store.addOrSaveDraft({ + id: 2, + type: 'reply', + refId: 'r1', + status: 'draft', + }) await store.addOrSaveDraft({ id: 3, status: 'draft' }) - await store.addOrSaveDraft({ id: 4, type: 'edit', refId: 'e2', status: 'draft' }) - await store.addOrSaveDraft({ id: 5, type: 'reply', refId: 'r2', status: 'draft' }) + await store.addOrSaveDraft({ + id: 4, + type: 'edit', + refId: 'e2', + status: 'draft', + }) + await store.addOrSaveDraft({ + id: 5, + type: 'reply', + refId: 'r2', + status: 'draft', + }) expect(store.draftsByTypeAndRefId).to.be.a('function') - expect(store.draftsByTypeAndRefId('edit', 'e1')).to.eql([{ id: 1, type: 'edit', refId: 'e1', status: 'draft' }]) - expect(store.draftsByTypeAndRefId('reply', 'r1')).to.eql([{ id: 2, type: 'reply', refId: 'r1', status: 'draft' }]) + expect(store.draftsByTypeAndRefId('edit', 'e1')).to.eql([ + { id: 1, type: 'edit', refId: 'e1', status: 'draft' }, + ]) + expect(store.draftsByTypeAndRefId('reply', 'r1')).to.eql([ + { id: 2, type: 'reply', refId: 'r1', status: 'draft' }, + ]) }) }) @@ -112,7 +136,7 @@ describe('Drafts store', () => { [id]: { id, status: 'draft', - } + }, }) }) @@ -128,10 +152,10 @@ describe('Drafts store', () => { expect(storage.getItem).to.have.been.calledTwice expect(storage.setItem).to.have.been.calledTwice expect(storage.setItem).to.have.been.calledWith('pleroma-fe-drafts', { - '1': { + 1: { id: '1', status: 'draft', - } + }, }) }) }) @@ -157,14 +181,14 @@ describe('Drafts store', () => { expect(storage.getItem).to.have.been.calledOnce expect(storage.setItem).to.have.been.calledOnce expect(storage.setItem).to.have.been.calledWith('pleroma-fe-drafts', { - 'a': { + a: { id: 'a', status: 'draft', }, - 'c': { + c: { id: 'c', status: 'draft', - } + }, }) }) }) diff --git a/test/unit/specs/stores/follow_requests.spec.js b/test/unit/specs/stores/follow_requests.spec.js new file mode 100644 index 000000000..d2b81680b --- /dev/null +++ b/test/unit/specs/stores/follow_requests.spec.js @@ -0,0 +1,338 @@ +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' + +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' +import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useNotificationsStore } from 'src/stores/notifications.js' +import { useUsersStore } from 'src/stores/users.js' + +import * as USER_API from 'src/api/user.js' + +const mockMastoAPIUser = ({ + screen_name = 'u1', + name = 'user1', + url = 'http://localhost/u1', + id = 'u1', +} = {}) => ({ + id, + acct: screen_name, + display_name: name, + fields: [], + avatar: '', + url, + pleroma: { + emoji_reactions: [], + }, +}) + +describe('Follow Requests store', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + vi.resetAllMocks() + }) + + describe('Getters', () => { + it('followRequestsCount returns total number of follow requests', async () => { + const store = useFollowRequestsStore() + + store.requests = new Map([ + ['1', {}], + ['2', {}], + ]) + + expect(store).to.have.property('followRequestsCount', 2) + }) + }) + + describe('Actions', () => { + describe('Fetcher stuff', () => { + it('startFetching should initialize fetcher and fetch some data', async () => { + const store = useFollowRequestsStore() + const mockFetch = vi.fn() + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify([mockMastoAPIUser()]), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify([mockMastoAPIUser()]), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + vi.stubGlobal('fetch', mockFetch) + + store.startFetching() + expect(store.fetcher).to.not.be.null + + await vi.advanceTimersToNextTimerAsync() + expect(mockFetch).to.have.been.calledOnce + + await vi.advanceTimersToNextTimerAsync() + expect(mockFetch).to.have.been.calledTwice + expect(useUsersStore().findUser('u1')).to.not.be.undefined + expect(store.requests.get('u1')).to.not.be.undefined + }) + + it('stopFetching should stop and remove the fetcher', async () => { + const store = useFollowRequestsStore() + const mockFetch = vi.fn() + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify([mockMastoAPIUser()]), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify([mockMastoAPIUser()]), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + vi.stubGlobal('fetch', mockFetch) + + store.startFetching() + expect(store.fetcher).to.not.be.null + + store.stopFetching() + expect(store.fetcher).to.be.null + }) + }) + + describe.each(['Approve', 'Deny'])('%s', (intent) => { + const doCall = `do${intent}` + const apiCall = USER_API[`MASTODON_${intent.toUpperCase()}_USER_URL`] + const forkCall = intent.toLowerCase() + const forkProperty = `modalOn${intent}Follow` + const modalProperty = `showing${intent}ConfirmDialog` + const modalCalls = ['show', 'hide'].map( + (vis) => `${vis}${intent}ConfirmDialog`, + ) + + + describe('Dialog calls', () => { + it(`${modalCalls[0]} should show dialog and set tempId`, async () => { + const store = useFollowRequestsStore() + + await store[modalCalls[0]]('u13') + + expect(store).to.have.property(modalProperty, true) + expect(store).to.have.property('tempId', 'u13') + }) + + it(`${modalCalls[1]} should hide dialog and clear tempId`, async () => { + const store = useFollowRequestsStore() + + await store[modalCalls[1]]() + + expect(store).to.have.property(modalProperty, false) + expect(store).to.have.property('tempId', null) + }) + }) + + describe('Fork calls', () => { + it(`Should call ${doCall} if confirmations are disabled (${forkProperty} = false)`, async () => { + const store = useFollowRequestsStore() + const modalSpy = vi.spyOn(store, modalCalls[0]).mockImplementation(() => ({})) + const apiSpy = vi.spyOn(store, doCall).mockImplementation(() => ({})) + useMergedConfigStore().mergedConfig = { [forkProperty]: false } + + await store[forkCall]('u23') + + expect(modalSpy).to.not.have.been.called + expect(apiSpy).to.have.been.calledOnce + expect(apiSpy).to.have.been.calledWith('u23') + }) + + it(`Should call ${modalCalls[0]} if confirmations are enabled (${forkProperty} = true)`, async () => { + const store = useFollowRequestsStore() + const modalSpy = vi.spyOn(store, modalCalls[0]).mockImplementation(() => ({})) + const apiSpy = vi.spyOn(store, doCall).mockImplementation(() => ({})) + useMergedConfigStore().mergedConfig = { [forkProperty]: true } + + await store[forkCall]('u23') + + expect(modalSpy).to.have.been.called + expect(apiSpy).to.not.have.been.calledOnce + }) + }) + + describe('Actual call', () => { + it('Should hide popover', async () => { + const store = useFollowRequestsStore() + const spy = vi.spyOn(store, modalCalls[1]) + const mockFetch = vi.fn() + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify('ok'), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + vi.stubGlobal('fetch', mockFetch) + + await store[doCall]() + + expect(spy).to.have.been.calledOnce + }) + + it('Should call API', async () => { + const store = useFollowRequestsStore() + const mockFetch = vi.fn() + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify('ok'), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + vi.stubGlobal('fetch', mockFetch) + + await store[doCall]('u99') + + expect(mockFetch).to.have.been.calledOnce + expect(mockFetch.mock.calls[0][0]).to.eql(apiCall('u99')) + }) + + it('Should mark notification as seen', async () => { + const store = useFollowRequestsStore() + const mockFetch = vi.fn() + store.findFollowRequestNotificationId = vi.fn() + store.findFollowRequestNotificationId.mockReturnValue('n91') + + const spy = vi.spyOn(useNotificationsStore(), 'markSingleNotificationAsSeen') + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify('ok'), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + vi.stubGlobal('fetch', mockFetch) + + await store[doCall]('u99') + + expect(spy).to.have.been.calledOnce + expect(spy).to.have.been.calledWith('n91') + }) + + it('Should fallback to tempId if no id is provided', async () => { + const store = useFollowRequestsStore() + const mockFetch = vi.fn() + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify('ok'), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + vi.stubGlobal('fetch', mockFetch) + + store.tempId = 'u80' + await store[doCall]() + + expect(mockFetch).to.have.been.calledOnce + expect(mockFetch.mock.calls[0][0]).to.eql(apiCall('u80')) + }) + + it('Should remove request from cache', async () => { + const store = useFollowRequestsStore() + const mockFetch = vi.fn() + + store.requests.set('u95', { id: 'u95' }) + store.requests.set('u96', { id: 'u96' }) + store.requests.set('u97', { id: 'u97' }) + store.requests.set('u98', { id: 'u98' }) + store.requests.set('u99', { id: 'u99' }) + + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify('ok'), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + + vi.stubGlobal('fetch', mockFetch) + + await store[doCall]('u99') + + expect(store.requests).to.have.length(4) + expect(store.requests.get('u99')).to.be.undefined + }) + }) + }) + + describe('Utility', () => { + describe('findFollowRequestNotificationId', () => { + it('should search notifications store for relevant notification', () => { + const store = useFollowRequestsStore() + + useNotificationsStore().data = [ + { + id: 'n4', + from_profile: { id: 'u3' }, + type: 'follow_request', + }, + { + id: 'n3', + from_profile: { id: 'u2' }, + type: 'repeat', + }, + { + id: 'n2', + from_profile: { id: 'u1' }, + type: 'follow_request', + }, + { + id: 'n1', + from_profile: { id: 'u1' }, + type: 'favorite', + }, + ] + + const result = store.findFollowRequestNotificationId('u1') + + expect(result).to.have.eql('n2') + }) + + it("shouldn't crash if there is no notification available", () => { + const store = useFollowRequestsStore() + + useNotificationsStore().data = [ + { + id: 'n4', + from_profile: { id: 'u3' }, + type: 'follow_request', + }, + { + id: 'n3', + from_profile: { id: 'u2' }, + type: 'repeat', + }, + { + id: 'n2', + from_profile: { id: 'u1' }, + type: 'follow_request', + }, + { + id: 'n1', + from_profile: { id: 'u1' }, + type: 'favorite', + }, + ] + + const result = store.findFollowRequestNotificationId('u5') + + expect(result).to.have.eql(undefined) + }) + }) + }) + }) +}) From e682a372d861c473ec2ddfe93decc359683ef64a Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 3 Sep 2026 21:23:14 +0300 Subject: [PATCH 32/39] drive-by: promiseInterval fetches on creation --- src/stores/fetchers/notifications_fetcher.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/stores/fetchers/notifications_fetcher.js b/src/stores/fetchers/notifications_fetcher.js index 5403d59c9..26629f02f 100644 --- a/src/stores/fetchers/notifications_fetcher.js +++ b/src/stores/fetchers/notifications_fetcher.js @@ -118,8 +118,6 @@ const notificationsFetcher = (credentials) => { const startFetching = () => { if (interval.value) throw new Error('Interval already exists!') - fetchAndUpdate() - interval.value = promiseInterval(fetchAndUpdate, 10000) } From 325822287388ed8722e448187e3e86117dc46596 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 3 Sep 2026 21:30:01 +0300 Subject: [PATCH 33/39] lint --- test/unit/specs/stores/follow_requests.spec.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/unit/specs/stores/follow_requests.spec.js b/test/unit/specs/stores/follow_requests.spec.js index d2b81680b..5e15b8004 100644 --- a/test/unit/specs/stores/follow_requests.spec.js +++ b/test/unit/specs/stores/follow_requests.spec.js @@ -117,7 +117,6 @@ describe('Follow Requests store', () => { (vis) => `${vis}${intent}ConfirmDialog`, ) - describe('Dialog calls', () => { it(`${modalCalls[0]} should show dialog and set tempId`, async () => { const store = useFollowRequestsStore() @@ -141,7 +140,9 @@ describe('Follow Requests store', () => { describe('Fork calls', () => { it(`Should call ${doCall} if confirmations are disabled (${forkProperty} = false)`, async () => { const store = useFollowRequestsStore() - const modalSpy = vi.spyOn(store, modalCalls[0]).mockImplementation(() => ({})) + const modalSpy = vi + .spyOn(store, modalCalls[0]) + .mockImplementation(() => ({})) const apiSpy = vi.spyOn(store, doCall).mockImplementation(() => ({})) useMergedConfigStore().mergedConfig = { [forkProperty]: false } @@ -154,7 +155,9 @@ describe('Follow Requests store', () => { it(`Should call ${modalCalls[0]} if confirmations are enabled (${forkProperty} = true)`, async () => { const store = useFollowRequestsStore() - const modalSpy = vi.spyOn(store, modalCalls[0]).mockImplementation(() => ({})) + const modalSpy = vi + .spyOn(store, modalCalls[0]) + .mockImplementation(() => ({})) const apiSpy = vi.spyOn(store, doCall).mockImplementation(() => ({})) useMergedConfigStore().mergedConfig = { [forkProperty]: true } @@ -208,7 +211,10 @@ describe('Follow Requests store', () => { store.findFollowRequestNotificationId = vi.fn() store.findFollowRequestNotificationId.mockReturnValue('n91') - const spy = vi.spyOn(useNotificationsStore(), 'markSingleNotificationAsSeen') + const spy = vi.spyOn( + useNotificationsStore(), + 'markSingleNotificationAsSeen', + ) mockFetch.mockResolvedValueOnce( new Response(JSON.stringify('ok'), { From 67bb642ffa650d193045476a0371e2679214e0ff Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 3 Sep 2026 21:33:03 +0300 Subject: [PATCH 34/39] fix test? --- test/unit/specs/stores/drafts.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/specs/stores/drafts.spec.js b/test/unit/specs/stores/drafts.spec.js index 9beb4587f..1a0667424 100644 --- a/test/unit/specs/stores/drafts.spec.js +++ b/test/unit/specs/stores/drafts.spec.js @@ -123,7 +123,7 @@ describe('Drafts store', () => { describe('addOrSaveDraft', () => { it('create draft', async () => { const store = useDraftsStore() - vi.setSystemTime(new Date(1997, 2, 29)) + vi.setSystemTime(new Date(859586400000)) const id = await store.addOrSaveDraft({ status: 'draft' }) From 6e04b28b719f95b53b526f568d69335ee577627e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 3 Sep 2026 21:51:23 +0300 Subject: [PATCH 35/39] fix tests again --- test/unit/specs/stores/follow_requests.spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/unit/specs/stores/follow_requests.spec.js b/test/unit/specs/stores/follow_requests.spec.js index 5e15b8004..20331a3fe 100644 --- a/test/unit/specs/stores/follow_requests.spec.js +++ b/test/unit/specs/stores/follow_requests.spec.js @@ -222,6 +222,12 @@ describe('Follow Requests store', () => { }), ) + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify('ok'), { + headers: { 'Content-Type': 'application/json' }, + }), + ) + vi.stubGlobal('fetch', mockFetch) await store[doCall]('u99') From da53e35817ca1515bd5fb03c38702c286cab4bac Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 3 Sep 2026 23:19:56 +0300 Subject: [PATCH 36/39] add check --- src/stores/users.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/users.js b/src/stores/users.js index 2eeb31b6e..b487eab46 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -788,7 +788,9 @@ export const useUsersStore = defineStore('users', { useListsStore().startFetching() useBookmarkFoldersStore().startFetching() useChatsStore().startFetching() - useFollowRequestsStore().startFetching() + if (user.locked) { + useFollowRequestsStore().startFetching() + } }) .finally(() => { useNotificationsStore().resume() From 4b04488ce081f8be3674f04500a2cc8772add7fc Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 4 Sep 2026 00:17:24 +0300 Subject: [PATCH 37/39] handle errors --- src/i18n/en.json | 2 ++ src/stores/follow_requests.js | 45 ++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/i18n/en.json b/src/i18n/en.json index 0e861fe77..82c2c2637 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1742,6 +1742,7 @@ "approve_confirm_accept_button": "Approve", "approve_confirm_cancel_button": "Do not approve", "approve_confirm": "Do you want to approve {user}'s follow request?", + "approve_error": "Failure approving follow request: {error}", "block": "Block", "blocked": "Blocked!", "block_confirm_title": "Block confirmation", @@ -1754,6 +1755,7 @@ "deny_confirm_accept_button": "Deny", "deny_confirm_cancel_button": "Do not deny", "deny_confirm": "Do you want to deny {user}'s follow request?", + "deny_error": "Failure denying follow request: {error}", "edit_profile": "Edit profile", "favorites": "Favorites", "follow": "Follow", diff --git a/src/stores/follow_requests.js b/src/stores/follow_requests.js index 647821782..733d5cd63 100644 --- a/src/stores/follow_requests.js +++ b/src/stores/follow_requests.js @@ -4,6 +4,7 @@ import followRequestFetcher from 'src/stores/fetchers/follow_requests.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useNotificationsStore } from 'src/stores/notifications.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { useInterfaceStore } from 'src/stores/interface.js' import { approveUser, denyUser } from 'src/api/user.js' @@ -79,27 +80,43 @@ export const useFollowRequestsStore = defineStore('followRequests', { const id = userId ?? this.tempId this.hideApproveConfirmDialog() - await approveUser({ - id, - credentials: useOAuthStore().token, - }) + try { + await approveUser({ + id, + credentials: useOAuthStore().token, + }) - const notifId = this.findFollowRequestNotificationId(id) - notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId) - this.requests.delete(id) + const notifId = this.findFollowRequestNotificationId(id) + notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId) + this.requests.delete(id) + } catch (error) { + useInterfaceStore().pushGlobalNotice({ + messageKey: 'user_card.approve_error', + messageArgs: { error }, + level: 'error', + }) + } }, async doDeny(userId) { const id = userId ?? this.tempId this.hideDenyConfirmDialog() - await denyUser({ - id, - credentials: useOAuthStore().token, - }) + try { + await denyUser({ + id, + credentials: useOAuthStore().token, + }) - const notifId = this.findFollowRequestNotificationId(id) - notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId) - this.requests.delete(id) + const notifId = this.findFollowRequestNotificationId(id) + notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId) + this.requests.delete(id) + } catch (error) { + useInterfaceStore().pushGlobalNotice({ + messageKey: 'user_card.deny_error', + messageArgs: { error }, + level: 'error', + }) + } }, // Utility From ded8ae863dcab3156d4e475253ed6702a3d10627 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 4 Sep 2026 00:18:58 +0300 Subject: [PATCH 38/39] lint+fix --- src/stores/follow_requests.js | 2 +- src/stores/users.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/follow_requests.js b/src/stores/follow_requests.js index 733d5cd63..192239dcd 100644 --- a/src/stores/follow_requests.js +++ b/src/stores/follow_requests.js @@ -1,10 +1,10 @@ import { defineStore } from 'pinia' import followRequestFetcher from 'src/stores/fetchers/follow_requests.js' +import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useNotificationsStore } from 'src/stores/notifications.js' import { useOAuthStore } from 'src/stores/oauth.js' -import { useInterfaceStore } from 'src/stores/interface.js' import { approveUser, denyUser } from 'src/api/user.js' diff --git a/src/stores/users.js b/src/stores/users.js index b487eab46..dbe0b9d8b 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -788,7 +788,7 @@ export const useUsersStore = defineStore('users', { useListsStore().startFetching() useBookmarkFoldersStore().startFetching() useChatsStore().startFetching() - if (user.locked) { + if (this.currentUser.locked) { useFollowRequestsStore().startFetching() } }) From eaa75f55e6df32ece681244739918c12fe0567e0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 4 Sep 2026 00:51:13 +0300 Subject: [PATCH 39/39] oops --- src/stores/users.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stores/users.js b/src/stores/users.js index dbe0b9d8b..6c40611b1 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -717,6 +717,7 @@ export const useUsersStore = defineStore('users', { }, logout() { const oauth = useOAuthStore() + const locked = this.currentUser.locked // Pause fetching useNotificationsStore().pause() @@ -727,7 +728,7 @@ export const useUsersStore = defineStore('users', { useListsStore().stopFetching() useBookmarkFoldersStore().stopFetching() useChatsStore().stopFetching() - if (this.currentUser.locked) { + if (locked) { useFollowRequestsStore().stopFetching() } @@ -788,7 +789,7 @@ export const useUsersStore = defineStore('users', { useListsStore().startFetching() useBookmarkFoldersStore().startFetching() useChatsStore().startFetching() - if (this.currentUser.locked) { + if (locked) { useFollowRequestsStore().startFetching() } })