diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..48f99d6ca --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": ["@babel/plugin-transform-runtime", "@vue/babel-plugin-jsx"], + "comments": true +} diff --git a/.woodpecker/build-package.yaml b/.woodpecker/build-package.yaml index b3516c948..63aa80e0f 100644 --- a/.woodpecker/build-package.yaml +++ b/.woodpecker/build-package.yaml @@ -14,7 +14,7 @@ labels: steps: build: - image: docker.io/node:24-alpine + image: docker.io/node:26-alpine commands: - apk add --no-cache zip git - yarn --frozen-lockfile diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index a11bdd8bc..70fb8df75 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -11,7 +11,7 @@ labels: steps: build: - image: docker.io/node:24-alpine + image: docker.io/node:26-alpine commands: - yarn --frozen-lockfile - yarn build diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 334f41aea..4433e1be8 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -9,10 +9,16 @@ when: steps: install-depends: image: &node-image - docker.io/node:24-alpine + docker.io/node:26-alpine commands: - yarn --frozen-lockfile + eslint: + image: *node-image + depends_on: install-depends + commands: + - yarn ci-eslint + biome: image: *node-image depends_on: install-depends diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml index 5927cec52..c3f2c3793 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/test.yaml @@ -36,7 +36,6 @@ steps: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" entrypoint: *script_file_entrypoint commands: - - npm install --global yarn - | if [ "${CI_PIPELINE_EVENT}" != "pull_request" ]; then mkdir -pv $APT_CACHE_DIR && apt-get -qq update diff --git a/biome.json b/biome.json index a03cd02b5..5ef6f79ee 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", "vcs": { "enabled": true, "clientKind": "git", @@ -18,7 +18,7 @@ "vue": "recommended" }, "rules": { - "preset": "none", + "recommended": false, "complexity": { "noAdjacentSpacesInRegex": "error", "noExtraBooleanCast": "error", @@ -80,9 +80,6 @@ "noUselessRegexBackrefs": "error", "noWith": "error", "useGetterReturn": "error" - }, - "style": { - "useVueMultiWordComponentNames": "off" } } }, diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..4335b36d5 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,20 @@ +import { defineConfig, globalIgnores } from 'eslint/config' +import vue from 'eslint-plugin-vue' + +export default defineConfig([ + ...vue.configs['flat/recommended'], + globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']), + { + files: ['src/**/*.vue'], + languageOptions: { + parserOptions: { + parser: '@babel/eslint-parser', + }, + }, + + rules: { + 'vue/require-prop-types': 0, + 'vue/multi-word-component-names': 0, + }, + }, +]) diff --git a/package.json b/package.json index b4c20ca8c..3ba4c6c24 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "e2e": "sh ./tools/e2e/run.sh", "test": "yarn run unit && yarn run e2e", "ci-biome": "yarn exec biome check", + "ci-eslint": "yarn exec eslint", "ci-stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'", - "lint": "yarn ci-biome; yarn ci-stylelint", - "lint-fix": "yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write" + "lint": "yarn ci-biome; yarn ci-eslint; yarn ci-stylelint", + "lint-fix": "yarn exec eslint -- --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write" }, "dependencies": { + "@babel/runtime": "^8.0.0", "@chenfengyuan/vue-qrcode": "2.0.0", "@fortawesome/fontawesome-svg-core": "7.1.0", "@fortawesome/free-regular-svg-icons": "7.1.0", @@ -54,6 +56,11 @@ "vue-virtual-scroller": "^3.0.5" }, "devDependencies": { + "@babel/core": "^8.0.0", + "@babel/eslint-parser": "^8.0.0", + "@babel/plugin-transform-runtime": "^8.0.0", + "@babel/preset-env": "^8.0.0", + "@babel/register": "^8.0.0", "@biomejs/biome": "2.5.11", "@pinia/testing": "1.0.3", "@ungap/event-target": "0.2.4", @@ -64,19 +71,24 @@ "@vitest/browser-playwright": "^4.1.11", "@vitest/coverage-v8": "^4.1.11", "@vitest/ui": "^4.1.11", + "@vue/babel-plugin-jsx": "3.0.0", "@vue/compiler-sfc": "3.5.22", "@vue/devtools-api": "^8.1.5", "@vue/test-utils": "2.5.0", "autoprefixer": "10.4.21", "chalk": "5.6.2", "cross-spawn": "7.0.6", + "eslint": "9.39.2", + "eslint-plugin-vue": "10.6.2", "iso-639-1": "3.1.5", "msw": "2.14.6", + "nightwatch": "3.12.2", "playwright": "1.61.0", "postcss": "8.5.6", "postcss-html": "^1.5.0", "postcss-scss": "^4.0.6", "sass-embedded": "^1.100.0", + "selenium-server": "3.141.59", "serve-static": "2.2.0", "stylelint": "16.25.0", "stylelint-config-html": "^1.1.0", @@ -85,14 +97,15 @@ "stylelint-config-recommended-vue": "^1.6.0", "stylelint-config-standard": "38.0.0", "vite": "^8.0.0", - "vite-plugin-biome": "^1.2.0", + "vite-plugin-eslint2": "^5.1.0", "vite-plugin-stylelint": "^6.1.0", "vite-plugin-vue-devtools": "^8.2.1", - "vitest": "^4.1.11" + "vitest": "^4.1.11", + "vue-eslint-parser": "10.2.0" }, "type": "module", "engines": { - "node": ">= 24.0.0" + "node": ">= 26.0.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/App.js b/src/App.js index ae38204b6..ed3d43e4a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import { throttle } from 'lodash-es' +import { throttle } from 'lodash' import { mapState } from 'pinia' import { defineAsyncComponent } from 'vue' @@ -49,12 +49,6 @@ 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 cad329a78..bd19c5c10 100644 --- a/src/App.vue +++ b/src/App.vue @@ -75,7 +75,6 @@ - diff --git a/src/api/helpers.js b/src/api/helpers.js index 6afd20468..f23960ed9 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -1,4 +1,4 @@ -import { snakeCase } from 'lodash-es' +import { snakeCase } from 'lodash' import { StatusCodeError } from 'src/services/errors/errors' diff --git a/src/api/user.js b/src/api/user.js index 2f356ebbc..d3a2a4a70 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -1,4 +1,4 @@ -import { last } from 'lodash-es' +import { last } from 'lodash' import { paramsString, promisedRequest } from './helpers.js' import { fetchFriends, MASTODON_STATUS_URL } from './public.js' @@ -43,10 +43,9 @@ 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' -export const MASTODON_APPROVE_USER_URL = (id) => +const MASTODON_APPROVE_USER_URL = (id) => `/api/v1/follow_requests/${id}/authorize` -export const MASTODON_DENY_USER_URL = (id) => - `/api/v1/follow_requests/${id}/reject` +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` diff --git a/src/components/chat_message_list/chat_message_list.js b/src/components/chat_message_list/chat_message_list.js index 6aef4336b..a51e6a0e4 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-es' +import { orderBy, uniqueId } from 'lodash' import ChatMessage from 'src/components/chat_message/chat_message.vue' diff --git a/src/components/chat_view/chat_view.js b/src/components/chat_view/chat_view.js index 7d80459bb..a0abcb5fe 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-es' +import { get, maxBy, minBy, sortBy, throttle } from 'lodash' 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 821a1be0e..53396f532 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/gallery/gallery.js b/src/components/gallery/gallery.js index f6c79ac76..c244a8cb3 100644 --- a/src/components/gallery/gallery.js +++ b/src/components/gallery/gallery.js @@ -1,4 +1,4 @@ -import { set, sumBy } from 'lodash-es' +import { set, sumBy } from 'lodash' import Attachment from 'src/components/attachment/attachment.vue' diff --git a/src/components/list/list.js b/src/components/list/list.js index 2283f5260..8abf02fd6 100644 --- a/src/components/list/list.js +++ b/src/components/list/list.js @@ -1,4 +1,4 @@ -import { isEmpty } from 'lodash-es' +import { isEmpty } from 'lodash' 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 4dde2f589..22568135a 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-es' +import { debounce } from 'lodash' import Checkbox from 'src/components/checkbox/checkbox.vue' 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 3fbd9b77b..0ffab6f78 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-es' +import { debounce } from 'lodash' 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 73bf6e515..9b948e306 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-es' +import { last } from 'lodash' 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 c3530975e..7f2a16186 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-es' +import { get } from 'lodash' import { mapState } from 'pinia' import { useInstanceStore } from 'src/stores/instance.js' diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index cff7f655f..51b60fb29 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -1,4 +1,5 @@ -import { mapActions, mapState } from 'pinia' +import { mapState } from 'pinia' +import { defineAsyncComponent } from 'vue' import Report from 'src/components/report/report.vue' import StatusContent from 'src/components/status_content/status_content.vue' @@ -15,10 +16,13 @@ import { import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useInstanceStore } from 'src/stores/instance.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useNotificationsStore } from 'src/stores/notifications.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useStatusesStore } from 'src/stores/statuses.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUsersStore } from 'src/stores/users.js' +import { approveUser, denyUser } from 'src/api/user.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { library } from '@fortawesome/fontawesome-svg-core' @@ -54,6 +58,8 @@ const Notification = { selecting: false, statusExpanded: false, unmuted: false, + showingApproveConfirmDialog: false, + showingDenyConfirmDialog: false, } }, props: ['notification'], @@ -67,6 +73,9 @@ const Notification = { UserPopover, UserLink, + ConfirmModal: defineAsyncComponent( + () => import('src/components/confirm_modal/confirm_modal.vue'), + ), }, mounted() { document.addEventListener('selectionchange', this.onContentSelect) @@ -116,7 +125,51 @@ const Notification = { toggleMute() { this.unmuted = !this.unmuted }, - ...mapActions(useFollowRequestsStore, ['approve', 'deny']), + showApproveConfirmDialog() { + this.showingApproveConfirmDialog = true + }, + hideApproveConfirmDialog() { + this.showingApproveConfirmDialog = false + }, + showDenyConfirmDialog() { + this.showingDenyConfirmDialog = true + }, + hideDenyConfirmDialog() { + this.showingDenyConfirmDialog = false + }, + approveUser() { + if (this.shouldConfirmApprove) { + this.showApproveConfirmDialog() + } else { + this.doApprove() + } + }, + doApprove() { + approveUser({ + id: this.user.id, + credentials: useOAuthStore().token, + }) + useFollowRequestsStore().remove(this.user.id) + useNotificationsStore().markSingleNotificationAsSeen(this.notification.id) + this.hideApproveConfirmDialog() + }, + denyUser() { + if (this.shouldConfirmDeny) { + this.showDenyConfirmDialog() + } else { + this.doDeny() + } + }, + doDeny() { + denyUser({ + id: this.user.id, + credentials: useOAuthStore().token, + }).then(() => { + useNotificationsStore().dismissNotificationLocal(this.notification.id) + useFollowRequestsStore().remove(this.user.id) + }) + this.hideDenyConfirmDialog() + }, }, computed: { status() { @@ -167,6 +220,12 @@ const Notification = { scaleMfm() { return this.mergedConfig.scaleMfm }, + shouldConfirmApprove() { + return this.mergedConfig.modalOnApproveFollow + }, + shouldConfirmDeny() { + return this.mergedConfig.modalOnDenyFollow + }, ...mapState(useUsersStore, ['currentUser']), }, } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 156c9ac11..601e574db 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -226,7 +226,7 @@