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 @@