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/changelog.d/drafts.fix b/changelog.d/drafts.fix new file mode 100644 index 000000000..f209455c5 --- /dev/null +++ b/changelog.d/drafts.fix @@ -0,0 +1 @@ +Fixed drafts creating duplicates on edit instead of updating 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. diff --git a/package.json b/package.json index 50138e6a2..8f6a2a98f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "hash-sum": "^2.0.0", "js-cookie": "3.0.5", "localforage": "1.10.0", + "lodash-es": "4.17.21", "parse-link-header": "2.0.0", "phoenix": "1.8.1", "pinia": "^3.0.4", @@ -54,8 +55,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", @@ -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,6 @@ "function-bind": "1.1.2", "http-proxy-middleware": "3.0.5", "iso-639-1": "3.1.5", - "lodash": "4.17.21", "msw": "2.14.6", "nightwatch": "3.12.2", "oxc": "^1.0.1", diff --git a/src/App.js b/src/App.js index ed3d43e4a..ae38204b6 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import { throttle } from 'lodash' +import { throttle } from 'lodash-es' import { mapState } from 'pinia' import { defineAsyncComponent } from 'vue' @@ -49,6 +49,12 @@ export default { MobilePostStatusButton, MobileNav, DesktopNav, + FollowRequestConfirm: defineAsyncComponent( + () => + import( + 'src/components/follow_request_confirm/follow_request_confirm.vue' + ), + ), SettingsModal: defineAsyncComponent( () => import('src/components/settings_modal/settings_modal.vue'), ), diff --git a/src/App.vue b/src/App.vue index bd19c5c10..cad329a78 100644 --- a/src/App.vue +++ b/src/App.vue @@ -75,6 +75,7 @@ + diff --git a/src/api/helpers.js b/src/api/helpers.js index f23960ed9..6afd20468 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -1,4 +1,4 @@ -import { snakeCase } from 'lodash' +import { snakeCase } from 'lodash-es' import { StatusCodeError } from 'src/services/errors/errors' diff --git a/src/api/user.js b/src/api/user.js index ec1763bb4..2f356ebbc 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -1,4 +1,4 @@ -import { last } from 'lodash' +import { last } from 'lodash-es' import { paramsString, promisedRequest } from './helpers.js' import { fetchFriends, MASTODON_STATUS_URL } from './public.js' @@ -17,7 +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 = '/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' @@ -39,9 +43,10 @@ export const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow` export const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow` const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests' -const MASTODON_APPROVE_USER_URL = (id) => +export const MASTODON_APPROVE_USER_URL = (id) => `/api/v1/follow_requests/${id}/authorize` -const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject` +export const MASTODON_DENY_USER_URL = (id) => + `/api/v1/follow_requests/${id}/reject` const MASTODON_USER_RELATIONSHIPS_URL = ({ id, withSuspended }) => `/api/v1/accounts/relationships/${paramsString({ id, withSuspended })}` export const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists` @@ -432,10 +437,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/chat_message_list/chat_message_list.js b/src/components/chat_message_list/chat_message_list.js index a51e6a0e4..6aef4336b 100644 --- a/src/components/chat_message_list/chat_message_list.js +++ b/src/components/chat_message_list/chat_message_list.js @@ -1,4 +1,4 @@ -import { orderBy, uniqueId } from 'lodash' +import { orderBy, uniqueId } from 'lodash-es' import ChatMessage from 'src/components/chat_message/chat_message.vue' 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/chat_view/chat_view.js b/src/components/chat_view/chat_view.js index a0abcb5fe..7d80459bb 100644 --- a/src/components/chat_view/chat_view.js +++ b/src/components/chat_view/chat_view.js @@ -1,4 +1,4 @@ -import { get, maxBy, minBy, sortBy, throttle } from 'lodash' +import { get, maxBy, minBy, sortBy, throttle } from 'lodash-es' import { mapState } from 'pinia' import { nextTick } from 'vue' diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue index 53396f532..821a1be0e 100644 --- a/src/components/color_input/color_input.vue +++ b/src/components/color_input/color_input.vue @@ -64,7 +64,7 @@ diff --git a/src/components/follow_requests/follow_requests.js b/src/components/follow_requests/follow_requests.js index 513298afc..181d5605e 100644 --- a/src/components/follow_requests/follow_requests.js +++ b/src/components/follow_requests/follow_requests.js @@ -1,12 +1,14 @@ import FollowRequestCard from 'src/components/follow_request_card/follow_request_card.vue' +import { useFollowRequestsStore } from 'src/stores/follow_requests.js' + const FollowRequests = { components: { FollowRequestCard, }, computed: { requests() { - return this.$store.state.api.followRequests + return useFollowRequestsStore().requests.values() }, }, } diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js index c244a8cb3..f6c79ac76 100644 --- a/src/components/gallery/gallery.js +++ b/src/components/gallery/gallery.js @@ -1,4 +1,4 @@ -import { set, sumBy } from 'lodash' +import { set, sumBy } from 'lodash-es' import Attachment from 'src/components/attachment/attachment.vue' diff --git a/src/components/list/list.js b/src/components/list/list.js index 8abf02fd6..2283f5260 100644 --- a/src/components/list/list.js +++ b/src/components/list/list.js @@ -1,4 +1,4 @@ -import { isEmpty } from 'lodash' +import { isEmpty } from 'lodash-es' import Checkbox from 'src/components/checkbox/checkbox.vue' diff --git a/src/components/lists_user_search/lists_user_search.js b/src/components/lists_user_search/lists_user_search.js index 22568135a..4dde2f589 100644 --- a/src/components/lists_user_search/lists_user_search.js +++ b/src/components/lists_user_search/lists_user_search.js @@ -1,4 +1,4 @@ -import { debounce } from 'lodash' +import { debounce } from 'lodash-es' import Checkbox from 'src/components/checkbox/checkbox.vue' 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/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 97645ca73..67e8c4418 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, + useAnnouncementsStore().unreadAnnouncementsCount, + useFollowRequestsStore().followRequestsCount, ) ) }, @@ -95,7 +96,7 @@ const MobileNav = { closingDrawerMarksAsSeen() { return useMergedConfigStore().mergedConfig.closingDrawerMarksAsSeen }, - ...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), + ...mapState(useAnnouncementsStore, ['unreadAnnouncementsCount']), ...mapState(useMergedConfigStore, { pinnedItems: (store) => 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/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js index 0ffab6f78..3fbd9b77b 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.js +++ b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -1,4 +1,4 @@ -import { debounce } from 'lodash' +import { debounce } from 'lodash-es' import { mapState } from 'pinia' import { useMergedConfigStore } from 'src/stores/merged_config.js' diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js index 9b948e306..73bf6e515 100644 --- a/src/components/moderation_tools/moderation_tools.js +++ b/src/components/moderation_tools/moderation_tools.js @@ -1,4 +1,4 @@ -import { last } from 'lodash' +import { last } from 'lodash-es' import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' import Popover from 'src/components/popover/popover.vue' diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js index 7f2a16186..c3530975e 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get } from 'lodash-es' import { mapState } from 'pinia' import { useInstanceStore } from 'src/stores/instance.js' diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index c3e760efb..e03a0c9d7 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' @@ -112,7 +112,7 @@ const NavPanel = { }, computed: { ...mapState(useAnnouncementsStore, { - unreadAnnouncementCount: 'unreadAnnouncementCount', + unreadAnnouncementsCount: 'unreadAnnouncementsCount', supportsAnnouncements: (store) => store.supportsAnnouncements, }), ...mapState(useInstanceCapabilitiesStore, [ @@ -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..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: 'followRequestCount', + 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..1a7832afe 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -1,9 +1,12 @@ -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' 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' @@ -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.unreadAnnouncementsCount, + followRequests: this.followRequestsCount, + unreadChats: this.unreadChatsCount, + } }, - ...mapStores(useAnnouncementsStore), + ...mapState(useAnnouncementsStore, ['unreadAnnouncementsCount']), + ...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..cbebbaf47 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -47,17 +47,11 @@
- {{ getters[item.badgeGetter] }} -
-
- {{ this[`${item.store}Store`][item.badgeGetter] }} + {{ badges[item.badgeGetter] }}
- - - {{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }} - - - {{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }} - - diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 2272c78af..b1a1468ab 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,17 +108,17 @@ const Notifications = { }, extraNotificationsCount() { return countExtraNotifications( - this.$store, useMergedConfigStore().mergedConfig, useChatsStore().unreadChatsCount, - useAnnouncementsStore().unreadAnnouncementCount, + useAnnouncementsStore().unreadAnnouncementsCount, + useFollowRequestsStore().followRequestsCount, ) }, unseenCountTitle() { return ( this.unseenNotifications.length + this.unreadChatsCount + - this.unreadAnnouncementCount + this.unreadAnnouncementsCount ) }, loading() { @@ -156,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/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 4fb6f4746..129da21eb 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -4,7 +4,7 @@ import { unescape as ldUnescape, reject, uniqBy, -} from 'lodash' +} from 'lodash-es' import { mapActions, mapState } from 'pinia' import { defineAsyncComponent } from 'vue' @@ -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' @@ -400,8 +401,6 @@ const PostStatusForm = { contentType: this.newStatus.contentType, poll, idempotencyKey: this.idempotencyKey, - - store: this.$store, } }, @@ -412,7 +411,6 @@ const PostStatusForm = { ...useEmojiStore().standardEmojiList, ...useEmojiStore().customEmoji, ], - store: this.$store, }) }, emojiSuggestor() { @@ -574,7 +572,7 @@ const PostStatusForm = { ...mapState(useUsersStore, ['currentUser']), ...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useInterfaceStore, { - mobileLayout: (store) => store.mobileLayout, + mobileLayout: (state) => state.mobileLayout, }), }, watch: { @@ -751,7 +749,6 @@ const PostStatusForm = { const description = this.newStatus.mediaDescriptions[id] if (!description || description.trim() === '') return return statusPoster.setMediaDescription({ - store: this.$store, id, description, }) @@ -985,13 +982,13 @@ 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, + // Draft ID overwrites status ID (which is undefined for fresh statuses) + id: this.draftId, }) .then((id) => { if (this.newStatus.id !== id) { @@ -1024,14 +1021,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/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js index b241d913c..1fcf934dd 100644 --- a/src/components/post_status_modal/post_status_modal.js +++ b/src/components/post_status_modal/post_status_modal.js @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get } from 'lodash-es' import { mapState } from 'pinia' import Modal from 'src/components/modal/modal.vue' diff --git a/src/components/quote/quote_form.js b/src/components/quote/quote_form.js index 700119be8..1b41af38c 100644 --- a/src/components/quote/quote_form.js +++ b/src/components/quote/quote_form.js @@ -1,4 +1,4 @@ -import { debounce } from 'lodash' +import { debounce } from 'lodash-es' import Checkbox from 'src/components/checkbox/checkbox.vue' import Quote from './quote.vue' diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index f80f153b1..70863a8a4 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -1,4 +1,4 @@ -import { flattenDeep, unescape as ldUnescape } from 'lodash' +import { flattenDeep, unescape as ldUnescape } from 'lodash-es' import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue' import { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.js' diff --git a/src/components/search/search.js b/src/components/search/search.js index 43a9b4f73..81478e798 100644 --- a/src/components/search/search.js +++ b/src/components/search/search.js @@ -1,4 +1,4 @@ -import { map, uniqBy } from 'lodash' +import { map, uniqBy } from 'lodash-es' import Conversation from 'src/components/conversation/conversation.vue' import FollowCard from 'src/components/follow_card/follow_card.vue' diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js index 33ead7c16..2290d9171 100644 --- a/src/components/settings_modal/admin_tabs/emoji_tab.js +++ b/src/components/settings_modal/admin_tabs/emoji_tab.js @@ -2,7 +2,7 @@ import Checkbox from 'components/checkbox/checkbox.vue' import Popover from 'components/popover/popover.vue' import Select from 'components/select/select.vue' import StillImage from 'components/still-image/still-image.vue' -import { clone } from 'lodash' +import { clone } from 'lodash-es' import { defineAsyncComponent } from 'vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' diff --git a/src/components/settings_modal/admin_tabs/http_tab.js b/src/components/settings_modal/admin_tabs/http_tab.js index 1e45763f9..5c9602766 100644 --- a/src/components/settings_modal/admin_tabs/http_tab.js +++ b/src/components/settings_modal/admin_tabs/http_tab.js @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get } from 'lodash-es' import AttachmentSetting from '../helpers/attachment_setting.vue' import BooleanSetting from '../helpers/boolean_setting.vue' diff --git a/src/components/settings_modal/admin_tabs/instance_tab.js b/src/components/settings_modal/admin_tabs/instance_tab.js index a428a78f9..586f1556e 100644 --- a/src/components/settings_modal/admin_tabs/instance_tab.js +++ b/src/components/settings_modal/admin_tabs/instance_tab.js @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get } from 'lodash-es' import AttachmentSetting from '../helpers/attachment_setting.vue' import BooleanSetting from '../helpers/boolean_setting.vue' diff --git a/src/components/settings_modal/admin_tabs/links_tab.js b/src/components/settings_modal/admin_tabs/links_tab.js index a6b99483b..d6a9b3a12 100644 --- a/src/components/settings_modal/admin_tabs/links_tab.js +++ b/src/components/settings_modal/admin_tabs/links_tab.js @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get } from 'lodash-es' import Checkbox from 'src/components/checkbox/checkbox.vue' import AttachmentSetting from '../helpers/attachment_setting.vue' diff --git a/src/components/settings_modal/helpers/pwa_manifest_icons_setting.js b/src/components/settings_modal/helpers/pwa_manifest_icons_setting.js index 0af54d3df..892a61c25 100644 --- a/src/components/settings_modal/helpers/pwa_manifest_icons_setting.js +++ b/src/components/settings_modal/helpers/pwa_manifest_icons_setting.js @@ -1,4 +1,4 @@ -import { clone } from 'lodash' +import { clone } from 'lodash-es' import Attachment from 'src/components/attachment/attachment.vue' import MediaUpload from 'src/components/media_upload/media_upload.vue' diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js index a1a946fca..0c321df63 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -1,4 +1,4 @@ -import { cloneDeep, get, isEqual, set } from 'lodash' +import { cloneDeep, get, isEqual, set } from 'lodash-es' import DraftButtons from './draft_buttons.vue' import LocalSettingIndicator from './local_setting_indicator.vue' @@ -8,6 +8,7 @@ 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 { useProfileConfigStore } from 'src/stores/profile_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' export default { @@ -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 }) @@ -412,8 +413,8 @@ 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 }) diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js index 216ce1b7c..0595e3091 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -1,4 +1,4 @@ -import { cloneDeep, isEqual } from 'lodash' +import { cloneDeep, isEqual } from 'lodash-es' import { mapActions, mapState } from 'pinia' import { defineAsyncComponent } from 'vue' diff --git a/src/components/settings_modal/tabs/composing_tab.js b/src/components/settings_modal/tabs/composing_tab.js index 3e734e6e9..7701d1673 100644 --- a/src/components/settings_modal/tabs/composing_tab.js +++ b/src/components/settings_modal/tabs/composing_tab.js @@ -16,6 +16,7 @@ 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 { useProfileConfigStore } from 'src/stores/profile_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUsersStore } from 'src/stores/users.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/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 601483106..09d14de0d 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,4 +1,4 @@ -import { cloneDeep } from 'lodash' +import { cloneDeep } from 'lodash-es' import { mapActions, mapState } from 'pinia' import { v4 as uuidv4 } from 'uuid' diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js index 719d3a7e3..98c3a1a5b 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js @@ -1,4 +1,4 @@ -import { get, map, reject } from 'lodash' +import { get, map, reject } from 'lodash-es' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import BlockCard from 'src/components/block_card/block_card.vue' diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js index a023b4415..e867e1d79 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.js +++ b/src/components/settings_modal/tabs/style_tab/style_tab.js @@ -1,4 +1,4 @@ -import { get, set, throttle, unset } from 'lodash' +import { get, set, throttle, unset } from 'lodash-es' import { computed, getCurrentInstance, diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index 52ca888fe..9a1b146d4 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -1,4 +1,4 @@ -import { flattenDeep, throttle } from 'lodash' +import { flattenDeep, throttle } from 'lodash-es' import Checkbox from 'src/components/checkbox/checkbox.vue' import ColorInput from 'src/components/color_input/color_input.vue' diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 2d2886d58..02c6d63d0 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' @@ -8,6 +7,8 @@ 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' import { useInterfaceStore } from 'src/stores/interface' @@ -61,10 +62,6 @@ const SideDrawer = { GestureService.DIRECTION_LEFT, this.toggleDrawer, ) - - if (this.currentUser?.locked) { - this.$store.dispatch('startFetchingFollowRequests') - } }, components: { UserCard, @@ -85,9 +82,6 @@ const SideDrawer = { unseenNotificationsCount() { return this.unseenNotifications.length }, - followRequestCount() { - return this.$store.state.api.followRequests.length - }, timelinesRoute() { let name if (useInterfaceStore().lastTimeline) { @@ -100,9 +94,10 @@ const SideDrawer = { return { name } } }, + ...mapState(useFollowRequestsStore, ['followRequestsCount']), ...mapState(useAnnouncementsStore, [ 'supportsAnnouncements', - 'unreadAnnouncementCount', + 'unreadAnnouncementsCount', ]), ...mapState(useInstanceCapabilitiesStore, [ 'pleromaChatMessagesAvailable', @@ -115,7 +110,7 @@ const SideDrawer = { hideSitename: (store) => store.instanceIdentity.hideSitename, }), ...mapState(useChatsStore, ['unreadChatsCount']), - ...mapGetters(['draftCount']), + ...mapState(useDraftsStore, ['draftsCount']), }, methods: { toggleDrawer() { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index c810d93a0..8bd3b1336 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 }} @@ -248,10 +248,10 @@ icon="bullhorn" /> {{ $t("nav.announcements") }} - {{ unreadAnnouncementCount }} + {{ unreadAnnouncementsCount }} @@ -269,10 +269,10 @@ icon="file-pen" /> {{ $t('nav.drafts') }} - {{ draftCount }} + {{ draftsCount }} diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js index 7ceda1e41..c00d6e679 100644 --- a/src/components/staff_panel/staff_panel.js +++ b/src/components/staff_panel/staff_panel.js @@ -1,4 +1,4 @@ -import { groupBy, map } from 'lodash' +import { groupBy, map } from 'lodash-es' import { mapState } from 'pinia' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' 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/timeline/timeline.js b/src/components/timeline/timeline.js index 38ee99931..7600e1765 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -1,4 +1,4 @@ -import { debounce, throttle } from 'lodash' +import { debounce, throttle } from 'lodash-es' import { mapState } from 'pinia' import Conversation from 'src/components/conversation/conversation.vue' diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 739c1579b..fdbda2783 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -3,7 +3,7 @@ import { escape as ldEscape, unescape as ldUnescape, merge, -} from 'lodash' +} from 'lodash-es' import { mapState } from 'pinia' import { defineAsyncComponent } from 'vue' @@ -418,7 +418,6 @@ export default { ...useEmojiStore().standardEmojiList, ...useEmojiStore().customEmoji, ], - store: this.$store, }) }, emojiSuggestor() { diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 521de0775..14bc77480 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get } from 'lodash-es' import FollowCard from 'src/components/follow_card/follow_card.vue' import List from 'src/components/list/list.vue' diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js index 107ec42f7..9daabec30 100644 --- a/src/components/who_to_follow_panel/who_to_follow_panel.js +++ b/src/components/who_to_follow_panel/who_to_follow_panel.js @@ -1,4 +1,4 @@ -import { shuffle } from 'lodash' +import { shuffle } from 'lodash-es' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' 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/i18n/messages.js b/src/i18n/messages.js index 51b8d920c..db18736ad 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -7,7 +7,7 @@ // sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json // There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry. -import { isEqual } from 'lodash' +import { isEqual } from 'lodash-es' import enMessages from './en.json' import { langCodeToJsonName, languages } from './languages.js' diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index a4dfe7d00..b1132f377 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-es' import { storage } from './storage.js' -import { useInterfaceStore } from 'src/stores/interface' - -let loaded = false - const defaultReducer = (state, paths) => paths.length === 0 ? state @@ -14,86 +10,10 @@ const defaultReducer = (state, paths) => return substate }, {}) -const saveImmedeatelyActions = [ - 'markNotificationsAsSeen', - 'setHighlight', - 'setOption', - 'setClientData', - 'setToken', - 'clearToken', -] - 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 735725250..953346427 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' @@ -37,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' : '' @@ -69,20 +61,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,18 +77,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' - }) - 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 }) + // 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/src/modules/api.js b/src/modules/api.js deleted file mode 100644 index 290fbfc27..000000000 --- a/src/modules/api.js +++ /dev/null @@ -1,79 +0,0 @@ -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 }) { - 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 - }, - 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) - }, - 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/default_config_state.js b/src/modules/default_config_state.js index 9935e88ce..9643b1856 100644 --- a/src/modules/default_config_state.js +++ b/src/modules/default_config_state.js @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get } from 'lodash-es' const browserLocale = (navigator.language || 'en').split('-')[0] 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 deleted file mode 100644 index 6aa236257..000000000 --- a/src/modules/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import api from './api.js' -import drafts from './drafts.js' -import profileConfig from './profileConfig.js' - -export default { - api, - profileConfig, - drafts, -} diff --git a/src/services/attributes_helper/attributes_helper.service.js b/src/services/attributes_helper/attributes_helper.service.js index 5b607600e..1772944d7 100644 --- a/src/services/attributes_helper/attributes_helper.service.js +++ b/src/services/attributes_helper/attributes_helper.service.js @@ -1,4 +1,4 @@ -import { kebabCase } from 'lodash' +import { kebabCase } from 'lodash-es' const propsToNative = (props) => Object.keys(props).reduce((acc, cur) => { diff --git a/src/services/completion/completion.js b/src/services/completion/completion.js index 7bd72536d..77b258352 100644 --- a/src/services/completion/completion.js +++ b/src/services/completion/completion.js @@ -1,4 +1,4 @@ -import { find, reduce } from 'lodash' +import { find, reduce } from 'lodash-es' export const replaceWord = (str, toReplace, replacement) => { return str.slice(0, toReplace.start) + replacement + str.slice(toReplace.end) diff --git a/src/services/component_utils/component_utils.js b/src/services/component_utils/component_utils.js index 1973a7923..c130c1af3 100644 --- a/src/services/component_utils/component_utils.js +++ b/src/services/component_utils/component_utils.js @@ -1,4 +1,4 @@ -import { isFunction } from 'lodash' +import { isFunction } from 'lodash-es' const getComponentOptions = (Component) => isFunction(Component) ? Component.options : Component diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 88e2fd0a5..6868a9805 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -1,6 +1,6 @@ import { parseLinkHeader } from '@web3-storage/parse-link-header' import escapeHtml from 'escape-html' -import { unescape as lodashUnescape } from 'lodash' +import { unescape as lodashUnescape } from 'lodash-es' import punycode from 'punycode.js' import { fileType } from '../file_type/file_type.service.js' diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js index 0742de3f4..439432faf 100644 --- a/src/services/errors/errors.js +++ b/src/services/errors/errors.js @@ -1,4 +1,4 @@ -import { capitalize } from 'lodash' +import { capitalize } from 'lodash-es' function humanizeErrors(errors) { return Object.entries(errors).reduce((errs, [k, val]) => { 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/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js index 4e2df2762..c3e3cc5b7 100644 --- a/src/services/html_converter/html_tree_converter.service.js +++ b/src/services/html_converter/html_tree_converter.service.js @@ -1,4 +1,4 @@ -import { unescape as ldUnescape } from 'lodash' +import { unescape as ldUnescape } from 'lodash-es' import { getTagName } from './utility.service.js' diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js index bdc07c1ec..47c910791 100644 --- a/src/services/locale/locale.service.js +++ b/src/services/locale/locale.service.js @@ -1,5 +1,5 @@ import ISO6391 from 'iso-639-1' -import { map } from 'lodash' +import { map } from 'lodash-es' import languagesObject from '../../i18n/messages' 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/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index f7877be15..5aea155c3 100644 --- a/src/services/status_poster/status_poster.service.js +++ b/src/services/status_poster/status_poster.service.js @@ -1,4 +1,4 @@ -import { map } from 'lodash' +import { map } from 'lodash-es' import { useOAuthStore } from 'src/stores/oauth.js' import { useStatusesStore } from 'src/stores/statuses.js' diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 5acd124b4..ac286928a 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,6 +1,6 @@ import sum from 'hash-sum' import localforage from 'localforage' -import { chunk, throttle } from 'lodash' +import { chunk, throttle } from 'lodash-es' import { getCssRules } from '../theme_data/css_utils.js' import { getEngineChecksum, init } from '../theme_data/theme_data_3.service.js' diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js index e5506ae42..71e4b4a18 100644 --- a/src/services/theme_data/iss_deserializer.js +++ b/src/services/theme_data/iss_deserializer.js @@ -1,4 +1,4 @@ -import { flattenDeep } from 'lodash' +import { flattenDeep } from 'lodash-es' export const deserializeShadow = (string) => { const modes = [ diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js index d42da3780..b1c8403e8 100644 --- a/src/services/theme_data/iss_utils.js +++ b/src/services/theme_data/iss_utils.js @@ -1,4 +1,4 @@ -import { sortBy } from 'lodash' +import { sortBy } from 'lodash-es' // "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }} // into an array [item2, item3] for iterating diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 694e41e12..9d71c461e 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -1,6 +1,6 @@ import { brightness, convert } from 'chromatism' import sum from 'hash-sum' -import { flattenDeep, sortBy } from 'lodash' +import { flattenDeep, sortBy } from 'lodash-es' import { alphaBlend, diff --git a/src/services/user_profile_link_generator/user_profile_link_generator.js b/src/services/user_profile_link_generator/user_profile_link_generator.js index 45cd0ea99..ab1e3c31d 100644 --- a/src/services/user_profile_link_generator/user_profile_link_generator.js +++ b/src/services/user_profile_link_generator/user_profile_link_generator.js @@ -1,4 +1,4 @@ -import { includes } from 'lodash' +import { includes } from 'lodash-es' const generateProfileLink = (id, screenName, restrictedNicknames) => { const complicated = diff --git a/src/stores/admin_settings.js b/src/stores/admin_settings.js index 4961761e7..10efdbfce 100644 --- a/src/stores/admin_settings.js +++ b/src/stores/admin_settings.js @@ -1,4 +1,4 @@ -import { cloneDeep, differenceWith, get, isEqual, set } from 'lodash' +import { cloneDeep, differenceWith, get, isEqual, set } from 'lodash-es' import { defineStore } from 'pinia' import { useOAuthStore } from 'src/stores/oauth.js' 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 } diff --git a/src/stores/bookmark_folders.js b/src/stores/bookmark_folders.js index 713a21d00..6d032e2bb 100644 --- a/src/stores/bookmark_folders.js +++ b/src/stores/bookmark_folders.js @@ -1,4 +1,4 @@ -import { find, remove } from 'lodash' +import { find, remove } from 'lodash-es' import { defineStore } from 'pinia' import { useOAuthStore } from 'src/stores/oauth.js' diff --git a/src/stores/chats.js b/src/stores/chats.js index 32296ff4f..71267faff 100644 --- a/src/stores/chats.js +++ b/src/stores/chats.js @@ -1,4 +1,4 @@ -import { orderBy, sumBy } from 'lodash' +import { orderBy, sumBy } from 'lodash-es' import { defineStore } from 'pinia' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' diff --git a/src/stores/drafts.js b/src/stores/drafts.js new file mode 100644 index 000000000..1a57a71af --- /dev/null +++ b/src/stores/drafts.js @@ -0,0 +1,76 @@ +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 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(id, draftWithId) + await saveDraftToStorage(draftWithId) + return id + }, + async abandonDraft(id) { + this.drafts.delete(id) + await deleteDraftFromStorage([id]) + }, + async abandonAllDrafts() { + const ids = [...this.drafts.keys()] + ids.forEach((id) => this.drafts.delete(id)) + await deleteDraftFromStorage(ids) + }, + }, +}) diff --git a/src/stores/emoji.js b/src/stores/emoji.js index 1471f3dfb..b893198d7 100644 --- a/src/stores/emoji.js +++ b/src/stores/emoji.js @@ -1,4 +1,4 @@ -import { merge } from 'lodash' +import { merge } from 'lodash-es' import { defineStore } from 'pinia' import { useInstanceStore } from 'src/stores/instance.js' diff --git a/src/stores/fetchers/follow_requests.js b/src/stores/fetchers/follow_requests.js new file mode 100644 index 000000000..c48b289ef --- /dev/null +++ b/src/stores/fetchers/follow_requests.js @@ -0,0 +1,42 @@ +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!') + + 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/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) } diff --git a/src/stores/follow_requests.js b/src/stores/follow_requests.js new file mode 100644 index 000000000..192239dcd --- /dev/null +++ b/src/stores/follow_requests.js @@ -0,0 +1,131 @@ +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 { 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) { + return state.requests.size + }, + }, + actions: { + // Fetcher stuff + startFetching() { + if (this.fetcher) throw new Error('Fetcher already exists!') + + this.fetcher = followRequestFetcher({ + credentials: useOAuthStore().token, + }) + + this.fetcher.startFetching() + }, + stopFetching() { + if (!this.fetcher) throw new Error("Fetcher doesn't exists!") + this.fetcher.stopFetching() + this.fetcher = null + }, + setFollowRequests(requests) { + this.requests = new Map(requests.map((user) => [user.id, user])) + }, + + // 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() + + try { + await approveUser({ + id, + credentials: useOAuthStore().token, + }) + + 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() + + try { + await denyUser({ + id, + credentials: useOAuthStore().token, + }) + + 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 + findFollowRequestNotificationId(userId) { + const notif = useNotificationsStore().data.find( + (notif) => + notif.from_profile.id === userId && notif.type === 'follow_request', + ) + return notif?.id + }, + }, +}) diff --git a/src/stores/instance.js b/src/stores/instance.js index 0d1da35d0..231140ca5 100644 --- a/src/stores/instance.js +++ b/src/stores/instance.js @@ -1,4 +1,4 @@ -import { set } from 'lodash' +import { set } from 'lodash-es' import { defineStore } from 'pinia' import { diff --git a/src/stores/instance_capabilities.js b/src/stores/instance_capabilities.js index 67c04a4ad..3e7ae3f0b 100644 --- a/src/stores/instance_capabilities.js +++ b/src/stores/instance_capabilities.js @@ -1,5 +1,8 @@ import { defineStore } from 'pinia' +import { useShoutStore } from 'src/stores/shout.js' +import { useUsersStore } from 'src/stores/users.js' + const defaultState = { postFormats: [], mailerEnabled: false, @@ -39,8 +42,11 @@ export const useInstanceCapabilitiesStore = defineStore( this[capability] = value - if (capability === 'shoutAvailable') { - window.vuex.dispatch('initializeSocket') + if ( + capability === 'shoutAvailable' && + useUsersStore().currentUser?.token + ) { + useShoutStore().initializeSocket() } }, }, diff --git a/src/stores/lists.js b/src/stores/lists.js index fcc3fefde..b3ce52d30 100644 --- a/src/stores/lists.js +++ b/src/stores/lists.js @@ -1,4 +1,4 @@ -import { find, remove } from 'lodash' +import { find, remove } from 'lodash-es' import { defineStore } from 'pinia' import { useOAuthStore } from 'src/stores/oauth.js' diff --git a/src/stores/local_config.js b/src/stores/local_config.js index 786da657b..82fb6f265 100644 --- a/src/stores/local_config.js +++ b/src/stores/local_config.js @@ -1,4 +1,4 @@ -import { cloneDeep, set } from 'lodash' +import { cloneDeep, set } from 'lodash-es' import { defineStore } from 'pinia' import { diff --git a/src/stores/polls.js b/src/stores/polls.js index e2f4b0ab2..805fb0026 100644 --- a/src/stores/polls.js +++ b/src/stores/polls.js @@ -1,4 +1,4 @@ -import { merge } from 'lodash' +import { merge } from 'lodash-es' import { defineStore } from 'pinia' import { useOAuthStore } from 'src/stores/oauth.js' diff --git a/src/modules/profileConfig.js b/src/stores/profile_config.js similarity index 61% rename from src/modules/profileConfig.js rename to src/stores/profile_config.js index 8b9c77425..a918e5ee2 100644 --- a/src/modules/profileConfig.js +++ b/src/stores/profile_config.js @@ -1,34 +1,36 @@ -import { get, set } from 'lodash' +import { get, set } from 'lodash-es' +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,67 @@ 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 + } + + const [user] = 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 79268bd57..05cd340be 100644 --- a/src/stores/shout.js +++ b/src/stores/shout.js @@ -1,14 +1,32 @@ +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, + 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) return + + 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 +46,9 @@ export const useShoutStore = defineStore('shout', { channel.join() this.channel = channel }, + disconnectSocket() { + this.socket?.disconnect() + this.socket = null + }, }, }) diff --git a/src/stores/sync_config.js b/src/stores/sync_config.js index 4de0c13cb..26c447530 100644 --- a/src/stores/sync_config.js +++ b/src/stores/sync_config.js @@ -12,7 +12,7 @@ import { take, uniqWith, unset, -} from 'lodash' +} from 'lodash-es' import { defineStore } from 'pinia' import { v4 as uuidv4 } from 'uuid' import { toRaw } from 'vue' diff --git a/src/stores/timelines.js b/src/stores/timelines.js index 86a2c617e..8de616c8a 100644 --- a/src/stores/timelines.js +++ b/src/stores/timelines.js @@ -1,4 +1,4 @@ -import { first, last } from 'lodash' +import { first, last } from 'lodash-es' import { defineStore } from 'pinia' import timelineFetcher from 'src/stores/fetchers/timeline_fetcher.js' diff --git a/src/stores/user_highlight.js b/src/stores/user_highlight.js index a4cbacd6e..6633c37aa 100644 --- a/src/stores/user_highlight.js +++ b/src/stores/user_highlight.js @@ -5,7 +5,7 @@ import { groupBy, isEqual, last, -} from 'lodash' +} from 'lodash-es' import { defineStore } from 'pinia' import { toRaw } from 'vue' diff --git a/src/stores/users.js b/src/stores/users.js index b9725a652..6c40611b1 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -1,11 +1,13 @@ import Cookies from 'js-cookie' -import { last } from 'lodash' +import { last } from 'lodash-es' 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' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' @@ -13,6 +15,8 @@ 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' @@ -140,6 +144,7 @@ export const useUsersStore = defineStore('users', { if (user.id === this.currentUser?.id) { this.currentUser = reactive + useProfileConfigStore().update(reactive) } // Initialize some stuff @@ -607,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 { @@ -645,6 +643,7 @@ export const useUsersStore = defineStore('users', { console.error('Error setting theme', e) }) }) + useProfileConfigStore().onLogin(user) useUserHighlightStore().initUserHighlight(user) @@ -665,10 +664,10 @@ export const useUsersStore = defineStore('users', { useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 }) /**/ - if (user.token) { + if (user.token && useInstanceCapabilitiesStore().shoutAvailable) { // Shoutbox - dispatch('setWsToken', user.token) - dispatch('initializeSocket') + useShoutStore().initializeSocket() + useShoutStore().initializeShout() } // DMs and Home @@ -684,7 +683,7 @@ export const useUsersStore = defineStore('users', { useBookmarkFoldersStore().startFetching() if (user.locked) { - dispatch('startFetchingFollowRequests') + useFollowRequestsStore().startFetching() } if (useMergedConfigStore().mergedConfig.useStreamingApi) { @@ -695,7 +694,7 @@ export const useUsersStore = defineStore('users', { useAnnouncementsStore().startFetching() this.fetchMutes() - dispatch('loadDrafts') + useDraftsStore().loadDrafts() } catch (error) { console.error(error) @@ -717,8 +716,8 @@ export const useUsersStore = defineStore('users', { } }, logout() { - const store = window.vuex const oauth = useOAuthStore() + const locked = this.currentUser.locked // Pause fetching useNotificationsStore().pause() @@ -729,8 +728,9 @@ export const useUsersStore = defineStore('users', { useListsStore().stopFetching() useBookmarkFoldersStore().stopFetching() useChatsStore().stopFetching() - - store?.dispatch('stopFetchingFollowRequests') + if (locked) { + useFollowRequestsStore().stopFetching() + } // NOTE: No need to verify the app still exists, because if it doesn't, // the token will be invalid too @@ -747,6 +747,7 @@ export const useUsersStore = defineStore('users', { }) .then(() => { oauth.clearToken() + useShoutStore().disconnectSocket() this.currentUser = null @@ -772,6 +773,7 @@ export const useUsersStore = defineStore('users', { Cookies.remove('__Host-pleroma_key', { path: '/' }) useInterfaceStore().onLogout() + useProfileConfigStore().onLogout() }) .catch((e) => { useInterfaceStore().pushGlobalNotice({ @@ -787,7 +789,9 @@ export const useUsersStore = defineStore('users', { useListsStore().startFetching() useBookmarkFoldersStore().startFetching() useChatsStore().startFetching() - store?.dispatch('startFetchingFollowRequests') + if (locked) { + useFollowRequestsStore().startFetching() + } }) .finally(() => { useNotificationsStore().resume() 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/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/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, diff --git a/test/unit/specs/components/draft.spec.js b/test/unit/specs/components/draft.spec.js deleted file mode 100644 index 40d63419b..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 { 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' - -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 981a6d020..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 { $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 { mountOpts } from '../../../fixtures/setup_test' +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) }) 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/drafts.spec.js b/test/unit/specs/stores/drafts.spec.js new file mode 100644 index 000000000..1a0667424 --- /dev/null +++ b/test/unit/specs/stores/drafts.spec.js @@ -0,0 +1,220 @@ +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' + +import { useDraftsStore } from 'src/stores/drafts.js' + +import { storage } from 'src/lib/storage.js' + +describe('Drafts store', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + + // Localforage does something weird that resets get/setItem and breaks + // mocking if we just spy one them without overriding implementation + vi.spyOn(storage, 'setItem').mockImplementation(() => ({})) + vi.spyOn(storage, 'getItem').mockImplementation(() => ({})) + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + describe('Getters', () => { + it('draftsCount returns total number of drafts', async () => { + const store = useDraftsStore() + + await store.addOrSaveDraft({ id: 1, status: 'draft' }) + await store.addOrSaveDraft({ id: 2, status: 'draft' }) + await store.addOrSaveDraft({ id: 3, status: 'draft' }) + + expect(store).to.have.property('draftsCount', 3) + }) + + it('draftsArray returns array of drafts', async () => { + const store = useDraftsStore() + + await store.addOrSaveDraft({ id: 1, status: 'draft' }) + await store.addOrSaveDraft({ id: 2, status: 'draft' }) + await store.addOrSaveDraft({ id: 3, status: 'draft' }) + + expect(store.draftsArray).to.have.length(3) + expect(store.draftsArray).to.be.an('Array') + expect(store.draftsArray).to.have.deep.members([ + { id: 1, status: 'draft' }, + { id: 2, status: 'draft' }, + { id: 3, status: 'draft' }, + ]) + }) + + 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: 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', + }) + + 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' }, + ]) + }) + }) + + describe('Actions', () => { + describe('loadDrafts', () => { + it('should load drafts from storage and populate cache', async () => { + const store = useDraftsStore() + storage.getItem.mockResolvedValueOnce({ + a: { id: 'a', status: 'draft' }, + b: { id: 'b', status: 'draft' }, + c: { id: 'c', status: 'draft' }, + }) + + await store.loadDrafts() + + expect(store.drafts.get('a')).to.have.property('status', 'draft') + expect(store.drafts.get('b')).to.have.property('status', 'draft') + expect(store.drafts.get('c')).to.have.property('status', 'draft') + + expect(storage.getItem).to.have.been.calledOnce + expect(storage.getItem).to.have.been.calledWith('pleroma-fe-drafts') + expect(storage.setItem).to.have.not.been.called + }) + + it('should handle case where there is no local draft storage yet', async () => { + const store = useDraftsStore() + storage.getItem.mockResolvedValueOnce(null) + + await store.loadDrafts() + + expect(store.drafts).to.have.property('size', 0) + + expect(storage.getItem).to.have.been.calledOnce + expect(storage.getItem).to.have.been.calledWith('pleroma-fe-drafts') + expect(storage.setItem).to.have.not.been.called + }) + }) + + describe('addOrSaveDraft', () => { + it('create draft', async () => { + const store = useDraftsStore() + vi.setSystemTime(new Date(859586400000)) + + const id = await store.addOrSaveDraft({ status: 'draft' }) + + expect(store.drafts).to.have.property('size', 1) + expect(id).to.eql('859586400000') + expect(store.drafts.get(id)).to.have.property('status', 'draft') + expect(storage.getItem).to.have.been.calledOnce + expect(storage.setItem).to.have.been.calledOnce + expect(storage.setItem).to.have.been.calledWith('pleroma-fe-drafts', { + [id]: { + id, + status: 'draft', + }, + }) + }) + + it('update draft', async () => { + const store = useDraftsStore() + await store.addOrSaveDraft({ id: '1', status: 'draft' }) + + expect(store.drafts.get('1')).to.have.property('status', 'draft') + + await store.addOrSaveDraft({ id: '1', status: 'updated' }) + + expect(store.drafts.get('1')).to.have.property('status', 'updated') + 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: { + id: '1', + status: 'draft', + }, + }) + }) + }) + + describe('abandonDraft', () => { + it('should remove draft from storage and cache', async () => { + const store = useDraftsStore() + store.drafts.set('a', { id: 'a', status: 'draft' }) + store.drafts.set('b', { id: 'b', status: 'draft' }) + store.drafts.set('c', { id: 'c', status: 'draft' }) + storage.getItem.mockResolvedValueOnce({ + a: { id: 'a', status: 'draft' }, + b: { id: 'b', status: 'draft' }, + c: { id: 'c', status: 'draft' }, + }) + + await store.abandonDraft('b') + + expect(store.drafts.get('a')).to.have.property('status', 'draft') + expect(store.drafts.get('b')).to.be.undefined + expect(store.drafts.get('c')).to.have.property('status', 'draft') + + 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: { + id: 'a', + status: 'draft', + }, + c: { + id: 'c', + status: 'draft', + }, + }) + }) + }) + + describe('abandonAllDrafts', () => { + it('should remove draft from storage and cache', async () => { + const store = useDraftsStore() + store.drafts.set('a', { id: 'a', status: 'draft' }) + store.drafts.set('b', { id: 'b', status: 'draft' }) + store.drafts.set('c', { id: 'c', status: 'draft' }) + storage.getItem.mockResolvedValueOnce({ + a: { id: 'a', status: 'draft' }, + b: { id: 'b', status: 'draft' }, + c: { id: 'c', status: 'draft' }, + }) + + await store.abandonAllDrafts() + + expect(store.drafts.get('a')).to.be.undefined + expect(store.drafts.get('b')).to.be.undefined + expect(store.drafts.get('c')).to.be.undefined + + expect(storage.getItem).to.have.been.calledOnce + expect(storage.setItem).to.have.been.calledOnce + expect(storage.setItem).to.have.been.calledWith('pleroma-fe-drafts', {}) + }) + }) + }) +}) 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..20331a3fe --- /dev/null +++ b/test/unit/specs/stores/follow_requests.spec.js @@ -0,0 +1,350 @@ +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' }, + }), + ) + + 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) + }) + }) + }) + }) +}) diff --git a/test/unit/specs/stores/statuses.spec.js b/test/unit/specs/stores/statuses.spec.js index f4064ec9d..1d4537dbc 100644 --- a/test/unit/specs/stores/statuses.spec.js +++ b/test/unit/specs/stores/statuses.spec.js @@ -1,5 +1,5 @@ import { createTestingPinia } from '@pinia/testing' -import { snakeCase } from 'lodash' +import { snakeCase } from 'lodash-es' import { setActivePinia } from 'pinia' import { useStatusesStore } from 'src/stores/statuses.js' diff --git a/test/unit/specs/stores/sync_config.spec.js b/test/unit/specs/stores/sync_config.spec.js index 3618068f7..372d67b3b 100644 --- a/test/unit/specs/stores/sync_config.spec.js +++ b/test/unit/specs/stores/sync_config.spec.js @@ -1,4 +1,4 @@ -import { cloneDeep } from 'lodash' +import { cloneDeep } from 'lodash-es' import { createPinia, setActivePinia } from 'pinia' import { useLocalConfigStore } from 'src/stores/local_config.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/test/unit/specs/stores/users.spec.js b/test/unit/specs/stores/users.spec.js index c44c749f3..6743ae8cc 100644 --- a/test/unit/specs/stores/users.spec.js +++ b/test/unit/specs/stores/users.spec.js @@ -1,11 +1,13 @@ import { createTestingPinia } from '@pinia/testing' -import { snakeCase } from 'lodash' +import { snakeCase } from 'lodash-es' import { setActivePinia } 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 { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useListsStore } from 'src/stores/lists.js' @@ -53,12 +55,14 @@ const mockMastoAPIUser = ({ name = userName, url = userUrl, id = userId, + locked = true, } = {}) => ({ id, acct: screen_name, display_name: name, fields: [], avatar: '', + locked, url, }) @@ -67,17 +71,20 @@ const mockUser = ({ id = userId, name = userName, url = userUrl, + locked = true, } = {}) => ({ _original: mockMastoAPIUser({ screen_name, id, name, url, + locked, }), id, name, screen_name, url, + locked, relationship: undefined, }) @@ -631,22 +638,24 @@ describe('Users store', () => { const spies = [ // Misc initialization - vi.spyOn(useSyncConfigStore(), 'initSyncConfig'), - vi.spyOn(useUserHighlightStore(), 'initUserHighlight'), - vi.spyOn(useInterfaceStore(), 'applyTheme'), - vi.spyOn(useInterfaceStore(), 'onLogin'), - vi.spyOn(useEmojiStore(), 'fetchEmoji'), + /* 0 */ vi.spyOn(useSyncConfigStore(), 'initSyncConfig'), + /* 1 */ vi.spyOn(useUserHighlightStore(), 'initUserHighlight'), + /* 2 */ vi.spyOn(useInterfaceStore(), 'applyTheme'), + /* 3 */ vi.spyOn(useInterfaceStore(), 'onLogin'), + /* 4 */ vi.spyOn(useEmojiStore(), 'fetchEmoji'), + /* 5 */ vi.spyOn(useDraftsStore(), 'loadDrafts'), // Timeline / Notifications - vi.spyOn(useNotificationsStore(), 'activate'), - vi.spyOn(useTimelinesStore(), 'activatePersistents'), + /* 6 */ vi.spyOn(useNotificationsStore(), 'activate'), + /* 7 */ vi.spyOn(useTimelinesStore(), 'activatePersistents'), // Fetchers - vi.spyOn(useChatsStore(), 'startFetching'), - vi.spyOn(useListsStore(), 'startFetching'), - vi.spyOn(useAnnouncementsStore(), 'startFetching'), - vi.spyOn(useBookmarkFoldersStore(), 'startFetching'), - vi.spyOn(useStreamingStore(), 'initSocket'), + /* 8 */ vi.spyOn(useChatsStore(), 'startFetching'), + /* 9 */ vi.spyOn(useListsStore(), 'startFetching'), + /* 10 */ vi.spyOn(useAnnouncementsStore(), 'startFetching'), + /* 11 */ vi.spyOn(useBookmarkFoldersStore(), 'startFetching'), + /* 12 */ vi.spyOn(useFollowRequestsStore(), 'startFetching'), + /* 13 */ vi.spyOn(useStreamingStore(), 'initSocket'), ] spies.forEach((spy) => { @@ -758,6 +767,7 @@ describe('Users store', () => { /* 11 */ vi.spyOn(useAnnouncementsStore(), 'stopFetching'), /* 12 */ vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'), /* 13 */ vi.spyOn(useStreamingStore(), 'stopSocket'), + /* 14 */ vi.spyOn(useFollowRequestsStore(), 'stopFetching'), ] spies.forEach((spy) => { @@ -771,6 +781,7 @@ describe('Users store', () => { const store = useUsersStore() store.currentUser = mockUser() + store.currentUser.locked = true // Adding some users to verify they are getting cleaned afterwards store.addNewUsers({ @@ -832,17 +843,19 @@ describe('Users store', () => { /* 3 */ vi.spyOn(useChatsStore(), 'stopFetching'), /* 4 */ vi.spyOn(useAnnouncementsStore(), 'stopFetching'), /* 5 */ vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'), + /* 6 */ vi.spyOn(useFollowRequestsStore(), 'stopFetching'), // ## RESUME ## // Timeline / Notifications - /* 6 */ vi.spyOn(useNotificationsStore(), 'resume'), - /* 7 */ vi.spyOn(useTimelinesStore(), 'resumeAll'), + /* 7 */ vi.spyOn(useNotificationsStore(), 'resume'), + /* 8 */ vi.spyOn(useTimelinesStore(), 'resumeAll'), // Fetchers (Pauseless) - /* 8 */ vi.spyOn(useListsStore(), 'startFetching'), - /* 9 */ vi.spyOn(useChatsStore(), 'startFetching'), - /* 10 */ vi.spyOn(useAnnouncementsStore(), 'startFetching'), - /* 11 */ vi.spyOn(useBookmarkFoldersStore(), 'startFetching'), + /* 9 */ vi.spyOn(useListsStore(), 'startFetching'), + /* 10 */ vi.spyOn(useChatsStore(), 'startFetching'), + /* 11 */ vi.spyOn(useAnnouncementsStore(), 'startFetching'), + /* 12 */ vi.spyOn(useBookmarkFoldersStore(), 'startFetching'), + /* 13 */ vi.spyOn(useFollowRequestsStore(), 'startFetching'), ] spies.forEach((spy) => { 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' } : {}), } diff --git a/yarn.lock b/yarn.lock index 8644d2d53..e7d6ebaa4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,13 +2,13 @@ # yarn lockfile v1 -"@asamuzakjp/css-color@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.1.1.tgz#41a612834dafd9353b89855b37baa8a03fb67bf2" - integrity sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA== +"@asamuzakjp/css-color@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz#cc42f5b85c593f79f1fa4f25d2b9b321e61d1794" + integrity sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw== dependencies: - "@csstools/css-calc" "^2.1.2" - "@csstools/css-color-parser" "^3.0.8" + "@csstools/css-calc" "^2.1.3" + "@csstools/css-color-parser" "^3.0.9" "@csstools/css-parser-algorithms" "^3.0.4" "@csstools/css-tokenizer" "^3.0.3" lru-cache "^10.4.3" @@ -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== @@ -1264,9 +1216,9 @@ "@babel/helper-validator-identifier" "^7.29.7" "@bazel/runfiles@^6.3.1": - version "6.3.1" - resolved "https://registry.yarnpkg.com/@bazel/runfiles/-/runfiles-6.3.1.tgz#3f8824b2d82853377799d42354b4df78ab0ace0b" - integrity sha512-1uLNT5NZsUVIGS4syuHwTzZ8HycMPyr6POA3FCE4GbMtc4rhoJk8aZKtNIRthJYfL+iioppi+rTfH3olMPr9nA== + version "6.5.0" + resolved "https://registry.yarnpkg.com/@bazel/runfiles/-/runfiles-6.5.0.tgz#63cf7b77b91b54873e75f7a08fabec215c6888be" + integrity sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA== "@bcoe/v8-coverage@^1.0.2": version "1.0.2" @@ -1469,40 +1421,30 @@ resolved "https://registry.yarnpkg.com/@cropper/utils/-/utils-2.0.1.tgz#ef24496854be61c677c0e97951813ff9495844d6" integrity sha512-A9RnAFmgNF5aZk5q2VZnFnHtXWu1kPyEN0LVsX8wJ2LBRu2nyETKwz+ZXVsVWliktToCaYojHKrS+6/HODyEZA== -"@csstools/color-helpers@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.2.tgz#82592c9a7c2b83c293d9161894e2a6471feb97b8" - integrity sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA== +"@csstools/color-helpers@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz#106c54c808cabfd1ab4c602d8505ee584c2996ef" + integrity sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA== -"@csstools/css-calc@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.2.tgz#bffd55f002dab119b76d4023f95cd943e6c8c11e" - integrity sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw== +"@csstools/css-calc@^2.1.3", "@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== -"@csstools/css-color-parser@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz#5fe9322920851450bf5e065c2b0e731b9e165394" - integrity sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ== +"@csstools/css-color-parser@^3.0.9": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz#4e386af3a99dd36c46fef013cfe4c1c341eed6f0" + integrity sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA== dependencies: - "@csstools/color-helpers" "^5.0.2" - "@csstools/css-calc" "^2.1.2" + "@csstools/color-helpers" "^5.1.0" + "@csstools/css-calc" "^2.1.4" -"@csstools/css-parser-algorithms@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz#74426e93bd1c4dcab3e441f5cc7ba4fb35d94356" - integrity sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A== - -"@csstools/css-parser-algorithms@^3.0.5": +"@csstools/css-parser-algorithms@^3.0.4", "@csstools/css-parser-algorithms@^3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== -"@csstools/css-tokenizer@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2" - integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw== - -"@csstools/css-tokenizer@^3.0.4": +"@csstools/css-tokenizer@^3.0.3", "@csstools/css-tokenizer@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== @@ -1804,7 +1746,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== @@ -1856,107 +1798,113 @@ outvariant "^1.4.3" strict-event-emitter "^0.5.1" -"@napi-rs/nice-android-arm-eabi@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz#9a0cba12706ff56500df127d6f4caf28ddb94936" - integrity sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w== +"@napi-rs/nice-android-arm-eabi@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz#4ebd966821cd6c2cc7cc020eb468de397bb9b40f" + integrity sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw== -"@napi-rs/nice-android-arm64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz#32fc32e9649bd759d2a39ad745e95766f6759d2f" - integrity sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA== +"@napi-rs/nice-android-arm64@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz#e183ba874512bc005852daab8b78c63e0a4288a8" + integrity sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw== -"@napi-rs/nice-darwin-arm64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz#d3c44c51b94b25a82d45803e2255891e833e787b" - integrity sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA== +"@napi-rs/nice-darwin-arm64@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz#64b1585809774cbb8bf95cea3d4c8827c9897394" + integrity sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A== -"@napi-rs/nice-darwin-x64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz#f1b1365a8370c6a6957e90085a9b4873d0e6a957" - integrity sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ== +"@napi-rs/nice-darwin-x64@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz#99c0c7f62cb1e23ca76881bb29cc6000aeccc6f0" + integrity sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ== -"@napi-rs/nice-freebsd-x64@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz#4280f081efbe0b46c5165fdaea8b286e55a8f89e" - integrity sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw== +"@napi-rs/nice-freebsd-x64@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz#9a5ca0e3ced86207887c98a5a560de8cde5a909e" + integrity sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ== -"@napi-rs/nice-linux-arm-gnueabihf@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz#07aec23a9467ed35eb7602af5e63d42c5d7bd473" - integrity sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q== +"@napi-rs/nice-linux-arm-gnueabihf@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz#b8a6a1bc88d0de3e99ac3fdea69980dc6e20b502" + integrity sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg== -"@napi-rs/nice-linux-arm64-gnu@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz#038a77134cc6df3c48059d5a5e199d6f50fb9a90" - integrity sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA== +"@napi-rs/nice-linux-arm64-gnu@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz#226f1ef30fcb80fa40370e843b75cc86e39e1183" + integrity sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ== -"@napi-rs/nice-linux-arm64-musl@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz#715d0906582ba0cff025109f42e5b84ea68c2bcc" - integrity sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw== +"@napi-rs/nice-linux-arm64-musl@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz#01345c3db79210ba5406c8729e8db75ed11c5f14" + integrity sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg== -"@napi-rs/nice-linux-ppc64-gnu@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz#ac1c8f781c67b0559fa7a1cd4ae3ca2299dc3d06" - integrity sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q== +"@napi-rs/nice-linux-ppc64-gnu@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz#ce7a1025227daab491ded40784b561394d688fcb" + integrity sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg== -"@napi-rs/nice-linux-riscv64-gnu@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz#b0a430549acfd3920ffd28ce544e2fe17833d263" - integrity sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig== +"@napi-rs/nice-linux-riscv64-gnu@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz#9bef5dc89a0425d03163853b4968dbb686d98fd5" + integrity sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw== -"@napi-rs/nice-linux-s390x-gnu@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz#5b95caf411ad72a965885217db378c4d09733e97" - integrity sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg== +"@napi-rs/nice-linux-s390x-gnu@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz#247c8c7c45876877bdb337cfeb290ff4fd82de62" + integrity sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ== -"@napi-rs/nice-linux-x64-gnu@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz#a98cdef517549f8c17a83f0236a69418a90e77b7" - integrity sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA== +"@napi-rs/nice-linux-x64-gnu@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz#7fd1f5e037cb44ab4f5f95a3b3225a99e3248f12" + integrity sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg== -"@napi-rs/nice-linux-x64-musl@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz#5e26843eafa940138aed437c870cca751c8a8957" - integrity sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ== +"@napi-rs/nice-linux-x64-musl@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz#d447cd7157ae5da5c0b15fc618bf61f0c344ff6f" + integrity sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw== -"@napi-rs/nice-win32-arm64-msvc@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz#bd62617d02f04aa30ab1e9081363856715f84cd8" - integrity sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg== +"@napi-rs/nice-openharmony-arm64@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz#1120e457d2cc6b2bc86ef0a697faefe2e194dfce" + integrity sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ== -"@napi-rs/nice-win32-ia32-msvc@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz#b8b7aad552a24836027473d9b9f16edaeabecf18" - integrity sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw== +"@napi-rs/nice-win32-arm64-msvc@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz#91e4cfecf339b43fa7934f0c8b19d04f4cdd9bc0" + integrity sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA== -"@napi-rs/nice-win32-x64-msvc@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz#37d8718b8f722f49067713e9f1e85540c9a3dd09" - integrity sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg== +"@napi-rs/nice-win32-ia32-msvc@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz#ed9300bba074d3e3b0a077d6b157f2b4ff70af0e" + integrity sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug== + +"@napi-rs/nice-win32-x64-msvc@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz#8292b82fb46458618ccff5b8130f78974349541e" + integrity sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ== "@napi-rs/nice@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@napi-rs/nice/-/nice-1.0.1.tgz#483d3ff31e5661829a1efb4825591a135c3bfa7d" - integrity sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/@napi-rs/nice/-/nice-1.1.1.tgz#c1aacd631ecd4c500c959e3e7cfedd5c73bffe2a" + integrity sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw== optionalDependencies: - "@napi-rs/nice-android-arm-eabi" "1.0.1" - "@napi-rs/nice-android-arm64" "1.0.1" - "@napi-rs/nice-darwin-arm64" "1.0.1" - "@napi-rs/nice-darwin-x64" "1.0.1" - "@napi-rs/nice-freebsd-x64" "1.0.1" - "@napi-rs/nice-linux-arm-gnueabihf" "1.0.1" - "@napi-rs/nice-linux-arm64-gnu" "1.0.1" - "@napi-rs/nice-linux-arm64-musl" "1.0.1" - "@napi-rs/nice-linux-ppc64-gnu" "1.0.1" - "@napi-rs/nice-linux-riscv64-gnu" "1.0.1" - "@napi-rs/nice-linux-s390x-gnu" "1.0.1" - "@napi-rs/nice-linux-x64-gnu" "1.0.1" - "@napi-rs/nice-linux-x64-musl" "1.0.1" - "@napi-rs/nice-win32-arm64-msvc" "1.0.1" - "@napi-rs/nice-win32-ia32-msvc" "1.0.1" - "@napi-rs/nice-win32-x64-msvc" "1.0.1" + "@napi-rs/nice-android-arm-eabi" "1.1.1" + "@napi-rs/nice-android-arm64" "1.1.1" + "@napi-rs/nice-darwin-arm64" "1.1.1" + "@napi-rs/nice-darwin-x64" "1.1.1" + "@napi-rs/nice-freebsd-x64" "1.1.1" + "@napi-rs/nice-linux-arm-gnueabihf" "1.1.1" + "@napi-rs/nice-linux-arm64-gnu" "1.1.1" + "@napi-rs/nice-linux-arm64-musl" "1.1.1" + "@napi-rs/nice-linux-ppc64-gnu" "1.1.1" + "@napi-rs/nice-linux-riscv64-gnu" "1.1.1" + "@napi-rs/nice-linux-s390x-gnu" "1.1.1" + "@napi-rs/nice-linux-x64-gnu" "1.1.1" + "@napi-rs/nice-linux-x64-musl" "1.1.1" + "@napi-rs/nice-openharmony-arm64" "1.1.1" + "@napi-rs/nice-win32-arm64-msvc" "1.1.1" + "@napi-rs/nice-win32-ia32-msvc" "1.1.1" + "@napi-rs/nice-win32-x64-msvc" "1.1.1" "@napi-rs/wasm-runtime@^1.1.4": version "1.1.4" @@ -2481,9 +2429,9 @@ undici-types "~6.20.0" "@types/selenium-webdriver@^4.1.14": - version "4.1.28" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.28.tgz#7b4f3c50a67494f8fd6d396a2eaab7d9df1f9f34" - integrity sha512-Au7CXegiS7oapbB16zxPToY4Cjzi9UQQMf3W2ZZM8PigMLTGR3iUAHjPUTddyE5g1SBjT/qpmvlsAQLBfNAdKg== + version "4.35.6" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.35.6.tgz#28acbfaa35d417d730d04a5a59def50bad558387" + integrity sha512-8nfyMRi4VvkY9QrQGyY/zkleAhnjnmE8YtdEeoCrWe3izp1P9vo9f5VTNRYF0up+l+kn+VuZah+je+bLddNV+g== dependencies: "@types/node" "*" "@types/ws" "*" @@ -2925,7 +2873,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== @@ -3350,7 +3298,7 @@ async-function@^1.0.0: resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== -async@^3.2.3, async@^3.2.4: +async@^3.2.4, async@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== @@ -3379,10 +3327,10 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axe-core@^4.9.1: - version "4.10.3" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc" - integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== +axe-core@^4.11.1: + version "4.13.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.13.0.tgz#f868ecb1bd61d982321760e51d841ab497ab86d0" + integrity sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A== axios@^1.7.4: version "1.8.4" @@ -3393,17 +3341,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" @@ -3615,7 +3552,17 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply- es-errors "^1.3.0" function-bind "^1.1.2" -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7, call-bind@^1.0.8: +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.9.tgz#39a644700c80bc7d0ca9102fc6d1d43b2fd7eee7" + integrity sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + get-intrinsic "^1.3.0" + set-function-length "^1.2.2" + +call-bind@^1.0.7, call-bind@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== @@ -3700,7 +3647,7 @@ chalk@5.6.2: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -4051,11 +3998,11 @@ cssesc@^3.0.0: integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== cssstyle@^4.0.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.3.0.tgz#83db22d1aec8eb7e5ecd812b4d14a17fb3dd243d" - integrity sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ== + version "4.6.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.6.0.tgz#ea18007024e3167f4f105315f3ec2d982bf48ed9" + integrity sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg== dependencies: - "@asamuzakjp/css-color" "^3.1.1" + "@asamuzakjp/css-color" "^3.2.0" rrweb-cssom "^0.8.0" csstype@^3.1.3: @@ -4178,9 +4125,9 @@ decamelize@^4.0.0: integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== decimal.js@^10.4.3: - version "10.5.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22" - integrity sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw== + version "10.6.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" + integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== deep-eql@4.0.1: version "4.0.1" @@ -4442,13 +4389,20 @@ encodeurl@^2.0.0: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +end-of-stream@^1.4.1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + enhanced-resolve@^5.17.1: version "5.18.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" @@ -4467,6 +4421,11 @@ entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + entities@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" @@ -5133,9 +5092,9 @@ file-entry-cache@^8.0.0: flat-cache "^4.0.0" filelist@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" - integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + version "1.0.6" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.6.tgz#1e8870942a7c636c862f7c49b9394937b6a995a3" + integrity sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA== dependencies: minimatch "^5.0.1" @@ -5404,7 +5363,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 +5414,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" @@ -6136,14 +6090,13 @@ jackspeak@^3.1.2: "@pkgjs/parseargs" "^0.11.0" jake@^10.8.5: - version "10.9.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" - integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + version "10.9.4" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6" + integrity sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA== dependencies: - async "^3.2.3" - chalk "^4.0.2" + async "^3.2.6" filelist "^1.0.4" - minimatch "^3.1.2" + picocolors "^1.1.1" jiti@^2.6.1: version "2.7.0" @@ -6443,6 +6396,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" @@ -6488,10 +6446,10 @@ 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: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.17.21: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== log-symbols@4.1.0, log-symbols@^4.1.0: version "4.1.0" @@ -6669,7 +6627,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@3.1.2, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@3.1.2, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -6690,10 +6648,17 @@ minimatch@9.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^3.1.1: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + minimatch@^5.0.1, minimatch@^5.1.0: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== dependencies: brace-expansion "^2.0.1" @@ -6840,11 +6805,11 @@ netmask@^2.0.2: integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== nightwatch-axe-verbose@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/nightwatch-axe-verbose/-/nightwatch-axe-verbose-2.3.1.tgz#42cd226989cb5205b699db42d74b1b967587d099" - integrity sha512-C6N95bwPHsRnv04eVIwJ6w5m6X1+Pddvo6nzpzOHQlO0j+pYRVU7zaQmFUJ0L4cqeUxReNEXyTUg/R9WWfHk7w== + version "2.5.1" + resolved "https://registry.yarnpkg.com/nightwatch-axe-verbose/-/nightwatch-axe-verbose-2.5.1.tgz#6bed281a4b3ba902f70c761d51e36f7177dcc99d" + integrity sha512-vvLUMyIbGHB8CA5XEGfliPstNCplcHeMn/CWi4cyg0CWMqWypGrV2IgP+WmiWpUgs0qvPmqVHeRHf0BTT7Ez2Q== dependencies: - axe-core "^4.9.1" + axe-core "^4.11.1" nightwatch@3.12.2: version "3.12.2" @@ -6947,9 +6912,9 @@ nth-check@^2.1.1: boolbase "^1.0.0" nwsapi@^2.2.12: - version "2.2.20" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef" - integrity sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA== + version "2.2.27" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.27.tgz#a7c71fc7d401546ad94a026c30e868c078a70e87" + integrity sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw== object-inspect@^1.13.3, object-inspect@^1.13.4: version "1.13.4" @@ -7237,11 +7202,11 @@ parse-link-header@2.0.0: xtend "~4.0.1" parse5@^7.1.2: - version "7.2.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a" - integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== + version "7.3.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== dependencies: - entities "^4.5.0" + entities "^6.0.0" parseurl@^1.3.3: version "1.3.3" @@ -7336,11 +7301,16 @@ picocolors@^1.0.0, picocolors@^1.1.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^2.2.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== + picomatch@^4.0.2, picomatch@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" @@ -7369,9 +7339,9 @@ pirates@^4.0.6: integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== piscina@^4.3.1: - version "4.9.2" - resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.9.2.tgz#80f2c2375231720337c703e443941adfac8caf75" - integrity sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ== + version "4.9.4" + resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.9.4.tgz#1fe911df1ffe920325c24822e5d7b596833a0df7" + integrity sha512-RyBDr2VheQ8ZfH3N8SzQZHztqVyOtadTwLnUYof6gdj5161/eu2wNhCbGl5GHnNKDpnkicJYpKtL2J+y/gk6XA== optionalDependencies: "@napi-rs/nice" "^1.0.1" @@ -7763,11 +7733,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" @@ -8245,6 +8210,14 @@ side-channel-list@^1.0.0: es-errors "^1.3.0" object-inspect "^1.13.3" +side-channel-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-map@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" @@ -8266,7 +8239,18 @@ side-channel-weakmap@^1.0.2: object-inspect "^1.13.3" side-channel-map "^1.0.1" -side-channel@^1.0.4, side-channel@^1.1.0: +side-channel@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" + integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-list "^1.0.1" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +side-channel@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== @@ -8784,9 +8768,9 @@ tldts@^7.0.5: tldts-core "^7.4.3" tmp@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + version "0.2.7" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.7.tgz#26f4db11d1601ce8012dcb8a798ece1c06a99059" + integrity sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw== to-regex-range@^5.0.1: version "5.0.1" @@ -8823,9 +8807,9 @@ tough-cookie@^6.0.1: tldts "^7.0.5" tr46@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.0.tgz#4a077922360ae807e172075ce5beb79b36e4a101" - integrity sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw== + version "5.1.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca" + integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw== dependencies: punycode "^2.3.1" @@ -9254,13 +9238,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" @@ -9350,7 +9327,20 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.13, which-typed-array@^1.1.16, which-typed-array@^1.1.18, which-typed-array@^1.1.19: +which-typed-array@^1.1.13: + version "1.1.22" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.22.tgz#8f3cc78aefb40b437346dd40a1dbfa5d1da43fe9" + integrity sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.9" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + +which-typed-array@^1.1.16, which-typed-array@^1.1.18, which-typed-array@^1.1.19: version "1.1.19" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== @@ -9452,9 +9442,9 @@ write-file-atomic@^5.0.1: signal-exit "^4.0.1" ws@^8.18.0: - version "8.18.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" - integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + version "8.21.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.3.tgz#660b4faddb6a3e575c86e078126919961f4de4fc" + integrity sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw== ws@^8.19.0, ws@^8.21.0: version "8.21.0"