diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 06fbf45f9..e2946b162 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ # This file is a template, and might need editing before it works on your project. # Official framework image. Look for the different tagged releases at: # https://hub.docker.com/r/library/node/tags/ -image: node:18 +image: node:20 stages: - check-changelog @@ -73,7 +73,7 @@ test: e2e-pleroma: stage: test - image: mcr.microsoft.com/playwright:v1.57.0-jammy + image: mcr.microsoft.com/playwright:v1.61.0-jammy services: - name: postgres:15-alpine alias: db @@ -157,7 +157,7 @@ e2e-pleroma: variables: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" FF_NETWORK_PER_BUILD: "true" - PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable + PLEROMA_IMAGE: git.pleroma.social/pleroma/pleroma:stable POSTGRES_USER: pleroma POSTGRES_PASSWORD: pleroma POSTGRES_DB: pleroma @@ -175,7 +175,7 @@ e2e-pleroma: NOTIFY_EMAIL: $E2E_ADMIN_EMAIL VITE_PROXY_TARGET: http://pleroma:4000 VITE_PROXY_ORIGIN: http://localhost:4000 - E2E_BASE_URL: http://localhost:8080 + E2E_BASE_URL: http://localhost:8099 script: - npm install -g yarn@1.22.22 - yarn --frozen-lockfile diff --git a/.node-version b/.node-version index 08b7109d0..5bd681170 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18.20.8 +20.19.0 diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index 059fdb1ff..af0bb98e3 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -16,7 +16,7 @@ labels: steps: build: - image: docker.io/node:18-alpine + image: docker.io/node:20-alpine commands: - apk add --no-cache zip git - yarn --frozen-lockfile diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml index 237135ee9..257887338 100644 --- a/.woodpecker/lint.yaml +++ b/.woodpecker/lint.yaml @@ -9,7 +9,7 @@ when: steps: install-depends: image: &node-image - docker.io/node:18-alpine + docker.io/node:20-alpine commands: - yarn --frozen-lockfile diff --git a/.woodpecker/test-e2e.yaml b/.woodpecker/test-e2e.yaml index c0bf103cd..2a7d1511d 100644 --- a/.woodpecker/test-e2e.yaml +++ b/.woodpecker/test-e2e.yaml @@ -25,12 +25,12 @@ variables: steps: test: - image: mcr.microsoft.com/playwright:v1.57.0-jammy + image: mcr.microsoft.com/playwright:v1.61.0-jammy entrypoint: *script_file_entrypoint environment: APT_CACHE_DIR: apt-cache DEBIAN_FRONTEND: noninteractive - E2E_BASE_URL: http://localhost:8080 + E2E_BASE_URL: http://localhost:8099 FF_NETWORK_PER_BUILD: "true" PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" VITE_PROXY_ORIGIN: "http://pleroma:4000" diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml index acc48aacc..89f6f2f93 100644 --- a/.woodpecker/test.yaml +++ b/.woodpecker/test.yaml @@ -25,7 +25,7 @@ variables: steps: test: - image: mcr.microsoft.com/playwright:v1.57.0-jammy + image: mcr.microsoft.com/playwright:v1.61.0-jammy environment: APT_CACHE_DIR: apt-cache DEBIAN_FRONTEND: noninteractive diff --git a/biome.json b/biome.json index 6a464a0e5..5ef6f79ee 100644 --- a/biome.json +++ b/biome.json @@ -46,6 +46,7 @@ "noUnusedLabels": "error", "noUnusedPrivateClassMembers": "error", "noUnusedVariables": "error", + "noUnusedImports": "error", "useIsNan": "error", "useValidForDirection": "error", "useValidTypeof": "error", diff --git a/build/check-versions.mjs b/build/check-versions.mjs index 8c5968a30..c22004b00 100644 --- a/build/check-versions.mjs +++ b/build/check-versions.mjs @@ -36,7 +36,6 @@ export default function () { const warning = warnings[i] console.warn(' ' + warning) } - console.warn() process.exit(1) } } diff --git a/build/sw_plugin.js b/build/sw_plugin.js index 03c5978d7..2f0a4819d 100644 --- a/build/sw_plugin.js +++ b/build/sw_plugin.js @@ -1,7 +1,6 @@ -import { readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import * as esbuild from 'esbuild' +import { exactRegex } from '@rolldown/pluginutils' import { build } from 'vite' import { @@ -15,106 +14,6 @@ const getSWMessagesAsText = async () => { } const projectRoot = dirname(dirname(fileURLToPath(import.meta.url))) -const swEnvName = 'virtual:pleroma-fe/service_worker_env' -const swEnvNameResolved = '\0' + swEnvName -const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };` -const getProdSwEnv = ({ assets }) => - `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };` - -export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { - const swFullSrc = resolve(projectRoot, swSrc) - const esbuildAlias = {} - Object.entries(alias).forEach(([source, dest]) => { - esbuildAlias[source] = dest.startsWith('/') ? projectRoot + dest : dest - }) - - return { - name: 'dev-sw-plugin', - apply: 'serve', - configResolved() { - /* no-op */ - }, - resolveId(id) { - const name = id.startsWith('/') ? id.slice(1) : id - if (name === swDest) { - return swFullSrc - } else if (name === swEnvName) { - return swEnvNameResolved - } - return null - }, - async load(id) { - if (id === swFullSrc) { - return readFile(swFullSrc, 'utf-8') - } else if (id === swEnvNameResolved) { - return getDevSwEnv() - } - return null - }, - /** - * vite does not bundle the service worker - * during dev, and firefox does not support ESM as service worker - * https://bugzilla.mozilla.org/show_bug.cgi?id=1360870 - */ - async transform(code, id) { - if (id === swFullSrc && transformSW) { - const res = await esbuild.build({ - entryPoints: [swSrc], - bundle: true, - write: false, - outfile: 'sw-pleroma.js', - alias: esbuildAlias, - plugins: [ - { - name: 'vite-like-root-resolve', - setup(b) { - b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({ - path: resolve(projectRoot, args.path.slice(1)), - })) - }, - }, - { - name: 'sw-messages', - setup(b) { - b.onResolve( - { filter: new RegExp('^' + swMessagesName + '$') }, - (args) => ({ - path: args.path, - namespace: 'sw-messages', - }), - ) - b.onLoad( - { filter: /.*/, namespace: 'sw-messages' }, - async () => ({ - contents: await getSWMessagesAsText(), - }), - ) - }, - }, - { - name: 'sw-env', - setup(b) { - b.onResolve( - { filter: new RegExp('^' + swEnvName + '$') }, - (args) => ({ - path: args.path, - namespace: 'sw-env', - }), - ) - b.onLoad({ filter: /.*/, namespace: 'sw-env' }, () => ({ - contents: getDevSwEnv(), - })) - }, - }, - ], - }) - const text = res.outputFiles[0].text - return text - } - }, - } -} - // Idea taken from // https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/plugins/build.ts // rollup does not support compiling to iife if we want to code-split; @@ -122,12 +21,17 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { // Run another vite build just for the service worker targeting iife at // the end of the build. export const buildSwPlugin = ({ swSrc, swDest }) => { + const swFullSrc = resolve(projectRoot, swSrc) + const swEnvName = 'virtual:pleroma-fe/service_worker_env' + const swEnvNameResolved = '\0' + swEnvName + let config + return { name: 'build-sw-plugin', enforce: 'post', - apply: 'build', configResolved(resolvedConfig) { + resolvedConfig config = { define: resolvedConfig.define, resolve: resolvedConfig.resolve, @@ -135,15 +39,16 @@ export const buildSwPlugin = ({ swSrc, swDest }) => { publicDir: false, build: { ...resolvedConfig.build, - lib: { - entry: swSrc, - formats: ['iife'], - name: 'sw_pleroma', - }, emptyOutDir: false, - rollupOptions: { + rolldownOptions: { + input: { + main: swSrc, + }, + context: 'self', output: { entryFileNames: swDest, + codeSplitting: false, + format: 'iife', }, }, }, @@ -157,29 +62,64 @@ export const buildSwPlugin = ({ swSrc, swDest }) => { const assets = Object.keys(bundle) .filter((name) => !/\.map$/.test(name)) .map((name) => '/' + name) + config.plugins.push({ name: 'build-sw-env-plugin', - resolveId(id) { - if (id === swEnvName) { - return swEnvNameResolved - } - return null + mode: 'production', + resolveId: { + filter: { id: exactRegex(swEnvName) }, + handler: () => swEnvNameResolved, }, - load(id) { - if (id === swEnvNameResolved) { - return getProdSwEnv({ assets }) - } - return null + load: { + filter: { id: exactRegex(swEnvNameResolved) }, + handler() { + return `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };` + }, }, }) }, }, + resolveId: { + filter: { id: new RegExp(swDest) }, + handler() { + return swFullSrc + }, + }, + load: { + filter: { id: new RegExp(swFullSrc) }, + async handler() { + config.plugins.push({ + name: 'dummy-sw-env', + mode: 'development', + resolveId: { + filter: { id: exactRegex(swEnvName) }, + handler: () => swEnvNameResolved, + }, + load: { + filter: { id: exactRegex(swEnvNameResolved) }, + handler: () => 'self.serviceWorkerOption = { assets: [] }', + }, + }) + + try { + const swBundle = await build(config) + return swBundle.output[0] + } catch (e) { + console.error('Error building ServiceWorker:', e) + } + }, + }, closeBundle: { order: 'post', sequential: true, async handler() { + if (process.env.VITEST) return console.info('Building service worker for production') - await build(config) + try { + await build(config) + } catch (e) { + console.error('Error building ServiceWorker:', e) + } }, }, } diff --git a/changelog.d/api-refactor.skip b/changelog.d/api-refactor.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/fast.change b/changelog.d/fast.change new file mode 100644 index 000000000..1f0a89092 --- /dev/null +++ b/changelog.d/fast.change @@ -0,0 +1 @@ +Migrated to Vite 8 and optimized our imports, more stuff is loaded on-demand, reducing the initial load time and transfer size diff --git a/changelog.d/minor.change b/changelog.d/minor.change index a234ad9c2..979d36955 100644 --- a/changelog.d/minor.change +++ b/changelog.d/minor.change @@ -4,3 +4,4 @@ reply/quote now is a radio group and wraps, fixes overflow on languages where la personal note input is now bigger moved "edit pinned" to the bottom for status action buttons. dots status action button drops down instead of up to avoid hiding the action buttons +improved attachment description (alt text) input and display diff --git a/changelog.d/minor.fix b/changelog.d/minor.fix index 69d7b11fe..420836364 100644 --- a/changelog.d/minor.fix +++ b/changelog.d/minor.fix @@ -9,3 +9,4 @@ second language input not having header post form's bottom left buttons not showing their toggled state some font overrides not working popovers opening outside of window's boundaries +occasional blank page when showing new posts diff --git a/changelog.d/more-fixes.skip b/changelog.d/more-fixes.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/profilebg.add b/changelog.d/profilebg.add new file mode 100644 index 000000000..a2c79074a --- /dev/null +++ b/changelog.d/profilebg.add @@ -0,0 +1 @@ +displaying other user's backgrounds (if supported by BE) diff --git a/changelog.d/reply-quote-config.fix b/changelog.d/reply-quote-config.fix new file mode 100644 index 000000000..b6ac4e5e9 --- /dev/null +++ b/changelog.d/reply-quote-config.fix @@ -0,0 +1 @@ +Fix reply form crash when quote-reply settings are unavailable diff --git a/changelog.d/user-management.add b/changelog.d/user-management.add new file mode 100644 index 000000000..ccd217f19 --- /dev/null +++ b/changelog.d/user-management.add @@ -0,0 +1 @@ +User administration + post scope/sensitivity admin change support diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 75a4979a1..c740e69c4 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -12,7 +12,7 @@ services: retries: 30 pleroma: - image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable} + image: ${PLEROMA_IMAGE:-git.pleroma.social/pleroma/pleroma:stable} environment: DB_USER: pleroma DB_PASS: pleroma @@ -39,6 +39,8 @@ services: interval: 5s timeout: 3s retries: 60 + ports: + - 4000:4000 e2e: build: @@ -51,7 +53,7 @@ services: CI: "1" VITE_PROXY_TARGET: http://pleroma:4000 VITE_PROXY_ORIGIN: http://localhost:4000 - E2E_BASE_URL: http://localhost:8080 + E2E_BASE_URL: http://localhost:8099 E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin} E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin} command: ["yarn", "e2e:pw"] diff --git a/docker/e2e/Dockerfile.e2e b/docker/e2e/Dockerfile.e2e index e84359ceb..7e3fbfbf1 100644 --- a/docker/e2e/Dockerfile.e2e +++ b/docker/e2e/Dockerfile.e2e @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/playwright:v1.57.0-jammy +FROM mcr.microsoft.com/playwright:v1.61.0-jammy WORKDIR /app diff --git a/package.json b/package.json index 85e79a1d3..f594d7a99 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "localforage": "1.10.0", "parse-link-header": "2.0.0", "phoenix": "1.8.1", - "pinia": "^3.0.0", + "pinia": "^3.0.4", "punycode.js": "2.3.1", "qrcode": "1.5.4", "querystring-es3": "0.2.1", @@ -65,10 +65,12 @@ "@biomejs/biome": "2.3.11", "@pinia/testing": "1.0.3", "@ungap/event-target": "0.2.4", - "@vitejs/plugin-vue": "^5.2.1", - "@vitejs/plugin-vue-jsx": "^4.1.1", - "@vitest/browser": "^3.0.7", - "@vitest/ui": "^3.0.7", + "@vitejs/devtools": "^0.3.1", + "@vitejs/plugin-vue": "^6.0.7", + "@vitejs/plugin-vue-jsx": "^5.1.5", + "@vitest/browser-playwright": "^4.1.7", + "@vitest/browser": "^4.1.7", + "@vitest/ui": "^4.1.7", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-plugin-jsx": "1.5.0", "@vue/compiler-sfc": "3.5.22", @@ -94,13 +96,14 @@ "http-proxy-middleware": "3.0.5", "iso-639-1": "3.1.5", "lodash": "4.17.21", - "msw": "2.10.5", + "msw": "2.14.6", "nightwatch": "3.12.2", - "playwright": "1.57.0", + "oxc": "^1.0.1", + "playwright": "1.61.0", "postcss": "8.5.6", "postcss-html": "^1.5.0", "postcss-scss": "^4.0.6", - "sass": "1.93.2", + "sass-embedded": "^1.100.0", "selenium-server": "3.141.59", "semver": "7.7.3", "serve-static": "2.2.0", @@ -113,10 +116,10 @@ "stylelint-config-recommended-scss": "^14.0.0", "stylelint-config-recommended-vue": "^1.6.0", "stylelint-config-standard": "38.0.0", - "vite": "^6.1.0", - "vite-plugin-eslint2": "^5.0.3", - "vite-plugin-stylelint": "^6.0.0", - "vitest": "^3.0.7", + "vite": "^8.0.0", + "vite-plugin-eslint2": "^5.1.0", + "vite-plugin-stylelint": "^6.1.0", + "vitest": "^4.1.7", "vue-eslint-parser": "10.2.0" }, "type": "module", diff --git a/src/App.js b/src/App.js index 9fb4d32e5..934b96713 100644 --- a/src/App.js +++ b/src/App.js @@ -2,22 +2,15 @@ import { throttle } from 'lodash' import { mapState } from 'pinia' import { defineAsyncComponent } from 'vue' -import DesktopNav from './components/desktop_nav/desktop_nav.vue' -import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue' -import FeaturesPanel from './components/features_panel/features_panel.vue' -import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' -import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue' -import MediaModal from './components/media_modal/media_modal.vue' -import MobileNav from './components/mobile_nav/mobile_nav.vue' -import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' -import NavPanel from './components/nav_panel/nav_panel.vue' -import PostStatusModal from './components/post_status_modal/post_status_modal.vue' -import ShoutPanel from './components/shout_panel/shout_panel.vue' -import SideDrawer from './components/side_drawer/side_drawer.vue' -import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue' -import UserPanel from './components/user_panel/user_panel.vue' -import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' -import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' +import DesktopNav from 'src/components/desktop_nav/desktop_nav.vue' +import FeaturesPanel from 'src/components/features_panel/features_panel.vue' +import GlobalError from 'src/components/global_error/global_error.vue' +import GlobalNoticeList from 'src/components/global_notice_list/global_notice_list.vue' +import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue' +import MobileNav from 'src/components/mobile_nav/mobile_nav.vue' +import MobilePostStatusButton from 'src/components/mobile_post_status_button/mobile_post_status_button.vue' +import NavPanel from 'src/components/nav_panel/nav_panel.vue' +import UserPanel from 'src/components/user_panel/user_panel.vue' import { getOrCreateServiceWorker } from './services/sw/sw' import { windowHeight, windowWidth } from './services/window_utils/window_utils' @@ -29,9 +22,6 @@ import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useShoutStore } from 'src/stores/shout.js' -import messages from 'src/i18n/messages' -import localeService from 'src/services/locale/locale.service.js' - // Helper to unwrap reactive proxies window.toValue = (x) => JSON.parse(JSON.stringify(x)) @@ -41,27 +31,45 @@ export default { UserPanel, NavPanel, Notifications: defineAsyncComponent( - () => import('./components/notifications/notifications.vue'), + () => import('src/components/notifications/notifications.vue'), ), InstanceSpecificPanel, FeaturesPanel, - WhoToFollowPanel, - ShoutPanel, - MediaModal, - SideDrawer, + WhoToFollowPanel: defineAsyncComponent( + () => + import('src/components/who_to_follow_panel/who_to_follow_panel.vue'), + ), + ShoutPanel: defineAsyncComponent( + () => import('src/components/shout_panel/shout_panel.vue'), + ), + MediaModal: defineAsyncComponent( + () => import('src/components/media_modal/media_modal.vue'), + ), MobilePostStatusButton, MobileNav, DesktopNav, SettingsModal: defineAsyncComponent( - () => import('./components/settings_modal/settings_modal.vue'), + () => import('src/components/settings_modal/settings_modal.vue'), ), UpdateNotification: defineAsyncComponent( - () => import('./components/update_notification/update_notification.vue'), + () => + import('src/components/update_notification/update_notification.vue'), ), - UserReportingModal, - PostStatusModal, - EditStatusModal, - StatusHistoryModal, + PostStatusModal: defineAsyncComponent( + () => import('src/components/post_status_modal/post_status_modal.vue'), + ), + UserReportingModal: defineAsyncComponent( + () => + import('src/components/user_reporting_modal/user_reporting_modal.vue'), + ), + EditStatusModal: defineAsyncComponent( + () => import('src/components/edit_status_modal/edit_status_modal.vue'), + ), + StatusHistoryModal: defineAsyncComponent( + () => + import('src/components/status_history_modal/status_history_modal.vue'), + ), + GlobalError, GlobalNoticeList, }, data: () => ({ @@ -149,13 +157,23 @@ export default { userBackground() { return this.currentUser.background_image }, + foreignProfileBackground() { + return ( + useMergedConfigStore().mergedConfig.allowForeignUserBackground && + useInterfaceStore().foreignProfileBackground + ) + }, instanceBackground() { return useMergedConfigStore().mergedConfig.hideInstanceWallpaper ? null : this.instanceBackgroundUrl }, background() { - return this.userBackground || this.instanceBackground + return ( + this.foreignProfileBackground || + this.userBackground || + this.instanceBackground + ) }, bgStyle() { if (this.background) { diff --git a/src/App.scss b/src/App.scss index f556a7d9d..0de5a3577 100644 --- a/src/App.scss +++ b/src/App.scss @@ -50,7 +50,7 @@ body { // have a cursor/pointer to operate them @media (any-pointer: fine) { * { - scrollbar-color: var(--fg) transparent; + scrollbar-color: var(--icon) transparent; &::-webkit-scrollbar { background: transparent; @@ -130,7 +130,7 @@ body { } // Body should have background to scrollbar otherwise it will use white (body color?) html { - scrollbar-color: var(--fg) var(--wallpaper); + scrollbar-color: var(--icon) var(--wallpaper); background: var(--wallpaper); } } @@ -144,6 +144,25 @@ h4 { margin: 0; } +code { + background: var(--background); + border: 1px solid var(--border); + border-radius: var(--roundness); + padding: 0 0.2em; + + & pre, + pre & { + display: block; + overflow-x: auto; + padding: 0.2em; + } + + &.pre { + white-space: pre; + display: block; + } +} + .iconLetter { display: inline-block; text-align: center; @@ -200,6 +219,7 @@ nav { background-color: var(--wallpaper); background-image: var(--body-background-image); background-position: 50%; + transition: background-image 1s; } .underlay { @@ -410,6 +430,14 @@ nav { button:not(.button-default) { color: var(--text); font-size: 100%; + text-align: initial; + padding: 0; + background: none; + border: none; + outline: none; + display: inline; + font-family: inherit; + line-height: unset; } &.disabled { @@ -417,45 +445,6 @@ nav { } } -.list-item { - border-color: var(--border); - border-style: solid; - border-width: 0; - border-top-width: 1px; - - &.-active, - &:hover { - border-top-width: 1px; - border-bottom-width: 1px; - } - - &.-active + &, - &:hover + & { - border-top-width: 0; - } - - &:hover + .menu-item-collapsible:not(.-expanded) + &, - &.-active + .menu-item-collapsible:not(.-expanded) + & { - border-top-width: 0; - } - - &[aria-expanded="true"] { - border-bottom-width: 1px; - } - - &:first-child { - border-top-right-radius: var(--roundness); - border-top-left-radius: var(--roundness); - border-top-width: 0; - } - - &:last-child { - border-bottom-right-radius: var(--roundness); - border-bottom-left-radius: var(--roundness); - border-bottom-width: 0; - } -} - .menu-item, .list-item { display: block; @@ -474,22 +463,6 @@ nav { --__line-height: 1.5em; --__horizontal-gap: 0.75em; --__vertical-gap: 0.5em; - - &.-non-interactive { - cursor: auto; - } - - a, - button:not(.button-default) { - text-align: initial; - padding: 0; - background: none; - border: none; - outline: none; - display: inline; - font-family: inherit; - line-height: unset; - } } .button-unstyled { @@ -815,9 +788,11 @@ option { align-items: baseline; line-height: 1.5; + p, span { display: block; flex: 1 1 auto; + margin: 0; } .dismiss { diff --git a/src/App.vue b/src/App.vue index f432d8bc4..84a6f7d3d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -28,10 +28,10 @@ > @@ -73,6 +73,7 @@ + diff --git a/src/api/admin.js b/src/api/admin.js new file mode 100644 index 000000000..c33f863a7 --- /dev/null +++ b/src/api/admin.js @@ -0,0 +1,491 @@ +import { promisedRequest } from './helpers.js' + +const REPORTS = '/api/v1/pleroma/admin/reports' +const CONFIG_URL = '/api/v1/pleroma/admin/config' +const DESCRIPTIONS_URL = '/api/v1/pleroma/admin/config/descriptions' + +const ANNOUNCEMENTS_URL = (id = '') => + `/api/v1/pleroma/admin/announcements/${id}` + +const FRONTENDS_URL = '/api/v1/pleroma/admin/frontends' +const FRONTENDS_INSTALL_URL = '/api/v1/pleroma/admin/frontends/install' + +const USERS_URL = (nickname = '') => `/api/v1/pleroma/admin/users/${nickname}` +const USERS_URL_LIST = ({ + page, + pageSize, + filters = {}, + query = '', + name = '', + email = '', +}) => { + const { + local = false, + external = false, + active = false, + needApproval = false, + unconfirmed = false, + deactivated = false, + isAdmin = true, + isModerator = true, + } = filters + const filters_str = [ + local && 'local', + external && 'external', + active && 'active', + needApproval && 'need_approval', + unconfirmed && 'unconfirmed', + deactivated && 'deactivated', + isAdmin && 'is_admin', + isModerator && 'is_moderator', + ] + .filter((x) => x) + .join(',') + return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}` +} + +const TAG_USER_URL = '/api/pleroma/admin/users/tag' + +const PERMISSION_GROUP_URL = (right) => + `/api/pleroma/admin/users/permission_group/${right}` +const ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate' +const DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate' +const SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest' +const UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest' +const APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve' +const CONFIRM_USERS_URL = '/api/v1/pleroma/admin/users/confirm_email' +const RESEND_CONFIRMATION_EMAIL_URL = + '/api/v1/pleroma/admin/users/resend_confirmation_email' +const LIST_STATUSES_URL = ({ id, page, pageSize, godmode, withReblogs }) => + `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&page=${page}&godmode=${godmode}&with_reblogs=${withReblogs}` +const CHANGE_STATUS_SCOPE_URL = (id) => `/api/v1/pleroma/admin/statuses/${id}` +const REQUIRE_PASSWORD_CHANGE_URL = + '/api/v1/pleroma/admin/users/force_password_reset' + +const DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa' +const EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' +const EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' +const EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}` +const EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download' +const EMOJI_PACKS_DL_REMOTE_ZIP_URL = '/api/v1/pleroma/emoji/packs/download_zip' +const EMOJI_PACKS_LS_REMOTE_URL = (url, page, pageSize) => + `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}` +const EMOJI_UPDATE_FILE_URL = (name) => + `/api/v1/pleroma/emoji/packs/files?name=${name}` + +// + +export const setUsersTags = ({ + tags, + credentials, + value, + screen_names: nicknames, +}) => + promisedRequest({ + url: TAG_USER_URL, + method: value ? 'PUT' : 'DELETE', + credentials, + payload: { + nicknames, + tags, + }, + }) + +export const setUsersRight = ({ + right, + credentials, + value, + screen_names: nicknames, +}) => + promisedRequest({ + url: PERMISSION_GROUP_URL(right), + method: value ? 'POST' : 'DELETE', + credentials, + payload: { + nicknames, + }, + }) + +export const setUsersActivationStatus = ({ + credentials, + screen_names: nicknames, + value, +}) => + promisedRequest({ + url: value ? ACTIVATE_USERS_URL : DEACTIVATE_USERS_URL, + method: 'PATCH', + credentials, + payload: { + nicknames, + }, + }).then((response) => response.users) + +export const setUsersApprovalStatus = ({ + credentials, + screen_names: nicknames, +}) => + promisedRequest({ + url: APPROVE_USERS_URL, + method: 'PATCH', + credentials, + payload: { + nicknames, + }, + }).then((response) => response.users) + +export const setUsersConfirmationStatus = ({ + credentials, + screen_names: nicknames, +}) => + promisedRequest({ + url: CONFIRM_USERS_URL, + method: 'PATCH', + credentials, + payload: { + nicknames, + }, + }).then((response) => response.users) + +export const setUsersSuggestionStatus = ({ + credentials, + screen_names: nicknames, + value, +}) => + promisedRequest({ + url: value ? SUGGEST_USERS_URL : UNSUGGEST_USERS_URL, + method: 'PATCH', + credentials, + payload: { + nicknames, + }, + }).then((response) => response.users) + +export const getUserData = ({ credentials, screen_name: nickname }) => + promisedRequest({ + url: USERS_URL(nickname), + method: 'GET', + credentials, + }) + +export const deleteAccounts = ({ credentials, screen_names: nicknames }) => + promisedRequest({ + url: USERS_URL(), + method: 'DELETE', + credentials, + payload: { + nicknames, + }, + }) + +export const getAnnouncements = ({ id, credentials }) => + promisedRequest({ url: ANNOUNCEMENTS_URL(id), credentials }) + +// the reported list is hardly useful because standards are for dating i guess, +// so make sure to fetchIfMissing right afterward using this call +export const listUsers = ({ opts, credentials }) => + promisedRequest({ + url: USERS_URL_LIST(opts), + credentials, + method: 'GET', + }) + +export const resendConfirmationEmail = ({ + screen_names: nicknames, + credentials, +}) => + promisedRequest({ + url: RESEND_CONFIRMATION_EMAIL_URL, + credentials, + method: 'PATCH', + payload: { + nicknames, + }, + }) + +export const requirePasswordChange = ({ + screen_names: nicknames, + credentials, +}) => + promisedRequest({ + url: REQUIRE_PASSWORD_CHANGE_URL, + credentials, + method: 'PATCH', + payload: { + nicknames, + }, + }) + +export const disableMFA = ({ screen_name: nickname, credentials }) => + promisedRequest({ + url: DISABLE_MFA_URL, + credentials, + method: 'PUT', + payload: { + nickname, + }, + }) + +export const listStatuses = ({ opts, credentials }) => + promisedRequest({ + url: LIST_STATUSES_URL(opts), + credentials, + method: 'GET', + }) + +export const changeStatusScope = ({ + opts: { id, sensitive, visibility }, + credentials, +}) => { + var payload = {} + if (typeof sensitive !== 'undefined') { + payload['sensitive'] = sensitive + } + if (typeof visibility !== 'undefined') { + payload['visibility'] = visibility + } + + return promisedRequest({ + url: CHANGE_STATUS_SCOPE_URL(id), + credentials, + method: 'PUT', + payload, + }) +} + +export const announcementToPayload = ({ + content, + startsAt, + endsAt, + allDay, +}) => { + const payload = { content } + + if (typeof startsAt !== 'undefined') { + payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null + } + + if (typeof endsAt !== 'undefined') { + payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null + } + + if (typeof allDay !== 'undefined') { + payload.all_day = allDay + } + + return payload +} + +export const postAnnouncement = ({ + credentials, + content, + startsAt, + endsAt, + allDay, +}) => + promisedRequest({ + url: ANNOUNCEMENTS_URL(), + credentials, + method: 'POST', + payload: announcementToPayload({ content, startsAt, endsAt, allDay }), + }) + +export const editAnnouncement = ({ + id, + credentials, + content, + startsAt, + endsAt, + allDay, +}) => + promisedRequest({ + url: ANNOUNCEMENTS_URL(id), + credentials, + method: 'PATCH', + payload: announcementToPayload({ content, startsAt, endsAt, allDay }), + }) + +export const deleteAnnouncement = ({ id, credentials }) => + promisedRequest({ + url: ANNOUNCEMENTS_URL(id), + credentials, + method: 'DELETE', + }) + +export const setReportState = ({ id, state, credentials }) => { + return promisedRequest({ + url: REPORTS, + credentials, + method: 'PATCH', + payload: { + reports: [ + { + id, + state, + }, + ], + }, + }) +} + +export const getInstanceDBConfig = ({ credentials }) => + promisedRequest({ + url: CONFIG_URL, + credentials, + }) + +export const getInstanceConfigDescriptions = ({ credentials }) => + promisedRequest({ + url: DESCRIPTIONS_URL, + credentials, + }) + +export const getAvailableFrontends = ({ credentials }) => + promisedRequest({ + url: FRONTENDS_URL, + credentials, + }) + +export const pushInstanceDBConfig = ({ credentials, payload }) => + promisedRequest({ + url: CONFIG_URL, + method: 'POST', + credentials, + payload, + }) + +export const installFrontend = ({ credentials, payload }) => + promisedRequest({ + url: FRONTENDS_INSTALL_URL, + credentials, + method: 'POST', + payload, + }) + +// Emoji packs +export const deleteEmojiPack = ({ name }) => + promisedRequest({ + url: EMOJI_PACK_URL(name), + method: 'DELETE', + }) + +export const reloadEmoji = ({ credentials }) => + promisedRequest({ + url: EMOJI_RELOAD_URL, + method: 'POST', + credentials, + }) + +export const importEmojiFromFS = ({ credentials }) => + promisedRequest({ + url: EMOJI_IMPORT_FS_URL, + credentials, + }) + +export const createEmojiPack = ({ name, credentials }) => + promisedRequest({ + url: EMOJI_PACK_URL(name), + method: 'POST', + credentials, + }) + +export const listRemoteEmojiPacks = ({ + instance, + page, + pageSize, + credentials, +}) => { + if (!instance.startsWith('http')) { + instance = 'https://' + instance + } + + return promisedRequest({ + url: EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize), + credentials, + }) +} + +export const downloadRemoteEmojiPack = ({ + instance, + packName, + as, + credentials, +}) => + promisedRequest({ + url: EMOJI_PACKS_DL_REMOTE_URL, + credentials, + method: 'POST', + payload: { + url: instance, + name: packName, + as, + }, + }) + +export const downloadRemoteEmojiPackZIP = ({ + url, + packName, + file, + credentials, +}) => { + const data = new FormData() + if (file) data.set('file', file) + if (url) data.set('url', url) + data.set('name', packName) + + return promisedRequest({ + url: EMOJI_PACKS_DL_REMOTE_ZIP_URL, + method: 'POST', + formData: data, + }) +} + +export const saveEmojiPackMetadata = ({ name, newData, credentials }) => + promisedRequest({ + url: EMOJI_PACK_URL(name), + credentials, + method: 'PATCH', + payload: { metadata: newData }, + }) + +export const addNewEmojiFile = ({ packName, file, shortcode, filename }) => { + const data = new FormData() + if (filename.trim() !== '') { + data.set('filename', filename) + } + if (shortcode.trim() !== '') { + data.set('shortcode', shortcode) + } + data.set('file', file) + + return promisedRequest({ + url: EMOJI_UPDATE_FILE_URL(packName), + method: 'POST', + formData: data, + }) +} + +export const updateEmojiFile = ({ + packName, + shortcode, + newShortcode, + newFilename, + credentials, + force, +}) => + promisedRequest({ + url: EMOJI_UPDATE_FILE_URL(packName), + credentials, + method: 'PATCH', + payload: { + shortcode, + new_shortcode: newShortcode, + new_filename: newFilename, + force, + }, + }) + +export const deleteEmojiFile = ({ packName, shortcode }) => + promisedRequest({ + url: `${EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, + method: 'DELETE', + }) diff --git a/src/api/chats.js b/src/api/chats.js new file mode 100644 index 000000000..51e83c74f --- /dev/null +++ b/src/api/chats.js @@ -0,0 +1,87 @@ +import { paramsString, promisedRequest } from './helpers.js' + +import { parseChat } from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats' +const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}` +const PLEROMA_CHAT_MESSAGES_URL = (id, { maxId, sinceId, limit }) => + `/api/v1/pleroma/chats/${id}/messages${paramsString({ maxId, sinceId, limit })}` +const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read` +const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => + `/api/v1/pleroma/chats/${chatId}/messages/${messageId}` + +export const chats = ({ credentials }) => + promisedRequest({ + url: PLEROMA_CHATS_URL, + credentials, + }).then(({ data }) => ({ + chatList: data.map(parseChat).filter((c) => c), + })) + +export const getOrCreateChat = ({ accountId, credentials }) => + promisedRequest({ + url: PLEROMA_CHAT_URL(accountId), + method: 'POST', + credentials, + }) + +export const chatMessages = ({ + id, + credentials, + maxId, + sinceId, + limit = 20, +}) => { + return promisedRequest({ + url: PLEROMA_CHAT_MESSAGES_URL(id, { maxId, sinceId, limit }), + method: 'GET', + credentials, + }) +} + +export const sendChatMessage = ({ + id, + content, + mediaId = null, + idempotencyKey, + credentials, +}) => { + const payload = { + content, + } + + if (mediaId) { + payload.media_id = mediaId + } + + const headers = {} + + if (idempotencyKey) { + headers['idempotency-key'] = idempotencyKey + } + + return promisedRequest({ + url: PLEROMA_CHAT_MESSAGES_URL(id), + method: 'POST', + payload, + credentials, + headers, + }) +} + +export const readChat = ({ id, lastReadId, credentials }) => + promisedRequest({ + url: PLEROMA_CHAT_READ_URL(id), + method: 'POST', + payload: { + last_read_id: lastReadId, + }, + credentials, + }) + +export const deleteChatMessage = ({ chatId, messageId, credentials }) => + promisedRequest({ + url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId), + method: 'DELETE', + credentials, + }) diff --git a/src/api/helpers.js b/src/api/helpers.js new file mode 100644 index 000000000..f965750c5 --- /dev/null +++ b/src/api/helpers.js @@ -0,0 +1,140 @@ +import { snakeCase } from 'lodash' + +import { StatusCodeError } from 'src/services/errors/errors' + +export const paramsString = (params = {}) => { + if (params == null || params === undefined) return '' + + if (typeof params !== 'object' || Array.isArray(params)) { + throw new Error('Params are not an object!') + } + + const entries = (() => { + if (params instanceof Map) { + return params.entries() + } else { + return Object.entries(params) + } + })() + + if (entries.length === 0) return '' + + const arrays = [] + const nonArrays = [] + + entries.forEach(([k, v]) => { + if (v == null) return // Drop nulls + if ( + (typeof v === 'object' && !Array.isArray(v)) || + typeof v === 'function' + ) { + throw new Error('Param cannot be non-primitive!') + } + if (Array.isArray(v)) { + arrays.push([k, v]) + } else { + nonArrays.push([k, v]) + } + }) + + arrays.forEach(([k, array]) => { + array.forEach((v) => { + if ( + typeof v === 'object' || + typeof v === 'function' || + typeof v === 'undefined' + ) + throw new Error('Array param cannot contain non-primitives!') + }) + }) + + return ( + '?' + + [ + ...nonArrays.map(([k, v]) => [snakeCase(k), v]), + // turning [a,[1,2,3]] into [[a[],1],[a[],2],[a[],3]] + ...arrays.reduce( + (acc, [k, arrayValue]) => [ + ...acc, + ...arrayValue.map((v) => [snakeCase(k) + '[]', v]), + ], + [], + ), + ] + .map(([k, v]) => `${k}=${window.encodeURIComponent(v)}`) + .join('&') + ) +} + +export const promisedRequest = async ({ + method, + url, + payload, + formData, + cache, + credentials, + headers = {}, +}) => { + const options = { + method, + credentials: 'same-origin', + headers: { + Accept: 'application/json', + ...headers, + }, + } + + if (!formData) { + options.headers['Content-Type'] = 'application/json' + } + + if (cache) { + options.cache = cache + } + + if (formData || payload) { + options.body = formData || JSON.stringify(payload) + } + + if (credentials) { + options.headers = { + ...options.headers, + ...authHeaders(credentials), + } + } + + const response = await fetch(url, options) + const data = await (async () => { + const [contentType] = response.headers + .get('content-type') + .split(';') + .map((x) => x.toLowerCase().trim()) + const contentLength = parseInt(response.headers.get('content-length')) + if (contentLength === 0) return null + + switch (contentType) { + case 'text/plain': + return await response.text() + case 'application/json': + return await response.json() + default: + return await response.bytes() + } + })() + + const { ok, status } = response + + if (ok) { + return { response, status, data } + } else { + throw new StatusCodeError(response.status, data, { url, options }, response) + } +} + +const authHeaders = (accessToken) => { + if (accessToken) { + return { Authorization: `Bearer ${accessToken}` } + } else { + return {} + } +} diff --git a/src/api/mfa.js b/src/api/mfa.js new file mode 100644 index 000000000..8c1677fbe --- /dev/null +++ b/src/api/mfa.js @@ -0,0 +1,45 @@ +import { promisedRequest } from './helpers.js' + +export const verifyOTPCode = ({ + clientId, + clientSecret, + instance, + mfaToken, + code, +}) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'totp') + + return promisedRequest({ + url: '/oauth/mfa/challenge', + method: 'POST', + formData, + }) +} + +export const verifyRecoveryCode = ({ + clientId, + clientSecret, + instance, + mfaToken, + code, +}) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'recovery') + + return promisedRequest({ + url: `${instance}/oauth/mfa/challenge`, + method: 'POST', + formData, + }) +} diff --git a/src/api/oauth.js b/src/api/oauth.js new file mode 100644 index 000000000..f0fd2950d --- /dev/null +++ b/src/api/oauth.js @@ -0,0 +1,146 @@ +import { paramsString, promisedRequest } from './helpers.js' + +const REDIRECT_URI = `${window.location.origin}/oauth-callback` + +export const MASTODON_APP_VERIFY_URL = '/api/v1/apps/verify_credentials' +export const MASTODON_APP_URL = '/api/v1/apps' +export const OAUTH_TOKEN_URL = '/oauth/token' +export const OAUTH_MFA_CHALLENGE_URL = '/oauth/mfa/challenge' +export const OAUTH_REVOKE_URL = '/oauth/revoke' + +export const createApp = () => { + const formData = new window.FormData() + + formData.append('client_name', 'PleromaFE') + formData.append('website', 'https://pleroma.social') + formData.append('redirect_uris', REDIRECT_URI) + formData.append('scopes', 'read write follow push admin') + + return promisedRequest({ + method: 'POST', + url: MASTODON_APP_URL, + formData, + }).then(({ data, ...rest }) => ({ + ...rest, + data: { + ...data, + clientId: data.client_id, + clientSecret: data.client_secret, + }, + })) +} + +export const verifyAppToken = ({ credentials }) => + promisedRequest({ + url: MASTODON_APP_VERIFY_URL, + credentials, + }) + +export const getLoginUrl = ({ instance, clientId }) => { + const data = { + responseType: 'code', + clientId, + redirectUri: REDIRECT_URI, + scope: 'read write follow push admin', + } + + return `${instance}/oauth/authorize${paramsString(data)}` +} + +export const getTokenWithCredentials = ({ + clientId, + clientSecret, + username, + password, +}) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'password') + formData.append('username', username) + formData.append('password', password) + + return promisedRequest({ + url: OAUTH_TOKEN_URL, + method: 'POST', + formData, + }) +} + +export const getToken = ({ clientId, clientSecret, code }) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'authorization_code') + formData.append('code', code) + formData.append('redirect_uri', `${window.location.origin}/oauth-callback`) + + return promisedRequest({ + url: OAUTH_TOKEN_URL, + method: 'POST', + formData, + }) +} + +export const getClientToken = ({ clientId, clientSecret }) => { + const formData = new window.FormData() + + formData.append('client_id', clientId) + formData.append('client_secret', clientSecret) + formData.append('grant_type', 'client_credentials') + formData.append('redirect_uri', `${window.location.origin}/oauth-callback`) + + return promisedRequest({ + url: OAUTH_TOKEN_URL, + method: 'POST', + formData, + }) +} + +export const verifyOTPCode = ({ app, mfaToken, code }) => { + const formData = new window.FormData() + + formData.append('client_id', app.client_id) + formData.append('client_secret', app.client_secret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'totp') + + return promisedRequest({ + url: OAUTH_MFA_CHALLENGE_URL, + method: 'POST', + formData, + }) +} + +export const verifyRecoveryCode = ({ app, mfaToken, code }) => { + const formData = new window.FormData() + + formData.append('client_id', app.client_id) + formData.append('client_secret', app.client_secret) + formData.append('mfa_token', mfaToken) + formData.append('code', code) + formData.append('challenge_type', 'recovery') + + return promisedRequest({ + url: OAUTH_MFA_CHALLENGE_URL, + method: 'POST', + formData, + }) +} + +export const revokeToken = ({ app, token }) => { + const formData = new window.FormData() + + formData.append('client_id', app.clientId) + formData.append('client_secret', app.clientSecret) + formData.append('token', token) + + return promisedRequest({ + url: OAUTH_REVOKE_URL, + method: 'POST', + formData, + }) +} diff --git a/src/api/public.js b/src/api/public.js new file mode 100644 index 000000000..e001ba749 --- /dev/null +++ b/src/api/public.js @@ -0,0 +1,279 @@ +import { paramsString, promisedRequest } from './helpers.js' +import { MASTODON_USER_TIMELINE_URL } from './timelines.js' + +import { + parseSource, + parseStatus, + parseUser, +} from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const MASTODON_SUGGESTIONS_URL = '/api/v1/suggestions' +const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' +const MASTODON_REGISTRATION_URL = '/api/v1/accounts' +const MASTODON_PASSWORD_RESET_URL = ({ email }) => + `/auth/password${paramsString({ email })}` + +const MASTODON_FOLLOWING_URL = ( + id, + { minId, maxId, sinceId, limit, withRelationships }, +) => + `/api/v1/accounts/${id}/following${paramsString({ minId, maxId, sinceId, limit, withRelationships })}` +const MASTODON_FOLLOWERS_URL = ( + id, + { minId, maxId, sinceId, limit, withRelationships }, +) => + `/api/v1/accounts/${id}/followers${paramsString({ minId, maxId, sinceId, limit, withRelationships })}` + +export const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}` +const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context` +const MASTODON_STATUS_SOURCE_URL = (id) => `/api/v1/statuses/${id}/source` +const MASTODON_STATUS_HISTORY_URL = (id) => `/api/v1/statuses/${id}/history` +const MASTODON_USER_URL = '/api/v1/accounts' +const MASTODON_USER_LOOKUP_URL = ({ acct }) => + `/api/v1/accounts/lookup${paramsString({ acct })}` +const MASTODON_POLL_URL = (id = '') => `/api/v1/polls/${id}` +const MASTODON_STATUS_FAVORITEDBY_URL = (id) => + `/api/v1/statuses/${id}/favourited_by` +const MASTODON_STATUS_REBLOGGEDBY_URL = (id) => + `/api/v1/statuses/${id}/reblogged_by` +const MASTODON_SEARCH_2 = ({ + q, + resolve, + limit, + offset, + following, + type, + withRelationships, + accountId, + excludeUnreviewed, +}) => + `/api/v2/search${paramsString({ q, resolve, limit, offset, following, type, withRelationships, accountId, excludeUnreviewed })}` +const MASTODON_USER_SEARCH_URL = ({ q, resolve }) => + `/api/v1/accounts/search${paramsString({ q, resolve })}` +const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' +const PLEROMA_EMOJI_REACTIONS_URL = (id) => + `/api/v1/pleroma/statuses/${id}/reactions` +const PLEROMA_SCROBBLES_URL = (id, { maxId, sinceId, minId, limit, offset }) => + `/api/v1/pleroma/accounts/${id}/scrobbles${paramsString({ maxId, sinceId, minId, limit, offset })}` + +const EMOJI_PACKS_URL = (page, pageSize) => + `/api/v1/pleroma/emoji/packs${paramsString({ page, pageSize })}` + +// Params needed: +// nickname +// email +// fullname +// password +// password_confirm +// +// Optional +// bio +// homepage +// location +// token +// language +export const register = ({ params, credentials }) => { + const { nickname, ...rest } = params + return promisedRequest({ + url: MASTODON_REGISTRATION_URL, + method: 'POST', + credentials, + payload: { + nickname, + locale: 'en_US', + agreement: true, + ...rest, + }, + }) +} + +export const getCaptcha = () => + promisedRequest({ + url: '/api/pleroma/captcha', + }) + +export const fetchUser = ({ id, credentials }) => + promisedRequest({ + url: `${MASTODON_USER_URL}/${id}`, + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) + +export const fetchUserByName = ({ name, credentials }) => + promisedRequest({ + url: MASTODON_USER_LOOKUP_URL({ acct: name }), + credentials, + }) + .then(({ data }) => data.id) + .catch((error) => { + if (error && error.statusCode === 404) { + // Either the backend does not support lookup endpoint, + // or there is no user with such name. Fallback and treat name as id. + return name + } else { + throw error + } + }) + .then((id) => fetchUser({ id, credentials })) + +export const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => + promisedRequest({ + url: MASTODON_FOLLOWING_URL(id, { maxId, sinceId, limit }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const fetchFollowers = ({ + id, + maxId, + sinceId, + limit = 20, + credentials, +}) => + promisedRequest({ + url: MASTODON_FOLLOWERS_URL(id, { + maxId, + sinceId, + limit, + withRelationships: true, + }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const fetchConversation = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_CONTEXT_URL(id), + credentials, + }).then((result) => ({ + ...result, + data: { + ...result.data, + ancestors: result.data.ancestors.map(parseStatus), + descendants: result.data.descendants.map(parseStatus), + }, + })) + +export const fetchStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_URL(id), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const fetchStatusSource = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_SOURCE_URL(id), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseSource(data) })) + +export const fetchStatusHistory = ({ status, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_HISTORY_URL(status.id), + credentials, + }).then(({ data, ...rest }) => { + return [...data].reverse().map((item) => { + item.originalStatus = status + return { ...rest, data: parseStatus(item) } + }) + }) + +export const listEmojiPacks = ({ page, pageSize, credentials }) => + promisedRequest({ + url: EMOJI_PACKS_URL(page, pageSize), + }) + +export const fetchPinnedStatuses = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_USER_TIMELINE_URL(id, { pinned: true }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseStatus) })) + +export const verifyCredentials = ({ credentials }) => + promisedRequest({ + url: MASTODON_LOGIN_URL, + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) + +export const resetPassword = ({ email }) => { + return promisedRequest({ + url: MASTODON_PASSWORD_RESET_URL({ email }), + method: 'POST', + }) +} + +export const suggestions = ({ credentials }) => + promisedRequest({ + url: MASTODON_SUGGESTIONS_URL, + credentials, + }) + +export const fetchPoll = ({ pollId, credentials }) => + promisedRequest({ + url: MASTODON_POLL_URL(encodeURIComponent(pollId)), + method: 'GET', + credentials, + }) + +export const fetchFavoritedByUsers = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_FAVORITEDBY_URL(id), + method: 'GET', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const fetchRebloggedByUsers = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_STATUS_REBLOGGEDBY_URL(id), + method: 'GET', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const fetchEmojiReactions = ({ id, credentials }) => + promisedRequest({ + url: PLEROMA_EMOJI_REACTIONS_URL(id), + credentials, + }).then(({ data, ...rest }) => ({ + ...rest, + data: data.map((r) => { + r.accounts = r.accounts.map(parseUser) + return r + }), + })) + +export const searchUsers = ({ credentials, query }) => + promisedRequest({ + url: MASTODON_USER_SEARCH_URL({ q: query, resolve: true }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const search2 = ({ + credentials, + q, + resolve, + limit, + offset, + following, + type, +}) => { + return promisedRequest({ + url: MASTODON_SEARCH_2({ + q, + resolve, + limit, + offset, + following, + type, + withRelationships: true, + }), + credentials, + }).then(({ data, ...rest }) => { + data.accounts = data.accounts.slice(0, limit).map((u) => parseUser(u)) + data.statuses = data.statuses.slice(0, limit).map((s) => parseStatus(s)) + return { ...rest, data } + }) +} + +export const fetchKnownDomains = ({ credentials }) => + promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) + +export const fetchScrobbles = ({ accountId, limit = 1 }) => + promisedRequest({ + url: PLEROMA_SCROBBLES_URL(accountId, { limit }), + }) diff --git a/src/api/timelines.js b/src/api/timelines.js new file mode 100644 index 000000000..0679b1eec --- /dev/null +++ b/src/api/timelines.js @@ -0,0 +1,198 @@ +import { paramsString, promisedRequest } from './helpers.js' + +import { + parseLinkHeaderPagination, + parseNotification, + parseStatus, +} from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const MASTODON_USER_HOME_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, +}) => + `/api/v1/timelines/home${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const MASTODON_LIST_TIMELINE_URL = ( + id, + { minId, sinceId, maxId, limit, replyVisibility }, +) => + `/api/v1/timelines/list/${id}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, +}) => + `/api/v1/timelines/direct${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const MASTODON_PUBLIC_TIMELINE = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, + local, + remote, + onlyMedia, +}) => + `/api/v1/timelines/public${paramsString({ minId, sinceId, maxId, limit, replyVisibility, local, remote, onlyMedia })}` +const MASTODON_TAG_TIMELINE_URL = ( + tag, + { minId, sinceId, maxId, limit, replyVisibility }, +) => + `/api/v1/timelines/tag/${tag}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +export const MASTODON_USER_TIMELINE_URL = ( + id, + { minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia }, +) => + `/api/v1/accounts/${id}/statuses${paramsString({ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia })}` +const MASTODON_USER_FAVORITES_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, +}) => + `/api/v1/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const MASTODON_BOOKMARK_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, + folderId, +}) => + `/api/v1/bookmarks${paramsString({ minId, sinceId, maxId, limit, replyVisibility, folderId })}` +const PLEROMA_STATUS_QUOTES_URL = ( + id, + { minId, sinceId, maxId, limit, replyVisibility }, +) => + `/api/v1/pleroma/statuses/${id}/quotes${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const PLEROMA_USER_FAVORITES_TIMELINE_URL = ( + id, + { minId, sinceId, maxId, limit, replyVisibility }, +) => + `/api/v1/pleroma/accounts/${id}/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` +const AKKOMA_BUBBLE_TIMELINE_URL = ({ + minId, + sinceId, + maxId, + limit, + replyVisibility, +}) => + `/api/v1/timelines/bubble${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` + +const MASTODON_USER_NOTIFICATIONS_URL = ({ + minId, + sinceId, + maxId, + limit, + includeTypes, + replyVisibility, +}) => + `/api/v1/notifications${paramsString({ minId, sinceId, maxId, limit, includeTypes, replyVisibility })}` + +export const fetchTimeline = ({ + timeline, + credentials, + sinceId, + minId, + maxId, + userId, + listId, + statusId, + tag, + withMuted, + replyVisibility = 'all', + includeTypes = [], + bookmarkFolderId, +}) => { + const timelineUrls = { + friends: MASTODON_USER_HOME_TIMELINE_URL, + public: MASTODON_PUBLIC_TIMELINE, + publicAndExternal: MASTODON_PUBLIC_TIMELINE, + dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, + user: MASTODON_USER_TIMELINE_URL, + media: MASTODON_USER_TIMELINE_URL, + list: MASTODON_LIST_TIMELINE_URL, + favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, + publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL, + bookmarks: MASTODON_BOOKMARK_TIMELINE_URL, + bubble: AKKOMA_BUBBLE_TIMELINE_URL, + tag: MASTODON_TAG_TIMELINE_URL, + quotes: PLEROMA_STATUS_QUOTES_URL, + + notifications: MASTODON_USER_NOTIFICATIONS_URL, + } + const urlFunc = timelineUrls[timeline] + + const twoArgs = new Set([ + 'user', + 'media', + 'list', + 'publicFavorites', + 'tag', + 'quotes', + ]) + + const params = { + minId, + sinceId, + maxId, + limit: 20, + } + + const id = (() => { + switch (timeline) { + case 'user': + case 'media': + return userId + case 'list': + return listId + case 'quotes': + return statusId + case 'tag': + return tag + } + })() + + const isNotifications = timeline === 'notifications' + + if (timeline === 'media') { + params.onlyMedia = true + } + if (timeline === 'public') { + params.local = true + } + if (timeline !== 'favorites' && timeline !== 'bookmarks') { + params.withMuted = withMuted + } + if (replyVisibility !== 'all') { + params.replyVisibility = replyVisibility + } + if (timeline === 'bookmarks' && bookmarkFolderId) { + params.folderId = bookmarkFolderId + } + + if (isNotifications && includeTypes.length > 0) { + params.includeTypes = includeTypes + } + + const url = twoArgs.has(timeline) ? urlFunc(id, params) : urlFunc(params) + return promisedRequest({ url, credentials }).then((result) => { + const pagination = parseLinkHeaderPagination( + result.response.headers.get('Link'), + { + flakeId: timeline !== 'bookmarks' && timeline !== 'notifications', + }, + ) + + return { + ...result, + data: result.data.map(isNotifications ? parseNotification : parseStatus), + pagination, + } + }) +} diff --git a/src/api/user.js b/src/api/user.js new file mode 100644 index 000000000..17e13195d --- /dev/null +++ b/src/api/user.js @@ -0,0 +1,921 @@ +import { concat, last } from 'lodash' + +import { paramsString, promisedRequest } from './helpers.js' +import { fetchFriends, MASTODON_STATUS_URL } from './public.js' + +import { + parseAttachment, + parseStatus, + parseUser, +} from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const MUTES_IMPORT_URL = '/api/pleroma/mutes_import' +const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' +const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' +const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' +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_READ_URL = '/api/v1/pleroma/notifications/read' + +const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' +const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' + +const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp' +const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp' +const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp' + +const MASTODON_DISMISS_NOTIFICATION_URL = (id) => + `/api/v1/notifications/${id}/dismiss` +const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite` +const MASTODON_UNFAVORITE_URL = (id) => `/api/v1/statuses/${id}/unfavourite` +const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog` +const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog` +const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}` +const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow` +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) => + `/api/v1/follow_requests/${id}/authorize` +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 })}` +const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists` +export const MASTODON_LIST_URL = (id = '') => `/api/v1/lists/${id}` +export const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts` +const MASTODON_USER_BLOCKS_URL = ({ + maxId, + sinceId, + limit, + withRelationships, +}) => + `/api/v1/blocks/${paramsString({ maxId, sinceId, limit, withRelationships })}` +const MASTODON_USER_MUTES_URL = ({ + maxId, + sinceId, + limit, + withRelationships, +}) => + `/api/v1/mutes/${paramsString({ maxId, sinceId, limit, withRelationships })}` +const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block` +const MASTODON_UNBLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/unblock` +const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute` +const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute` +const MASTODON_REMOVE_USER_FROM_FOLLOWERS = (id) => + `/api/v1/accounts/${id}/remove_from_followers` +const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note` +const MASTODON_BOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark` +const MASTODON_UNBOOKMARK_STATUS_URL = (id) => + `/api/v1/statuses/${id}/unbookmark` +const MASTODON_POST_STATUS_URL = '/api/v1/statuses' +const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' +const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes` +const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' +const MASTODON_REPORT_USER_URL = '/api/v1/reports' +const MASTODON_PIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/pin` +const MASTODON_UNPIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/unpin` +const MASTODON_MUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/mute` +const MASTODON_UNMUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/unmute` +const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' +const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements' +const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) => + `/api/v1/announcements/${id}/dismiss` +const PLEROMA_EMOJI_REACT_URL = (id, emoji) => + `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` +const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => + `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` +const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups' +const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders' +const PLEROMA_BOOKMARK_FOLDER_URL = (id) => + `/api/v1/pleroma/bookmark_folders/${id}` + +// #Posts +export const favorite = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_FAVORITE_URL(id), + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unfavorite = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNFAVORITE_URL(id), + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const retweet = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_RETWEET_URL(id), + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unretweet = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNRETWEET_URL(id), + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const reactWithEmoji = ({ id, emoji, credentials }) => + promisedRequest({ + url: PLEROMA_EMOJI_REACT_URL(id, emoji), + method: 'PUT', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unreactWithEmoji = ({ id, emoji, credentials }) => + promisedRequest({ + url: PLEROMA_EMOJI_UNREACT_URL(id, emoji), + method: 'DELETE', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const bookmarkStatus = ({ id, credentials, ...options }) => + promisedRequest({ + url: MASTODON_BOOKMARK_STATUS_URL(id), + credentials, + method: 'POST', + payload: { + folder_id: options.folder_id, + }, + }) + +export const unbookmarkStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNBOOKMARK_STATUS_URL(id), + credentials, + method: 'POST', + }) + +export const pinOwnStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_PIN_OWN_STATUS(id), + credentials, + method: 'POST', + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unpinOwnStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNPIN_OWN_STATUS(id), + credentials, + method: 'POST', + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const muteConversation = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_MUTE_CONVERSATION(id), + credentials, + method: 'POST', + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const unmuteConversation = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNMUTE_CONVERSATION(id), + credentials, + method: 'POST', + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) + +export const vote = ({ pollId, choices, credentials }) => { + return promisedRequest({ + url: MASTODON_VOTE_URL(encodeURIComponent(pollId)), + method: 'POST', + credentials, + payload: { + choices, + }, + }) +} + +// #Posting +export const postStatus = ({ + credentials, + status, + spoilerText, + visibility, + sensitive, + poll, + mediaIds = [], + inReplyToStatusId, + quoteId, + contentType, + preview, + idempotencyKey, +}) => { + const form = new FormData() + const pollOptions = poll.options || [] + + form.append('status', status) + form.append('source', 'Pleroma FE') + if (spoilerText) form.append('spoiler_text', spoilerText) + if (visibility) form.append('visibility', visibility) + if (sensitive) form.append('sensitive', sensitive) + if (contentType) form.append('content_type', contentType) + mediaIds.forEach((val) => { + form.append('media_ids[]', val) + }) + if (pollOptions.some((option) => option !== '')) { + const normalizedPoll = { + expires_in: parseInt(poll.expiresIn, 10), + multiple: poll.multiple, + } + Object.keys(normalizedPoll).forEach((key) => { + form.append(`poll[${key}]`, normalizedPoll[key]) + }) + + pollOptions.forEach((option) => { + form.append('poll[options][]', option) + }) + } + if (inReplyToStatusId) { + form.append('in_reply_to_id', inReplyToStatusId) + } + if (quoteId) { + form.append('quote_id', quoteId) + } + if (preview) { + form.append('preview', 'true') + } + + const headers = {} + if (idempotencyKey) { + headers['idempotency-key'] = idempotencyKey + } + + return promisedRequest({ + url: MASTODON_POST_STATUS_URL, + formData: form, + method: 'POST', + credentials, + headers, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) +} + +export const editStatus = ({ + id, + credentials, + status, + spoilerText, + sensitive, + poll, + mediaIds = [], + contentType, +}) => { + const form = new FormData() + const pollOptions = poll.options || [] + + form.append('status', status) + if (spoilerText) form.append('spoiler_text', spoilerText) + if (sensitive) form.append('sensitive', sensitive) + if (contentType) form.append('content_type', contentType) + mediaIds.forEach((val) => { + form.append('media_ids[]', val) + }) + + if (pollOptions.some((option) => option !== '')) { + const normalizedPoll = { + expires_in: parseInt(poll.expiresIn, 10), + multiple: poll.multiple, + } + Object.keys(normalizedPoll).forEach((key) => { + form.append(`poll[${key}]`, normalizedPoll[key]) + }) + + pollOptions.forEach((option) => { + form.append('poll[options][]', option) + }) + } + + return promisedRequest({ + url: MASTODON_STATUS_URL(id), + formData: form, + method: 'PUT', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) +} + +export const deleteStatus = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_DELETE_URL(id), + credentials, + method: 'DELETE', + }) + +export const uploadMedia = ({ formData, credentials }) => + promisedRequest({ + url: MASTODON_MEDIA_UPLOAD_URL, + formData, + method: 'POST', + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) })) + +export const setMediaDescription = ({ id, description, credentials }) => + promisedRequest({ + url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`, + method: 'PUT', + credentials, + payload: { + description, + }, + }).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) })) + +// #Notifications +export const dismissNotification = ({ credentials, id }) => + promisedRequest({ + url: MASTODON_DISMISS_NOTIFICATION_URL(id), + method: 'POST', + payload: { id }, + credentials, + }) + +export const markNotificationsAsSeen = ({ + id, + credentials, + single = false, +}) => { + const formData = new FormData() + + if (single) { + formData.append('id', id) + } else { + formData.append('max_id', id) + } + + return promisedRequest({ + url: NOTIFICATION_READ_URL, + formData, + credentials, + method: 'POST', + }) +} + +// #Announcements +export const getAnnouncements = ({ credentials }) => + promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials }) + +export const dismissAnnouncement = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id), + credentials, + method: 'POST', + }) + +// #Imports +export const importMutes = ({ file, credentials }) => { + const formData = new FormData() + formData.append('list', file) + return promisedRequest({ + url: MUTES_IMPORT_URL, + formData, + method: 'POST', + credentials, + }).then((response) => response.ok) +} + +export const importBlocks = ({ file, credentials }) => { + const formData = new FormData() + formData.append('list', file) + return promisedRequest({ + url: BLOCKS_IMPORT_URL, + formData, + method: 'POST', + credentials, + }).then((response) => response.ok) +} + +export const importFollows = ({ file, credentials }) => { + const formData = new FormData() + formData.append('list', file) + return promisedRequest({ + url: FOLLOW_IMPORT_URL, + formData, + method: 'POST', + credentials, + }).then((response) => response.ok) +} + +export const exportFriends = ({ id, credentials }) => { + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO refactor this + return new Promise(async (resolve, reject) => { + try { + let friends = [] + let more = true + while (more) { + const maxId = friends.length > 0 ? last(friends).id : undefined + const users = await fetchFriends({ + id, + maxId, + credentials, + withRelationships: true, + }) + friends = concat(friends, users) + if (users.length === 0) { + more = false + } + } + resolve(friends) + } catch (err) { + reject(err) + } + }) +} + +// #Profile settings +export const updateNotificationSettings = ({ credentials, settings }) => { + return promisedRequest({ + url: NOTIFICATION_SETTINGS_URL, + credentials, + method: 'PUT', + payload: settings, + }) +} + +export const updateProfileImages = ({ + credentials, + avatar = null, + avatarName = null, + banner = null, + background = null, +}) => { + const form = new FormData() + if (avatar !== null) { + if (avatarName !== null) { + form.append('avatar', avatar, avatarName) + } else { + form.append('avatar', avatar) + } + } + if (banner !== null) form.append('header', banner) + if (background !== null) form.append('pleroma_background_image', background) + return promisedRequest({ + url: MASTODON_PROFILE_UPDATE_URL, + credentials, + method: 'PATCH', + formData: form, + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) +} + +export const updateProfile = ({ credentials, params }) => { + const formData = new FormData() + + for (const name in params) { + if (name === 'fields_attributes') { + params[name].forEach((param, i) => { + formData.append(name + `[${i}][name]`, param.name) + formData.append(name + `[${i}][value]`, param.value) + }) + } else { + if (typeof params[name] === 'object') { + console.warn( + 'Object detected in updateProfile API call. This will not work, use updateProfileJSON instead.', + ) + console.warn('Object:\n' + JSON.stringify(params[name], null, 2)) + } + formData.append(name, params[name]) + } + } + + return promisedRequest({ + url: MASTODON_PROFILE_UPDATE_URL, + credentials, + method: 'PATCH', + formData, + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) +} + +export const updateProfileJSON = ({ credentials, params }) => + promisedRequest({ + url: MASTODON_PROFILE_UPDATE_URL, + credentials, + payload: params, + method: 'PATCH', + }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) + +export const changeEmail = ({ credentials, email, password }) => { + const form = new FormData() + + form.append('email', email) + form.append('password', password) + + return promisedRequest({ + url: CHANGE_EMAIL_URL, + formData: form, + method: 'POST', + credentials, + }) +} + +export const moveAccount = ({ credentials, password, targetAccount }) => { + const form = new FormData() + + form.append('password', password) + form.append('target_account', targetAccount) + + return promisedRequest({ + url: MOVE_ACCOUNT_URL, + formData: form, + method: 'POST', + credentials, + }) +} + +export const changePassword = ({ + credentials, + password, + newPassword, + newPasswordConfirmation, +}) => { + const form = new FormData() + + form.append('password', password) + form.append('new_password', newPassword) + form.append('new_password_confirmation', newPasswordConfirmation) + + return promisedRequest({ + url: CHANGE_PASSWORD_URL, + formData: form, + method: 'POST', + credentials, + }) +} + +// #MFA +export const settingsMFA = ({ credentials }) => + promisedRequest({ + url: MFA_SETTINGS_URL, + credentials, + method: 'GET', + }) + +export const mfaDisableOTP = ({ credentials, password }) => { + const form = new FormData() + + form.append('password', password) + + return promisedRequest({ + url: MFA_DISABLE_OTP_URL, + formData: form, + method: 'DELETE', + credentials, + }) +} + +export const mfaConfirmOTP = ({ credentials, password, token }) => { + const form = new FormData() + + form.append('password', password) + form.append('code', token) + + return promisedRequest({ + url: MFA_CONFIRM_OTP_URL, + formData: form, + credentials, + method: 'POST', + }) +} +export const mfaSetupOTP = ({ credentials }) => + promisedRequest({ + url: MFA_SETUP_OTP_URL, + credentials, + method: 'GET', + }) +export const generateMfaBackupCodes = ({ credentials }) => + promisedRequest({ + url: MFA_BACKUP_CODES_URL, + credentials, + method: 'GET', + }) + +// #Aliases +export const addAlias = ({ credentials, alias }) => + promisedRequest({ + url: ALIASES_URL, + method: 'PUT', + credentials, + payload: { alias }, + }) + +export const deleteAlias = ({ credentials, alias }) => + promisedRequest({ + url: ALIASES_URL, + method: 'DELETE', + credentials, + payload: { alias }, + }) + +export const listAliases = ({ credentials }) => + promisedRequest({ + url: ALIASES_URL, + method: 'GET', + credentials, + params: { + _cacheBooster: new Date().getTime(), + }, + }) + +// User manipulation +export const fetchUserRelationship = ({ id, withSuspended, credentials }) => + promisedRequest({ + url: MASTODON_USER_RELATIONSHIPS_URL({ id, withSuspended }), + credentials, + }) + +export const followUser = ({ id, credentials, ...options }) => { + const payload = {} + + if (options.reblogs !== undefined) { + payload.reblogs = options.reblogs + } + + if (options.notify !== undefined) { + payload.notify = options.notify + } + + return promisedRequest({ + url: MASTODON_FOLLOW_URL(id), + payload, + credentials, + method: 'POST', + }) +} + +export const unfollowUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNFOLLOW_URL(id), + credentials, + method: 'POST', + }) +export const fetchUserInLists = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_USER_IN_LISTS(id), + credentials, + }) + +export const removeUserFromFollowers = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), + credentials, + method: 'POST', + }) + +export const fetchFollowRequests = ({ credentials }) => + promisedRequest({ + url: MASTODON_FOLLOW_REQUESTS_URL, + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const approveUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_APPROVE_USER_URL(id), + credentials, + method: 'POST', + }) + +export const denyUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_DENY_USER_URL(id), + credentials, + method: 'POST', + }) + +export const editUserNote = ({ id, credentials, comment }) => + promisedRequest({ + url: MASTODON_USER_NOTE_URL(id), + credentials, + payload: { + comment, + }, + method: 'POST', + }) + +export const fetchMutes = ({ maxId, credentials }) => + promisedRequest({ + url: MASTODON_USER_MUTES_URL({ maxId, withRelationships: true }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const muteUser = ({ id, expiresIn, credentials }) => { + const payload = {} + if (expiresIn) { + payload.expires_in = expiresIn + } + + return promisedRequest({ + url: MASTODON_MUTE_USER_URL(id), + credentials, + method: 'POST', + payload, + }) +} + +export const unmuteUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNMUTE_USER_URL(id), + credentials, + method: 'POST', + }) + +export const fetchBlocks = ({ maxId, credentials }) => + promisedRequest({ + url: MASTODON_USER_BLOCKS_URL({ maxId, withRelationships: true }), + credentials, + }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) + +export const blockUser = ({ id, expiresIn, credentials }) => { + const payload = {} + if (expiresIn) { + payload.duration = expiresIn + } + + return promisedRequest({ + url: MASTODON_BLOCK_USER_URL(id), + credentials, + method: 'POST', + payload, + }) +} + +export const unblockUser = ({ id, credentials }) => + promisedRequest({ + url: MASTODON_UNBLOCK_USER_URL(id), + credentials, + method: 'POST', + }) + +export const reportUser = ({ + credentials, + userId, + statusIds, + comment, + forward, +}) => + promisedRequest({ + url: MASTODON_REPORT_USER_URL, + method: 'POST', + payload: { + account_id: userId, + status_ids: statusIds, + comment, + forward, + }, + credentials, + }) + +// #Domain mutes +export const fetchDomainMutes = ({ credentials }) => + promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials }) + +export const muteDomain = ({ domain, credentials }) => + promisedRequest({ + url: MASTODON_DOMAIN_BLOCKS_URL, + method: 'POST', + payload: { domain }, + credentials, + }) + +export const unmuteDomain = ({ domain, credentials }) => + promisedRequest({ + url: MASTODON_DOMAIN_BLOCKS_URL, + method: 'DELETE', + payload: { domain }, + credentials, + }) + +// #Backups +export const addBackup = ({ credentials }) => + promisedRequest({ + url: PLEROMA_BACKUP_URL, + method: 'POST', + credentials, + }) + +export const listBackups = ({ credentials }) => + promisedRequest({ + url: PLEROMA_BACKUP_URL, + method: 'GET', + credentials, + params: { + _cacheBooster: new Date().getTime(), + }, + }) + +// #OAuth +export const fetchOAuthTokens = ({ credentials }) => + promisedRequest({ + url: '/api/oauth_tokens.json', + credentials, + }) + +export const revokeOAuthToken = ({ id, credentials }) => + promisedRequest({ + url: `/api/oauth_tokens/${id}`, + credentials, + method: 'DELETE', + }) + +// #Lists +export const fetchLists = ({ credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(), + credentials, + }) + +export const createList = ({ title, credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(), + credentials, + method: 'POST', + payload: { title }, + }) + +export const getList = ({ listId, credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(listId), + credentials, + }) + +export const updateList = ({ listId, title, credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(listId), + + credentials, + method: 'PUT', + payload: { title }, + }) + +export const getListAccounts = ({ listId, credentials }) => + promisedRequest({ + url: MASTODON_LIST_ACCOUNTS_URL(listId), + credentials, + }).then((data) => data.map(({ id }) => id)) + +export const addAccountsToList = ({ listId, accountIds, credentials }) => + promisedRequest({ + url: MASTODON_LIST_ACCOUNTS_URL(listId), + credentials, + method: 'POST', + payload: { account_ids: accountIds }, + }) + +export const removeAccountsFromList = ({ listId, accountIds, credentials }) => + promisedRequest({ + url: MASTODON_LIST_ACCOUNTS_URL(listId), + credentials, + method: 'DELETE', + payload: { account_ids: accountIds }, + }) + +export const deleteList = ({ listId, credentials }) => + promisedRequest({ + url: MASTODON_LIST_URL(listId), + method: 'DELETE', + credentials, + }) + +// #Bookmarks +export const fetchBookmarkFolders = ({ credentials }) => + promisedRequest({ + url: PLEROMA_BOOKMARK_FOLDERS_URL, + credentials, + }) + +export const createBookmarkFolder = ({ name, emoji, credentials }) => + promisedRequest({ + url: PLEROMA_BOOKMARK_FOLDERS_URL, + credentials, + method: 'POST', + payload: { name, emoji }, + }) + +export const updateBookmarkFolder = ({ folderId, name, emoji, credentials }) => + promisedRequest({ + url: PLEROMA_BOOKMARK_FOLDER_URL(folderId), + credentials, + method: 'PATCH', + payload: { name, emoji }, + }) + +export const deleteBookmarkFolder = ({ folderId, credentials }) => + promisedRequest({ + url: PLEROMA_BOOKMARK_FOLDER_URL(folderId), + method: 'DELETE', + credentials, + }) + +// #So long and thanks for all the fish +export const deleteAccount = ({ credentials, password }) => { + const formData = new FormData() + + formData.append('password', password) + + return promisedRequest({ + url: DELETE_ACCOUNT_URL, + formData, + method: 'POST', + credentials, + }) +} diff --git a/src/api/websocket.js b/src/api/websocket.js new file mode 100644 index 000000000..d952372e5 --- /dev/null +++ b/src/api/websocket.js @@ -0,0 +1,176 @@ +import { paramsString } from './helpers.js' + +import { + parseChat, + parseNotification, + parseStatus, +} from 'src/services/entity_normalizer/entity_normalizer.service.js' + +const MASTODON_STREAMING = ({ accessToken, stream }) => + `/api/v1/streaming${paramsString({ accessToken, stream })}` + +export const getMastodonSocketURI = ({ credentials, stream }) => { + return MASTODON_STREAMING({ accessToken: credentials, stream }) +} + +const MASTODON_STREAMING_EVENTS = new Set([ + 'update', + 'notification', + 'delete', + 'filters_changed', + 'status.update', +]) + +const PLEROMA_STREAMING_EVENTS = new Set([ + 'pleroma:chat_update', + 'pleroma:respond', +]) + +// A thin wrapper around WebSocket API that allows adding a pre-processor to it +// Uses EventTarget and a CustomEvent to proxy events +export const ProcessedWS = ({ + url, + preprocessor = handleMastoWS, + id = 'Unknown', + credentials, +}) => { + const eventTarget = new EventTarget() + const socket = new WebSocket(url) + if (!socket) throw new Error(`Failed to create socket ${id}`) + const proxy = (original, eventName, processor = (a) => a) => { + original.addEventListener(eventName, (eventData) => { + eventTarget.dispatchEvent( + new CustomEvent(eventName, { detail: processor(eventData) }), + ) + }) + } + socket.addEventListener('open', (wsEvent) => { + console.debug(`[WS][${id}] Socket connected`, wsEvent) + if (credentials) { + socket.send( + JSON.stringify({ + type: 'pleroma:authenticate', + token: credentials, + }), + ) + } + }) + socket.addEventListener('error', (wsEvent) => { + console.debug(`[WS][${id}] Socket errored`, wsEvent) + }) + socket.addEventListener('close', (wsEvent) => { + console.debug( + `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, + wsEvent, + ) + }) + // Commented code reason: very spammy, uncomment to enable message debug logging + /* + socket.addEventListener('message', (wsEvent) => { + console.debug( + `[WS][${id}] Message received`, + wsEvent + ) + }) + /**/ + + const onAuthenticated = () => { + eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated')) + } + + proxy(socket, 'open') + proxy(socket, 'close') + proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated })) + proxy(socket, 'error') + + // 1000 = Normal Closure + eventTarget.close = () => { + socket.close(1000, 'Shutting down socket') + } + eventTarget.getState = () => socket.readyState + eventTarget.subscribe = (stream, args = {}) => { + console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args) + socket.send( + JSON.stringify({ + type: 'subscribe', + stream, + ...args, + }), + ) + } + eventTarget.unsubscribe = (stream, args = {}) => { + console.debug( + `[WS][${id}] Unsubscribing from stream ${stream} with args`, + args, + ) + socket.send( + JSON.stringify({ + type: 'unsubscribe', + stream, + ...args, + }), + ) + } + + return eventTarget +} + +export const handleMastoWS = ( + wsEvent, + { + onAuthenticated = () => { + /* no-op */ + }, + } = {}, +) => { + const { data } = wsEvent + if (!data) return + const parsedEvent = JSON.parse(data) + const { event, payload } = parsedEvent + if ( + MASTODON_STREAMING_EVENTS.has(event) || + PLEROMA_STREAMING_EVENTS.has(event) + ) { + // MastoBE and PleromaBE both send payload for delete as a PLAIN string + if (event === 'delete') { + return { event, id: payload } + } + const data = payload ? JSON.parse(payload) : null + if (event === 'pleroma:respond') { + if (data.type === 'pleroma:authenticate') { + if (data.result === 'success') { + console.debug('[WS] Successfully authenticated') + onAuthenticated() + } else { + if (data.error === 'already_authenticated') { + onAuthenticated() + } else { + console.error('[WS] Unable to authenticate:', data.error) + wsEvent.target.close() + } + } + } + return null + } else if (event === 'update') { + return { event, status: parseStatus(data) } + } else if (event === 'status.update') { + return { event, status: parseStatus(data) } + } else if (event === 'notification') { + return { event, notification: parseNotification(data) } + } else if (event === 'pleroma:chat_update') { + return { event, chatUpdate: parseChat(data) } + } + } else { + console.warn('Unknown event', wsEvent) + return null + } +} + +export const WSConnectionStatus = Object.freeze({ + JOINED: 1, + CLOSED: 2, + ERROR: 3, + DISABLED: 4, + STARTING: 5, + STARTING_INITIAL: 6, +}) diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 678a5e7d1..4ccfa4d41 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -6,6 +6,10 @@ import { createRouter, createWebHistory } from 'vue-router' import VueVirtualScroller from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' +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 { config } from '@fortawesome/fontawesome-svg-core' import { FontAwesomeIcon, @@ -15,7 +19,6 @@ import { config.autoAddCss = false import App from '../App.vue' -import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import FaviconService from '../services/favicon_service/favicon_service.js' import { applyStyleConfig } from '../services/style_setter/style_setter.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js' @@ -25,7 +28,6 @@ import { } from '../services/window_utils/window_utils' import routes from './routes' -import { useAnnouncementsStore } from 'src/stores/announcements' import { useAuthFlowStore } from 'src/stores/auth_flow' import { useEmojiStore } from 'src/stores/emoji.js' import { useI18nStore } from 'src/stores/i18n' @@ -34,7 +36,7 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j import { useInterfaceStore } from 'src/stores/interface.js' import { useLocalConfigStore } from 'src/stores/local_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useOAuthStore } from 'src/stores/oauth' +import { useOAuthStore } from 'src/stores/oauth.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' @@ -152,8 +154,10 @@ const getStaticConfig = async () => { throw res } } catch (error) { - console.warn('Failed to load static/config.json, continuing without it.') - console.warn(error) + console.warn( + 'Failed to load static/config.json, continuing without it.', + error, + ) return {} } } @@ -175,14 +179,16 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { if (source === 'name') return if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return useInstanceStore().set({ - value: config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default, + value: + config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default, path: `instanceIdentity.${source}`, }) }) Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) => useInstanceStore().set({ - value: config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default, + value: + config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default, path: `prefsStorage.${source}`, }), ) @@ -253,16 +259,6 @@ const getStickers = async ({ store }) => { } } -const getAppSecret = async ({ store }) => { - const oauth = useOAuthStore() - if (oauth.userToken) { - store.commit( - 'setBackendInteractor', - backendInteractorService(oauth.getToken), - ) - } -} - const resolveStaffAccounts = ({ store, accounts }) => { const nicknames = accounts.map((uri) => uri.split('/').pop()) useInstanceStore().set({ @@ -440,8 +436,7 @@ const getNodeInfo = async ({ store }) => { throw res } } catch (e) { - console.warn('Could not load nodeinfo') - console.warn(e) + console.warn('Could not load nodeinfo', e) } } @@ -454,14 +449,13 @@ const setConfig = async ({ store }) => { const apiConfig = configInfos[0] const staticConfig = configInfos[1] - getAppSecret({ store }) await setSettings({ store, apiConfig, staticConfig }) } const checkOAuthToken = async ({ store }) => { const oauth = useOAuthStore() - if (oauth.getUserToken) { - return store.dispatch('loginUser', oauth.getUserToken) + if (oauth.userToken) { + return store.dispatch('loginUser', oauth.userToken) } return Promise.resolve() } @@ -473,6 +467,16 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { // "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied." app.use(pinia) + app.config.errorHandler = (error, instance, info) => { + console.error( + 'Global Vue Error Handler caught an error:', + error, + instance, + info, + ) + useInterfaceStore().setGlobalError({ error, instance, info }) + } + const waitForAllStoresToLoad = async () => { // the stores that do not persist technically do not need to be awaited here, // but that involves either hard-coding the stores in some place (prone to errors) @@ -571,10 +575,6 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { getInstanceConfig({ store }), ]).catch((e) => Promise.reject(e)) - // Start fetching things that don't need to block the UI - store.dispatch('fetchMutes') - store.dispatch('loadDrafts') - useAnnouncementsStore().startFetchingAnnouncements() getTOS({ store }) getStickers({ store }) @@ -608,6 +608,9 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { app.component('FAIcon', FontAwesomeIcon) app.component('FALayers', FontAwesomeLayers) + app.component('Status', Status) + app.component('RichContent', RichContent) + app.component('StillImage', StillImage) // remove after vue 3.3 app.config.unwrapInjectedRef = true diff --git a/src/boot/routes.js b/src/boot/routes.js index 193daf4a7..9b00a8004 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -1,36 +1,16 @@ -import About from 'components/about/about.vue' -import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' -import AuthForm from 'components/auth_form/auth_form.js' -import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue' -import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue' -import Chat from 'components/chat/chat.vue' -import ChatList from 'components/chat_list/chat_list.vue' -import ConversationPage from 'components/conversation-page/conversation-page.vue' -import DMs from 'components/dm_timeline/dm_timeline.vue' -import Drafts from 'components/drafts/drafts.vue' -import FollowRequests from 'components/follow_requests/follow_requests.vue' -import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue' -import Interactions from 'components/interactions/interactions.vue' -import Lists from 'components/lists/lists.vue' -import ListsEdit from 'components/lists_edit/lists_edit.vue' -import ListsTimeline from 'components/lists_timeline/lists_timeline.vue' -import Notifications from 'components/notifications/notifications.vue' -import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' -import PasswordReset from 'components/password_reset/password_reset.vue' -import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue' -import PublicTimeline from 'components/public_timeline/public_timeline.vue' -import Registration from 'components/registration/registration.vue' -import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue' -import Search from 'components/search/search.vue' -import ShoutPanel from 'components/shout_panel/shout_panel.vue' -import TagTimeline from 'components/tag_timeline/tag_timeline.vue' -import UserProfile from 'components/user_profile/user_profile.vue' -import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' +import { defineAsyncComponent } from 'vue' +import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue' +import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue' +import ConversationPage from 'src/components/conversation-page/conversation-page.vue' +import DMs from 'src/components/dm_timeline/dm_timeline.vue' +import FriendsTimeline from 'src/components/friends_timeline/friends_timeline.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue' -import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue' -import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue' -import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' +import PublicAndExternalTimeline from 'src/components/public_and_external_timeline/public_and_external_timeline.vue' +import PublicTimeline from 'src/components/public_timeline/public_timeline.vue' +import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue' +import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue' +import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' @@ -100,12 +80,23 @@ export default (store) => { { name: 'external-user-profile', path: '/users/$:id', - component: UserProfile, + component: defineAsyncComponent( + () => import('src/components/user_profile/user_profile.vue'), + ), + }, + { + name: 'user-profile-admin-view', + path: '/users/$:id/admin_view', + component: defineAsyncComponent( + () => import('src/components/user_profile/user_profile_admin_view.vue'), + ), }, { name: 'interactions', path: '/users/:username/interactions', - component: Interactions, + component: defineAsyncComponent( + () => import('src/components/interactions/interactions.vue'), + ), beforeEnter: validateAuthenticatedRoute, }, { @@ -114,69 +105,148 @@ export default (store) => { component: DMs, beforeEnter: validateAuthenticatedRoute, }, - { name: 'registration', path: '/registration', component: Registration }, + { + name: 'registration', + path: '/registration', + component: defineAsyncComponent( + () => import('src/components/registration/registration.vue'), + ), + }, { name: 'password-reset', path: '/password-reset', - component: PasswordReset, + component: defineAsyncComponent( + () => import('src/components/password_reset/password_reset.vue'), + ), props: true, }, { name: 'registration-token', path: '/registration/:token', - component: Registration, + component: defineAsyncComponent( + () => import('src/components/registration/registration.vue'), + ), }, { name: 'friend-requests', path: '/friend-requests', - component: FollowRequests, + component: defineAsyncComponent( + () => import('src/components/follow_requests/follow_requests.vue'), + ), beforeEnter: validateAuthenticatedRoute, }, { name: 'notifications', path: '/:username/notifications', - component: Notifications, + component: defineAsyncComponent( + () => import('src/components/notifications/notifications.vue'), + ), props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute, }, - { name: 'login', path: '/login', component: AuthForm }, + { + name: 'login', + path: '/login', + component: defineAsyncComponent( + () => import('src/components/auth_form/auth_form.js'), + ), + }, { name: 'shout-panel', path: '/shout-panel', - component: ShoutPanel, + component: defineAsyncComponent( + () => import('src/components/shout_panel/shout_panel.vue'), + ), props: () => ({ floating: false }), }, { name: 'oauth-callback', path: '/oauth-callback', - component: OAuthCallback, + component: defineAsyncComponent( + () => import('src/components/oauth_callback/oauth_callback.vue'), + ), props: (route) => ({ code: route.query.code }), }, { name: 'search', path: '/search', - component: Search, + component: defineAsyncComponent( + () => import('src/components/search/search.vue'), + ), props: (route) => ({ query: route.query.query }), }, { name: 'who-to-follow', path: '/who-to-follow', - component: WhoToFollow, + component: defineAsyncComponent( + () => import('src/components/who_to_follow/who_to_follow.vue'), + ), beforeEnter: validateAuthenticatedRoute, }, - { name: 'about', path: '/about', component: About }, + { + name: 'about', + path: '/about', + component: defineAsyncComponent( + () => import('src/components/about/about.vue'), + ), + }, { name: 'announcements', path: '/announcements', - component: AnnouncementsPage, + component: defineAsyncComponent( + () => + import('src/components/announcements_page/announcements_page.vue'), + ), + }, + { + name: 'drafts', + path: '/drafts', + component: defineAsyncComponent( + () => import('src/components/drafts/drafts.vue'), + ), + }, + { + name: 'user-profile', + path: '/users/:name', + component: defineAsyncComponent( + () => import('src/components/user_profile/user_profile.vue'), + ), + }, + { + name: 'legacy-user-profile', + path: '/:name', + component: defineAsyncComponent( + () => import('src/components/user_profile/user_profile.vue'), + ), + }, + { + name: 'lists', + path: '/lists', + component: defineAsyncComponent( + () => import('src/components/lists/lists.vue'), + ), + }, + { + name: 'lists-timeline', + path: '/lists/:id', + component: defineAsyncComponent( + () => import('src/components/lists_timeline/lists_timeline.vue'), + ), + }, + { + name: 'lists-edit', + path: '/lists/:id/edit', + component: defineAsyncComponent( + () => import('src/components/lists_edit/lists_edit.vue'), + ), + }, + { + name: 'lists-new', + path: '/lists/new', + component: defineAsyncComponent( + () => import('src/components/lists_edit/lists_edit.vue'), + ), }, - { name: 'drafts', path: '/drafts', component: Drafts }, - { name: 'user-profile', path: '/users/:name', component: UserProfile }, - { name: 'legacy-user-profile', path: '/:name', component: UserProfile }, - { name: 'lists', path: '/lists', component: Lists }, - { name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline }, - { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }, - { name: 'lists-new', path: '/lists/new', component: ListsEdit }, { name: 'edit-navigation', path: '/nav-edit', @@ -187,12 +257,19 @@ export default (store) => { { name: 'bookmark-folders', path: '/bookmark_folders', - component: BookmarkFolders, + component: defineAsyncComponent( + () => import('src/components/bookmark_folders/bookmark_folders.vue'), + ), }, { name: 'bookmark-folder-new', path: '/bookmarks/new-folder', - component: BookmarkFolderEdit, + component: defineAsyncComponent( + () => + import( + 'src/components/bookmark_folder_edit/bookmark_folder_edit.vue' + ), + ), }, { name: 'bookmark-folder', @@ -202,7 +279,12 @@ export default (store) => { { name: 'bookmark-folder-edit', path: '/bookmarks/:id/edit', - component: BookmarkFolderEdit, + component: defineAsyncComponent( + () => + import( + 'src/components/bookmark_folder_edit/bookmark_folder_edit.vue' + ), + ), }, ] @@ -211,14 +293,18 @@ export default (store) => { { name: 'chat', path: '/users/:username/chats/:recipient_id', - component: Chat, + component: defineAsyncComponent( + () => import('src/components/chat/chat.vue'), + ), meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute, }, { name: 'chats', path: '/users/:username/chats', - component: ChatList, + component: defineAsyncComponent( + () => import('src/components/chat_list/chat_list.vue'), + ), meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute, }, diff --git a/src/components/about/about.js b/src/components/about/about.js index dc6733491..e293895d5 100644 --- a/src/components/about/about.js +++ b/src/components/about/about.js @@ -1,10 +1,10 @@ import { mapState } from 'pinia' -import FeaturesPanel from '../features_panel/features_panel.vue' -import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue' -import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue' -import StaffPanel from '../staff_panel/staff_panel.vue' -import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue' +import FeaturesPanel from 'src/components/features_panel/features_panel.vue' +import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue' +import MRFTransparencyPanel from 'src/components/mrf_transparency_panel/mrf_transparency_panel.vue' +import StaffPanel from 'src/components/staff_panel/staff_panel.vue' +import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue' import { useInstanceStore } from 'src/stores/instance.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' @@ -27,7 +27,11 @@ const About = { frontendVersionLink() { return pleromaFeCommitUrl + this.frontendVersion }, - ...mapState(useInstanceStore, ['backendVersion', 'backendRepository', 'frontendVersion']), + ...mapState(useInstanceStore, [ + 'backendVersion', + 'backendRepository', + 'frontendVersion', + ]), showInstanceSpecificPanel() { return ( useInstanceStore().instanceIdentity.showInstanceSpecificPanel && diff --git a/src/components/about/about.vue b/src/components/about/about.vue index df395c7dd..f7bc0d00d 100644 --- a/src/components/about/about.vue +++ b/src/components/about/about.vue @@ -7,7 +7,9 @@
-
{{ $t('settings.version.title') }}
+
+ {{ $t('settings.version.title') }} +
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index f204adbde..9b4775b97 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -1,10 +1,9 @@ import { mapState } from 'pinia' +import { defineAsyncComponent } from 'vue' +import Popover from 'src/components/popover/popover.vue' +import ProgressButton from 'src/components/progress_button/progress_button.vue' import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' -import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue' -import ConfirmModal from '../confirm_modal/confirm_modal.vue' -import Popover from '../popover/popover.vue' -import ProgressButton from '../progress_button/progress_button.vue' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' @@ -27,8 +26,15 @@ const AccountActions = { ProgressButton, Popover, UserListMenu, - ConfirmModal, - UserTimedFilterModal, + ConfirmModal: defineAsyncComponent( + () => import('src/components/confirm_modal/confirm_modal.vue'), + ), + UserTimedFilterModal: defineAsyncComponent( + () => + import( + 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue' + ), + ), }, methods: { showConfirmRemoveUserFromFollowers() { diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 94cb91ee0..0c93872de 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -94,7 +94,7 @@ - - + - - + import('src/components/flash/flash.vue')), + + VideoAttachment: defineAsyncComponent( + () => import('src/components/video_attachment/video_attachment.vue'), + ), + Popover, }, computed: { classNames() { @@ -180,9 +182,6 @@ const Attachment = { setFlashLoaded(event) { this.flashLoaded = event }, - toggleDescription() { - this.showDescription = !this.showDescription - }, toggleHidden(event) { if ( this.mergedConfig.useOneClickNsfw && diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss index 97515eb32..3c3ba7751 100644 --- a/src/components/attachment/attachment.scss +++ b/src/components/attachment/attachment.scss @@ -134,7 +134,7 @@ width: 2em; height: 2em; margin-left: 0.5em; - font-size: 1.25em; + font-size: 1em; } } @@ -265,3 +265,27 @@ } } } + +.description-popover { + padding: 1em; + width: 50ch; + max-width: 90vw; + overflow: hidden; + box-sizing: border-box; + + summary { + display: inline-block; + margin-bottom: 0.5em; + font-weight: bold; + pointer-events: none; + } + + span { + display: block; + overflow-y: auto; + max-height: 10.5em; + text-wrap: pretty; + line-height: 1.5; + white-space: pre-wrap; + } +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 0db86ff8a..1a1b84628 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -30,21 +30,16 @@
- -

- {{ localDescription }} -

+ />
- + + +
- -

- {{ localDescription }} -

+ />
diff --git a/src/components/auth_form/auth_form.js b/src/components/auth_form/auth_form.js index 9e05095ca..7d853d119 100644 --- a/src/components/auth_form/auth_form.js +++ b/src/components/auth_form/auth_form.js @@ -1,9 +1,9 @@ import { mapState } from 'pinia' import { h, resolveComponent } from 'vue' -import LoginForm from '../login_form/login_form.vue' -import MFARecoveryForm from '../mfa_form/recovery_form.vue' -import MFATOTPForm from '../mfa_form/totp_form.vue' +import LoginForm from 'src/components/login_form/login_form.vue' +import MFARecoveryForm from 'src/components/mfa_form/recovery_form.vue' +import MFATOTPForm from 'src/components/mfa_form/totp_form.vue' import { useAuthFlowStore } from 'src/stores/auth_flow.js' diff --git a/src/components/avatar_list/avatar_list.js b/src/components/avatar_list/avatar_list.js index 830b64219..7806fba81 100644 --- a/src/components/avatar_list/avatar_list.js +++ b/src/components/avatar_list/avatar_list.js @@ -1,4 +1,4 @@ -import UserAvatar from '../user_avatar/user_avatar.vue' +import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import { useInstanceStore } from 'src/stores/instance.js' diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js index a42293cc1..ae1f1c9c6 100644 --- a/src/components/basic_user_card/basic_user_card.js +++ b/src/components/basic_user_card/basic_user_card.js @@ -1,7 +1,6 @@ -import RichContent from 'src/components/rich_content/rich_content.jsx' -import UserAvatar from '../user_avatar/user_avatar.vue' -import UserLink from '../user_link/user_link.vue' -import UserPopover from '../user_popover/user_popover.vue' +import UserAvatar from 'src/components/user_avatar/user_avatar.vue' +import UserLink from 'src/components/user_link/user_link.vue' +import UserPopover from 'src/components/user_popover/user_popover.vue' import { useInstanceStore } from 'src/stores/instance.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' @@ -9,11 +8,19 @@ import { useMergedConfigStore } from 'src/stores/merged_config.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' const BasicUserCard = { - props: ['user'], + props: { + user: { + type: Object, + }, + showLineLabels: { + type: Boolean, + default: false, + }, + }, components: { UserPopover, UserAvatar, - RichContent, + UserLink, }, methods: { @@ -29,7 +36,7 @@ const BasicUserCard = { allowNonSquareEmoji() { return useMergedConfigStore().mergedConfig.nonSquareEmoji }, - } + }, } export default BasicUserCard diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 572b07abb..49786bf1b 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -23,6 +23,10 @@ :title="user.name" class="basic-user-card-user-name" > + + {{ $t('admin_dash.users.labels.name_colon') }} + {{ ' ' }} +
+ + {{ $t('admin_dash.users.labels.handle_colon') }} + {{ ' ' }} + .basic-user-card { display: flex; - flex: 1 0; + flex: 1 1 10em; + min-width: 1em; margin: 0; + line-height: 1.25; --emoji-size: 1em; @@ -69,7 +79,7 @@ &-user-name-value, &-screen-name { - display: inline-block; + display: inline; max-width: 100%; overflow: hidden; white-space: nowrap; diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js index 967ce2a3c..6484accff 100644 --- a/src/components/block_card/block_card.js +++ b/src/components/block_card/block_card.js @@ -1,7 +1,7 @@ import { mapState } from 'pinia' +import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue' -import BasicUserCard from '../basic_user_card/basic_user_card.vue' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' diff --git a/src/components/block_card/block_card.vue b/src/components/block_card/block_card.vue index 90b9a2b16..be6069c14 100644 --- a/src/components/block_card/block_card.vue +++ b/src/components/block_card/block_card.vue @@ -1,5 +1,5 @@ diff --git a/src/components/bookmark_folder_card/bookmark_folder_card.js b/src/components/bookmark_folder_card/bookmark_folder_card.js deleted file mode 100644 index 37b3f2e5e..000000000 --- a/src/components/bookmark_folder_card/bookmark_folder_card.js +++ /dev/null @@ -1,15 +0,0 @@ -import { library } from '@fortawesome/fontawesome-svg-core' -import { faEllipsisH } from '@fortawesome/free-solid-svg-icons' - -library.add(faEllipsisH) - -const BookmarkFolderCard = { - props: ['folder', 'allBookmarks'], - computed: { - firstLetter() { - return this.folder ? this.folder.name[0] : null - }, - }, -} - -export default BookmarkFolderCard diff --git a/src/components/bookmark_folder_card/bookmark_folder_card.vue b/src/components/bookmark_folder_card/bookmark_folder_card.vue deleted file mode 100644 index 9e8bef618..000000000 --- a/src/components/bookmark_folder_card/bookmark_folder_card.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - - - diff --git a/src/components/bookmark_folder_edit/bookmark_folder_edit.js b/src/components/bookmark_folder_edit/bookmark_folder_edit.js index a036895bf..ae5aebedd 100644 --- a/src/components/bookmark_folder_edit/bookmark_folder_edit.js +++ b/src/components/bookmark_folder_edit/bookmark_folder_edit.js @@ -1,8 +1,10 @@ -import apiService from '../../services/api/api.service' -import EmojiPicker from '../emoji_picker/emoji_picker.vue' +import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchBookmarkFolders } from 'src/api/user.js' const BookmarkFolderEdit = { data() { @@ -22,8 +24,10 @@ const BookmarkFolderEdit = { }, created() { if (!this.id) return - const credentials = this.$store.state.users.currentUser.credentials - apiService.fetchBookmarkFolders({ credentials }).then((folders) => { + + fetchBookmarkFolders({ + credentials: useOAuthStore().token, + }).then(({ data: folders }) => { const folder = folders.find((folder) => folder.id === this.id) if (!folder) return diff --git a/src/components/bookmark_folders/bookmark_folders.js b/src/components/bookmark_folders/bookmark_folders.js index 1147fd3a5..cd12d4c3a 100644 --- a/src/components/bookmark_folders/bookmark_folders.js +++ b/src/components/bookmark_folders/bookmark_folders.js @@ -1,4 +1,4 @@ -import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue' +import FolderCard from 'src/components/folder_card/folder_card.vue' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' @@ -9,7 +9,7 @@ const BookmarkFolders = { } }, components: { - BookmarkFolderCard, + FolderCard, }, computed: { bookmarkFolders() { diff --git a/src/components/bookmark_folders/bookmark_folders.vue b/src/components/bookmark_folders/bookmark_folders.vue index fdd461064..56c9b62ce 100644 --- a/src/components/bookmark_folders/bookmark_folders.vue +++ b/src/components/bookmark_folders/bookmark_folders.vue @@ -12,14 +12,28 @@
- - + + + + {{ $t('nav.all_bookmarks') }} + +
+ diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js index a241b6ac7..bf633c18c 100644 --- a/src/components/bookmark_timeline/bookmark_timeline.js +++ b/src/components/bookmark_timeline/bookmark_timeline.js @@ -1,4 +1,4 @@ -import Timeline from '../timeline/timeline.vue' +import Timeline from 'src/components/timeline/timeline.vue' const Bookmarks = { created() { diff --git a/src/components/bubble_timeline/bubble_timeline.js b/src/components/bubble_timeline/bubble_timeline.js index d3835e0e8..fcbc31ad2 100644 --- a/src/components/bubble_timeline/bubble_timeline.js +++ b/src/components/bubble_timeline/bubble_timeline.js @@ -1,4 +1,4 @@ -import Timeline from '../timeline/timeline.vue' +import Timeline from 'src/components/timeline/timeline.vue' const BubbleTimeline = { components: { diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index caeb2aea7..16a03ab1d 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -1,14 +1,13 @@ -import _ from 'lodash' +import { throttle } from 'lodash' import { mapState as mapPiniaState } from 'pinia' import { mapGetters, mapState } from 'vuex' -import { WSConnectionStatus } from '../../services/api/api.service.js' +import ChatMessage from 'src/components/chat_message/chat_message.vue' +import ChatTitle from 'src/components/chat_title/chat_title.vue' +import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import chatService from '../../services/chat_service/chat_service.js' import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js' import { promiseInterval } from '../../services/promise_interval/promise_interval.js' -import ChatMessage from '../chat_message/chat_message.vue' -import ChatTitle from '../chat_title/chat_title.vue' -import PostStatusForm from '../post_status_form/post_status_form.vue' import { getNewTopPosition, getScrollPosition, @@ -17,6 +16,14 @@ import { } from './chat_layout_utils.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + chatMessages, + getOrCreateChat, + sendChatMessage, +} from 'src/api/chats.js' +import { WSConnectionStatus } from 'src/api/websocket.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons' @@ -115,7 +122,6 @@ const Chat = { mobileLayout: (store) => store.layoutType === 'mobile', }), ...mapState({ - backendInteractor: (state) => state.api.backendInteractor, mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus, currentUser: (state) => state.users.currentUser, }), @@ -224,7 +230,7 @@ const Chat = { } }, 5000) }, - handleScroll: _.throttle(function () { + handleScroll: throttle(function () { this.lastScrollPosition = getScrollPosition() if (!this.currentChat) { return @@ -267,43 +273,48 @@ const Chat = { const fetchOlderMessages = !!maxId const sinceId = fetchLatest && chatMessageService.maxId - return this.backendInteractor - .chatMessages({ id: chatId, maxId, sinceId }) - .then((messages) => { - // Clear the current chat in case we're recovering from a ws connection loss. - if (isFirstFetch) { - chatService.clear(chatMessageService) - } + return chatMessages({ + id: chatId, + maxId, + sinceId, + credentials: useOAuthStore().token, + }).then(({ data: messages }) => { + // Clear the current chat in case we're recovering from a ws connection loss. + if (isFirstFetch) { + chatService.clear(chatMessageService) + } - const positionBeforeUpdate = getScrollPosition() - this.$store - .dispatch('addChatMessages', { chatId, messages }) - .then(() => { - this.$nextTick(() => { - if (fetchOlderMessages) { - this.handleScrollUp(positionBeforeUpdate) - } + const positionBeforeUpdate = getScrollPosition() + this.$store + .dispatch('addChatMessages', { chatId, messages }) + .then(() => { + this.$nextTick(() => { + if (fetchOlderMessages) { + this.handleScrollUp(positionBeforeUpdate) + } - // In vertical screens, the first batch of fetched messages may not always take the - // full height of the scrollable container. - // If this is the case, we want to fetch the messages until the scrollable container - // is fully populated so that the user has the ability to scroll up and load the history. - if (!isScrollable() && messages.length > 0) { - this.fetchChat({ - maxId: this.currentChatMessageService.minId, - }) - } - }) + // In vertical screens, the first batch of fetched messages may not always take the + // full height of the scrollable container. + // If this is the case, we want to fetch the messages until the scrollable container + // is fully populated so that the user has the ability to scroll up and load the history. + if (!isScrollable() && messages.length > 0) { + this.fetchChat({ + maxId: this.currentChatMessageService.minId, + }) + } }) - }) + }) + }) }, async startFetching() { let chat = this.findOpenedChatByRecipientId(this.recipientId) if (!chat) { try { - chat = await this.backendInteractor.getOrCreateChat({ + const { data } = await getOrCreateChat({ accountId: this.recipientId, + credentials: useOAuthStore().token, }) + chat = data } catch (e) { console.error('Error creating or getting a chat', e) this.errorLoadingChat = true @@ -369,9 +380,11 @@ const Chat = { doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) { if (retriesLeft <= 0) return - this.backendInteractor - .sendChatMessage(params) - .then((data) => { + sendChatMessage({ + params, + credentials: useOAuthStore().token, + }) + .then(({ data }) => { this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, updateMaxId: false, diff --git a/src/components/chat_list/chat_list.js b/src/components/chat_list/chat_list.js index 4c3194ae1..597fcc709 100644 --- a/src/components/chat_list/chat_list.js +++ b/src/components/chat_list/chat_list.js @@ -1,8 +1,8 @@ import { mapGetters, mapState } from 'vuex' -import ChatListItem from '../chat_list_item/chat_list_item.vue' -import ChatNew from '../chat_new/chat_new.vue' -import List from '../list/list.vue' +import ChatListItem from 'src/components/chat_list_item/chat_list_item.vue' +import ChatNew from 'src/components/chat_new/chat_new.vue' +import List from 'src/components/list/list.vue' const ChatList = { components: { diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue index 881df2988..2e79ea100 100644 --- a/src/components/chat_list/chat_list.vue +++ b/src/components/chat_list/chat_list.vue @@ -22,7 +22,7 @@ v-if="sortedChatList.length > 0" class="timeline" > - + + diff --git a/src/components/folder_card/folder_card.js b/src/components/folder_card/folder_card.js new file mode 100644 index 000000000..7a68bc3ac --- /dev/null +++ b/src/components/folder_card/folder_card.js @@ -0,0 +1,38 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faEllipsisH } from '@fortawesome/free-solid-svg-icons' + +library.add(faEllipsisH) + +const FolderCard = { + props: { + name: { + type: String, + required: true, + }, + emoji: { + type: String, + required: false, + default: null, + }, + emojiUrl: { + type: String, + required: false, + default: null, + }, + link: { + type: Object, + required: true, + }, + linkEdit: { + type: Object, + required: true, + }, + }, + computed: { + firstLetter() { + return this.name[0] + }, + }, +} + +export default FolderCard diff --git a/src/components/folder_card/folder_card.vue b/src/components/folder_card/folder_card.vue new file mode 100644 index 000000000..043a326cc --- /dev/null +++ b/src/components/folder_card/folder_card.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js index 644f95f85..3fecc025f 100644 --- a/src/components/follow_button/follow_button.js +++ b/src/components/follow_button/follow_button.js @@ -1,14 +1,17 @@ +import { defineAsyncComponent } from 'vue' + import { requestFollow, requestUnfollow, } from '../../services/follow_manipulate/follow_manipulate' -import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { useMergedConfigStore } from 'src/stores/merged_config.js' export default { props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], components: { - ConfirmModal, + ConfirmModal: defineAsyncComponent( + () => import('src/components/confirm_modal/confirm_modal.vue'), + ), }, data() { return { diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue index 3606b68be..ba0cda46d 100644 --- a/src/components/follow_button/follow_button.vue +++ b/src/components/follow_button/follow_button.vue @@ -8,7 +8,7 @@ > {{ label }} - - + diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js index e4c84dcdd..8fffbf730 100644 --- a/src/components/follow_card/follow_card.js +++ b/src/components/follow_card/follow_card.js @@ -1,7 +1,7 @@ -import BasicUserCard from '../basic_user_card/basic_user_card.vue' -import FollowButton from '../follow_button/follow_button.vue' -import RemoteFollow from '../remote_follow/remote_follow.vue' -import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue' +import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' +import FollowButton from 'src/components/follow_button/follow_button.vue' +import RemoteFollow from 'src/components/remote_follow/remote_follow.vue' +import RemoveFollowerButton from 'src/components/remove_follower_button/remove_follower_button.vue' const FollowCard = { props: ['user', 'noFollowsYou'], diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js index a6ffcd28b..c2e10c242 100644 --- a/src/components/follow_request_card/follow_request_card.js +++ b/src/components/follow_request_card/follow_request_card.js @@ -1,14 +1,20 @@ +import { defineAsyncComponent } from 'vue' + import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js' import BasicUserCard from '../basic_user_card/basic_user_card.vue' -import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { approveUser, denyUser } from 'src/api/user.js' const FollowRequestCard = { props: ['user'], components: { BasicUserCard, - ConfirmModal, + ConfirmModal: defineAsyncComponent( + () => import('src/components/confirm_modal/confirm_modal.vue'), + ), }, data() { return { @@ -45,7 +51,10 @@ const FollowRequestCard = { } }, doApprove() { - this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) + approveUser({ + id: this.user.id, + credentials: useOAuthStore().token, + }) this.$store.dispatch('removeFollowRequest', this.user) const notifId = this.findFollowRequestNotificationId() @@ -67,12 +76,14 @@ const FollowRequestCard = { }, doDeny() { const notifId = this.findFollowRequestNotificationId() - this.$store.state.api.backendInteractor - .denyUser({ id: this.user.id }) - .then(() => { - this.$store.dispatch('dismissNotificationLocal', { id: notifId }) - this.$store.dispatch('removeFollowRequest', this.user) - }) + + denyUser({ + id: this.user.id, + credentials: useOAuthStore().token, + }).then(() => { + this.$store.dispatch('dismissNotificationLocal', { id: notifId }) + this.$store.dispatch('removeFollowRequest', this.user) + }) this.hideDenyConfirmDialog() }, }, diff --git a/src/components/follow_request_card/follow_request_card.vue b/src/components/follow_request_card/follow_request_card.vue index 55b651120..64b185094 100644 --- a/src/components/follow_request_card/follow_request_card.vue +++ b/src/components/follow_request_card/follow_request_card.vue @@ -15,7 +15,7 @@ - {{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }} - - + {{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }} - + diff --git a/src/components/follow_requests/follow_requests.js b/src/components/follow_requests/follow_requests.js index 7dbecee53..513298afc 100644 --- a/src/components/follow_requests/follow_requests.js +++ b/src/components/follow_requests/follow_requests.js @@ -1,4 +1,4 @@ -import FollowRequestCard from '../follow_request_card/follow_request_card.vue' +import FollowRequestCard from 'src/components/follow_request_card/follow_request_card.vue' const FollowRequests = { components: { diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js index 81c46ec89..697d83ee2 100644 --- a/src/components/font_control/font_control.js +++ b/src/components/font_control/font_control.js @@ -1,7 +1,7 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import Popover from 'src/components/popover/popover.vue' +import Select from 'src/components/select/select.vue' import LocalSettingIndicator from 'src/components/settings_modal/helpers/local_setting_indicator.vue' -import Select from '../select/select.vue' import { useInterfaceStore } from 'src/stores/interface.js' diff --git a/src/components/friends_timeline/friends_timeline.js b/src/components/friends_timeline/friends_timeline.js index c0c032a8f..b6bee7305 100644 --- a/src/components/friends_timeline/friends_timeline.js +++ b/src/components/friends_timeline/friends_timeline.js @@ -1,4 +1,4 @@ -import Timeline from '../timeline/timeline.vue' +import Timeline from 'src/components/timeline/timeline.vue' const FriendsTimeline = { components: { diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js index a8d0d53e9..c244a8cb3 100644 --- a/src/components/gallery/gallery.js +++ b/src/components/gallery/gallery.js @@ -1,6 +1,6 @@ import { set, sumBy } from 'lodash' -import Attachment from '../attachment/attachment.vue' +import Attachment from 'src/components/attachment/attachment.vue' import { useMediaViewerStore } from 'src/stores/media_viewer.js' @@ -27,8 +27,10 @@ const Gallery = { return { sizes: {}, hidingLong: true, + playingMedia: new Set(), } }, + emits: ['play', 'pause'], components: { Attachment }, computed: { rows() { @@ -47,7 +49,7 @@ const Gallery = { : attachments .reduce( (acc, attachment, i) => { - const peek = attachments[i+1] + const peek = attachments[i + 1] const nextEnd = peek == null const nextWide = !nextEnd && !displayTypes.has(peek?.type) @@ -68,12 +70,23 @@ const Gallery = { } const maxPerRow = 3 - const currentRow = acc[acc.length - 1].items - if ((nextWide || nextEnd) && currentRow.length >= maxPerRow) { - const last = currentRow.splice(-1)[0] - return [...acc, { items: [last, attachment] }] + const currentRow = acc[acc.length - 1] + const previousRow = acc[acc.length - 2] + + if (currentRow.items.length >= maxPerRow) { + if (nextWide || nextEnd) { + if (previousRow?.items.length > 1) { + currentRow.items.push(attachment) + return [...acc, { items: [] }] + } else { + const last = currentRow.items.splice(-1)[0] + return [...acc, { items: [last, attachment] }] + } + } else { + return [...acc, { items: [attachment] }] + } } else { - currentRow.push(attachment) + currentRow.items.push(attachment) } return acc }, @@ -104,11 +117,21 @@ const Gallery = { return this.attachmentsDimensionalScore > 1 } }, + hasPlayingMedia() { + return this.playingMedia.size > 0 + }, }, methods: { onNaturalSizeLoad({ id, width, height }) { set(this.sizes, id, { width, height }) }, + onMediaStateChange(playing, id) { + if (playing) { + this.playingMedia.add(id) + } else { + this.playingMedia.delete(id) + } + }, rowStyle(row) { if (row.audio) { return { 'padding-bottom': '25%' } // fixed reduced height for audio @@ -135,6 +158,15 @@ const Gallery = { useMediaViewerStore().setMedia(this.attachments) }, }, + watch: { + hasPlayingMedia(newValue) { + if (newValue) { + this.$emit('play') + } else { + this.$emit('pause') + } + }, + }, } export default Gallery diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index eb1054df6..a32d4feb7 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -34,6 +34,8 @@ :style="itemStyle(attachment.id, row.items)" @set-media="onMedia" @natural-size-load="onNaturalSizeLoad" + @play="() => onMediaStateChange(true, attachment.id)" + @pause="() => onMediaStateChange(false, attachment.id)" /> @@ -129,7 +131,7 @@ .gallery-item { margin: 0; - height: 15em; + height: 20em; } } } diff --git a/src/components/global_error/global_error.js b/src/components/global_error/global_error.js new file mode 100644 index 000000000..cd4593521 --- /dev/null +++ b/src/components/global_error/global_error.js @@ -0,0 +1,56 @@ +import { mapActions, mapState } from 'pinia' + +import ErrorModal from 'src/components/error_modal/error_modal.vue' + +import { useInterfaceStore } from 'src/stores/interface.js' + +const GlobalError = { + components: { + ErrorModal, + }, + computed: { + title() { + if (this.globalError == null) return null + return this.globalError.title && this.$t(this.globalError.title) + }, + content() { + if (this.globalError == null) return null + if (this.globalError.content) { + return this.$t(this.globalError.content, [this.globalError.error]) + } else { + return null + } + }, + details() { + if (this.globalError == null) return null + if (this.globalError.error != null) { + return ( + this.globalError.error.toString() + + '\n\n' + + this.globalError.error.stack + ) + } else { + return this.globalError.details + } + }, + recoverText() { + if (this.globalError == null) return null + if (this.globalError.recoverText == null) return null + return this.$t(this.globalError.recoverText) + }, + ...mapState(useInterfaceStore, ['globalError']), + }, + methods: { + clear() { + this.globalError.clear?.() + this.clearGlobalError() + }, + recover() { + this.globalError.recover?.() + this.clearGlobalError() + }, + ...mapActions(useInterfaceStore, ['clearGlobalError']), + }, +} + +export default GlobalError diff --git a/src/components/global_error/global_error.vue b/src/components/global_error/global_error.vue new file mode 100644 index 000000000..2b6e83655 --- /dev/null +++ b/src/components/global_error/global_error.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/components/instance_specific_panel/instance_specific_panel.vue b/src/components/instance_specific_panel/instance_specific_panel.vue index c8ed0a2de..597155e27 100644 --- a/src/components/instance_specific_panel/instance_specific_panel.vue +++ b/src/components/instance_specific_panel/instance_specific_panel.vue @@ -11,3 +11,9 @@ + + diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js index 87e9e1b87..d008c4b30 100644 --- a/src/components/interactions/interactions.js +++ b/src/components/interactions/interactions.js @@ -1,5 +1,5 @@ +import Notifications from 'src/components/notifications/notifications.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' -import Notifications from '../notifications/notifications.vue' const tabModeDict = { mentions: ['mention'], @@ -17,7 +17,7 @@ const Interactions = { allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, filterMode: tabModeDict.mentions, - canSeeReports: this.$store.state.users.currentUser.privileges.includes( + canSeeReports: this.$store.state.users.currentUser.privileges.has( 'reports_manage_reports', ), } diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue index 1e2efe402..98fba754d 100644 --- a/src/components/interactions/interactions.vue +++ b/src/components/interactions/interactions.vue @@ -42,7 +42,6 @@ item.id, + }, + getClass: { + type: Function, + default: () => '', + }, + preSelect: { + type: Array, + default: [], + }, + nonInteractive: { + type: Boolean, + default: false, + }, + scrollable: { + type: Boolean, + default: false, + }, + selectable: { + type: Boolean, + default: false, + }, + externalItems: { + type: Array, + default: null, + }, + }, + emits: ['fetchRequested', 'select'], + components: { + Checkbox, + }, + data() { + return { + items: [], + selected: new Set(this.preSelect), + loading: false, + bottomedOut: true, + error: null, + page: 1, + total: null, + } + }, + computed: { + allKeys() { + return new Set(this.finalItems.map(this.getKey)) + }, + selectedItems() { + return this.items.filter((item) => this.selected.has(this.getKey(item))) + }, + allSelected() { + return ( + this.selected.size !== 0 && + this.selected.size === this.finalItems.length + ) + }, + noneSelected() { + return this.selected.size === 0 + }, + someSelected() { + return !this.allSelected && !this.noneSelected + }, + finalItems() { + return this.externalItems || this.items + }, + }, + created() { + window.addEventListener('scroll', this.scrollLoad) + + if (this.fetchFunction && this.items.length === 0) { + this.fetchEntries() + } + }, + unmounted() { + window.removeEventListener('scroll', this.scrollLoad) + }, + methods: { + fetchEntries() { + if (this.loading) return + + this.loading = true + this.error = null + + this.fetchFunction(this.page) + .then((result) => { + this.loading = false + this.bottomedOut = isEmpty(result.items) + if (this.externalItems) return + this.page += 1 + this.total = result.count + this.items.push(...result.items) + }) + .catch((error) => { + this.loading = false + this.error = error + console.error('Error loading list data:', error) + }) + }, + reset() { + this.items = [] + this.page = 1 + this.total = null + this.error = null + this.loading = false + this.fetchEntries() + }, + scrollLoad(e) { + if (this.fetchFunction) { + const bodyBRect = document.body.getBoundingClientRect() + const height = Math.max(bodyBRect.height, -bodyBRect.y) + if ( + this.$el.offsetHeight > 0 && + window.innerHeight + window.pageYOffset >= height - 750 + ) { + this.fetchEntries() + } + } + }, + isSelected(item) { + return this.selected.has(this.getKey(item)) + }, + toggle(checked, item) { + const key = this.getKey(item) + if (checked) { + this.selected.add(key) + } else { + this.selected.delete(key) + } + + this.$emit('select', this.selected) + }, + toggleAll(value) { + if (value) { + this.selected = new Set([...this.allKeys]) + } else { + this.selected = new Set([]) + } + this.$emit('select', this.selected) + }, + }, +} + +export default List diff --git a/src/components/list/list.vue b/src/components/list/list.vue index f34a5f073..19c52cf4b 100644 --- a/src/components/list/list.vue +++ b/src/components/list/list.vue @@ -1,48 +1,90 @@ - + + + diff --git a/src/components/lists/lists.js b/src/components/lists/lists.js index ae58960f5..7545d9126 100644 --- a/src/components/lists/lists.js +++ b/src/components/lists/lists.js @@ -1,4 +1,4 @@ -import ListsCard from '../lists_card/lists_card.vue' +import FolderCard from 'src/components/folder_card/folder_card.vue' import { useListsStore } from 'src/stores/lists.js' @@ -9,7 +9,7 @@ const Lists = { } }, components: { - ListsCard, + FolderCard, }, computed: { lists() { diff --git a/src/components/lists/lists.vue b/src/components/lists/lists.vue index 05df5b72f..f3987205e 100644 --- a/src/components/lists/lists.vue +++ b/src/components/lists/lists.vue @@ -14,10 +14,12 @@
-
diff --git a/src/components/lists_card/lists_card.js b/src/components/lists_card/lists_card.js deleted file mode 100644 index 81b811534..000000000 --- a/src/components/lists_card/lists_card.js +++ /dev/null @@ -1,10 +0,0 @@ -import { library } from '@fortawesome/fontawesome-svg-core' -import { faEllipsisH } from '@fortawesome/free-solid-svg-icons' - -library.add(faEllipsisH) - -const ListsCard = { - props: ['list'], -} - -export default ListsCard diff --git a/src/components/lists_card/lists_card.vue b/src/components/lists_card/lists_card.vue deleted file mode 100644 index a5dc6371e..000000000 --- a/src/components/lists_card/lists_card.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js index 483a9c02a..d7a8525a8 100644 --- a/src/components/lists_edit/lists_edit.js +++ b/src/components/lists_edit/lists_edit.js @@ -1,11 +1,11 @@ import { mapState as mapPiniaState } from 'pinia' import { mapGetters, mapState } from 'vuex' +import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' +import ListsUserSearch from 'src/components/lists_user_search/lists_user_search.vue' import PanelLoading from 'src/components/panel_loading/panel_loading.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' -import BasicUserCard from '../basic_user_card/basic_user_card.vue' -import ListsUserSearch from '../lists_user_search/lists_user_search.vue' -import UserAvatar from '../user_avatar/user_avatar.vue' +import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import { useInterfaceStore } from 'src/stores/interface.js' import { useListsStore } from 'src/stores/lists.js' diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue index 3ff8c1072..25b818deb 100644 --- a/src/components/lists_edit/lists_edit.vue +++ b/src/components/lists_edit/lists_edit.vue @@ -50,7 +50,7 @@
{ - const app = { + window.location.href = getLoginUrl({ clientId: this.clientId, - clientSecret: this.clientSecret, - } - oauthApi.login({ ...app, ...data }) + instance: this.server, + }) }) }, submitPassword() { @@ -56,37 +50,32 @@ const LoginForm = { // NOTE: we do not really need the app token, but obtaining a token and // calling verify_credentials is the only way to ensure the app still works. this.ensureAppToken().then(() => { - const app = { + getTokenWithCredentials({ clientId: this.clientId, clientSecret: this.clientSecret, - } - - oauthApi - .getTokenWithCredentials({ - ...app, - instance: this.server, - username: this.user.username, - password: this.user.password, - }) - .then((result) => { - if (result.error) { - if (result.error === 'mfa_required') { - this.requireMFA({ settings: result }) - } else if (result.identifier === 'password_reset_required') { - this.$router.push({ - name: 'password-reset', - params: { passwordResetRequested: true }, - }) - } else { - this.error = result.error - this.focusOnPasswordInput() - } - return - } + instance: this.server, + username: this.user.username, + password: this.user.password, + }) + .then(({ data: result }) => { this.login(result).then(() => { this.$router.push({ name: 'friends' }) }) }) + .catch((error) => { + if (error.errorData?.error === 'mfa_required') { + this.requireMFA({ settings: error }) + } else if (error.identifier === 'password_reset_required') { + this.$router.push({ + name: 'password-reset', + params: { passwordResetRequested: true }, + }) + } else { + this.error = error + this.focusOnPasswordInput() + } + return + }) }) }, clearError() { diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 0808b8cd6..fe5994f7c 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -1,10 +1,7 @@ -import Flash from 'src/components/flash/flash.vue' +import { defineAsyncComponent } from 'vue' + +import Modal from 'src/components/modal/modal.vue' import GestureService from '../../services/gesture_service/gesture_service' -import Modal from '../modal/modal.vue' -import PinchZoom from '../pinch_zoom/pinch_zoom.vue' -import StillImage from '../still-image/still-image.vue' -import SwipeClick from '../swipe_click/swipe_click.vue' -import VideoAttachment from '../video_attachment/video_attachment.vue' import { useMediaViewerStore } from 'src/stores/media_viewer.js' @@ -20,12 +17,17 @@ library.add(faChevronLeft, faChevronRight, faCircleNotch, faTimes) const MediaModal = { components: { - StillImage, - VideoAttachment, - PinchZoom, - SwipeClick, + VideoAttachment: defineAsyncComponent( + () => import('src/components/video_attachment/video_attachment.vue'), + ), + PinchZoom: defineAsyncComponent( + () => import('src/components/pinch_zoom/pinch_zoom.vue'), + ), + SwipeClick: defineAsyncComponent( + () => import('src/components/swipe_click/swipe_click.vue'), + ), Modal, - Flash, + Flash: defineAsyncComponent(() => import('src/components/flash/flash.vue')), }, data() { return { diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 93587210f..f2f47a0b6 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -89,13 +89,16 @@ /> - - {{ description }} - + {{ $t('status.attachment_description') }} + {{ description }} + {{ $t('media_modal.counter', { current: currentIndex + 1, total: media.length }, currentIndex + 1) }} @@ -159,19 +162,43 @@ $modal-view-button-icon-margin: 0.5em; .counter { /* Hardcoded since background is also hardcoded */ color: white; - margin-top: 1em; - text-shadow: 0 0 10px black, 0 0 10px black; - padding: 0.2em 2em; + text-shadow: 0 0 1em black, 0 0 1em black, 0 0 1em black; + margin: 1em 2em; + overflow: hidden; + + } + + .description + .counter { + margin-top: 0; } .description { flex: 0 0 auto; - overflow-y: auto; - min-height: 1em; - max-width: 35.8em; + max-width: 80ch; max-height: 9.5em; - overflow-wrap: break-word; - text-wrap: pretty; + + summary { + margin-bottom: 0.5em; + display: inline-block; + font-weight: bold; + pointer-events: none; + } + + span { + display: block; + overflow-y: auto; + min-height: 1em; + text-wrap: pretty; + max-height: 10.5em; + white-space: pre-wrap; + line-height: 1.5; + scrollbar-color: white transparent; + + &::-webkit-scrollbar-button, + &::-webkit-scrollbar-thumb { + background-color: white; + } + } } .modal-image { diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index 5edc89516..48f6d1d8e 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -1,13 +1,13 @@ import { mapState as mapPiniaState } from 'pinia' -import { defineAsyncComponent } from 'vue' -import { mapGetters, mapState } from 'vuex' +import { mapState } from 'vuex' +import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue' +import UserAvatar from 'src/components/user_avatar/user_avatar.vue' +import UserPopover from 'src/components/user_popover/user_popover.vue' import { highlightClass, highlightStyle, } from '../../services/user_highlighter/user_highlighter.js' -import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' -import UserAvatar from '../user_avatar/user_avatar.vue' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' @@ -24,9 +24,7 @@ const MentionLink = { components: { UserAvatar, UnicodeDomainIndicator, - UserPopover: defineAsyncComponent( - () => import('../user_popover/user_popover.vue'), - ), + UserPopover, }, props: { url: { diff --git a/src/components/mentions/mentions.js b/src/components/mentions/mentions.js index 10167ac77..48e06a950 100644 --- a/src/components/mentions/mentions.js +++ b/src/components/mentions/mentions.js @@ -1,4 +1,4 @@ -import Timeline from '../timeline/timeline.vue' +import Timeline from 'src/components/timeline/timeline.vue' const Mentions = { computed: { diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js index f4ddcadd0..e04218bd8 100644 --- a/src/components/mfa_form/recovery_form.js +++ b/src/components/mfa_form/recovery_form.js @@ -1,11 +1,11 @@ import { mapActions, mapState, mapStores } from 'pinia' -import mfaApi from '../../services/new_api/mfa.js' - import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { verifyRecoveryCode } from 'src/api/mfa.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' @@ -43,18 +43,18 @@ export default { code: this.code, } - mfaApi.verifyRecoveryCode(data).then((result) => { - if (result.error) { - this.error = result.error + verifyRecoveryCode(data) + .then((result) => { + this.login(result).then(() => { + this.$router.push({ name: 'friends' }) + }) + }) + .catch((error) => { + this.error = error this.code = null this.focusOnCodeInput() return - } - - this.login(result).then(() => { - this.$router.push({ name: 'friends' }) }) - }) }, }, } diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js index 6d51cba94..056098c25 100644 --- a/src/components/mfa_form/totp_form.js +++ b/src/components/mfa_form/totp_form.js @@ -1,11 +1,11 @@ import { mapActions, mapState, mapStores } from 'pinia' -import mfaApi from '../../services/new_api/mfa.js' - import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' +import { verifyOTPCode } from 'src/api/mfa.js' + import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' @@ -46,18 +46,18 @@ export default { code: this.code, } - mfaApi.verifyOTPCode(data).then((result) => { - if (result.error) { - this.error = result.error + verifyOTPCode(data) + .then(({ data: result }) => { + this.login(result).then(() => { + this.$router.push({ name: 'friends' }) + }) + }) + .catch((error) => { + this.error = error this.code = null this.focusOnCodeInput() return - } - - this.login(result).then(() => { - this.$router.push({ name: 'friends' }) }) - }) }, }, } diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index f47cef893..b47376d53 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -1,4 +1,5 @@ import { mapState } from 'pinia' +import { defineAsyncComponent } from 'vue' import { mapGetters } from 'vuex' import NavigationPins from 'src/components/navigation/navigation_pins.vue' @@ -7,9 +8,6 @@ import { countExtraNotifications, unseenNotificationsFromStore, } from '../../services/notification_utils/notification_utils' -import ConfirmModal from '../confirm_modal/confirm_modal.vue' -import Notifications from '../notifications/notifications.vue' -import SideDrawer from '../side_drawer/side_drawer.vue' import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useInstanceStore } from 'src/stores/instance.js' @@ -29,10 +27,16 @@ library.add(faTimes, faBell, faBars, faArrowUp, faMinus, faCheckDouble) const MobileNav = { components: { - SideDrawer, - Notifications, + SideDrawer: defineAsyncComponent( + () => import('src/components/side_drawer/side_drawer.vue'), + ), + Notifications: defineAsyncComponent( + () => import('src/components/notifications/notifications.vue'), + ), NavigationPins, - ConfirmModal, + ConfirmModal: defineAsyncComponent( + () => import('src/components/confirm_modal/confirm_modal.vue'), + ), }, data: () => ({ notificationsCloseGesture: undefined, @@ -64,6 +68,7 @@ const MobileNav = { countExtraNotifications( this.$store, useMergedConfigStore().mergedConfig, + useAnnouncementsStore().unreadAnnouncementCount, ) ) }, @@ -147,9 +152,6 @@ const MobileNav = { }, onScroll({ target: { scrollTop, clientHeight, scrollHeight } }) { this.notificationsAtTop = scrollTop > 0 - if (scrollTop + clientHeight >= scrollHeight) { - this.$refs.notifications.fetchOlderNotifications() - } }, }, watch: { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index 0eb8c986e..743b7deb0 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -94,6 +94,7 @@ />
+
- {{ $t('login.logout_confirm') }} - +
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js index 4149fd6d0..ca4852ab3 100644 --- a/src/components/moderation_tools/moderation_tools.js +++ b/src/components/moderation_tools/moderation_tools.js @@ -1,7 +1,9 @@ -import DialogModal from '../dialog_modal/dialog_modal.vue' -import Popover from '../popover/popover.vue' +import { last } from 'lodash' -import { useInstanceStore } from 'src/stores/instance.js' +import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' +import Popover from 'src/components/popover/popover.vue' + +import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -16,41 +18,382 @@ const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription' const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription' const SANDBOX = 'mrf_tag:sandbox' const QUARANTINE = 'mrf_tag:quarantine' +const TAGS = new Set([ + FORCE_NSFW, + STRIP_MEDIA, + FORCE_UNLISTED, + DISABLE_REMOTE_SUBSCRIPTION, + DISABLE_ANY_SUBSCRIPTION, + SANDBOX, + QUARANTINE, +]) + +const ENTRIES = [ + { + check: '!state:activated', + label: 'user_card.admin_menu.activate_account', + }, + { + check: 'state:activated', + label: 'user_card.admin_menu.deactivate_account', + }, + { + separator: true, + }, + { + check: '!state:confirmed', + label: 'user_card.admin_menu.confirm_account', + }, + { + check: 'action:resend_confirmation', + conditions: ['!state:confirmed'], + label: 'user_card.admin_menu.resend_confirmation', + }, + // No API for revocation + // { + // check: 'state:confirmed', + // label: 'user_card.admin_menu.unconfirm_account', + // }, + { + check: '!state:approved', + conditions: ['property:local'], + label: 'user_card.admin_menu.approve_account', + }, + // No API for revocation + // { + // check: 'state:approved', + // label: 'user_card.admin_menu.unapprove_account', + // }, + { + check: '!state:suggested', + // conditions: ['property:local'], // TODO Should we allow non-local users in suggested? + label: 'user_card.admin_menu.suggest_account', + }, + { + check: 'state:suggested', + label: 'user_card.admin_menu.remove_suggested_account', + }, + { + separator: true, + }, + { + check: 'action:statuses', + label: 'user_card.admin_menu.show_statuses', + conditions: ['count:1'], + }, + { + separator: true, + }, + { + check: 'action:disable_mfa', + label: 'user_card.admin_menu.disable_mfa', + }, + { + check: 'action:require_password_change', + label: 'user_card.admin_menu.require_password_change', + }, + { + separator: true, + }, + { + check: '!rights:moderator', + label: 'user_card.admin_menu.grant_moderator', + conditions: ['property:local', 'state:activated'], + }, + { + check: 'rights:moderator', + label: 'user_card.admin_menu.revoke_moderator', + conditions: ['property:local', 'state:activated'], + }, + { + check: '!rights:admin', + label: 'user_card.admin_menu.grant_admin', + conditions: ['property:local', 'state:activated'], + }, + { + check: 'rights:admin', + label: 'user_card.admin_menu.revoke_admin', + conditions: ['property:local', 'state:activated'], + }, + { + separator: true, + }, + { + check: FORCE_NSFW, + label: 'user_card.admin_menu.force_nsfw', + }, + { + check: STRIP_MEDIA, + label: 'user_card.admin_menu.strip_media', + }, + { + check: FORCE_UNLISTED, + label: 'user_card.admin_menu.force_unlisted', + }, + { + check: SANDBOX, + label: 'user_card.admin_menu.sandbox', + }, + { + check: DISABLE_ANY_SUBSCRIPTION, + conditions: ['property:local'], + label: 'user_card.admin_menu.disable_any_subscription', + }, + { + check: DISABLE_REMOTE_SUBSCRIPTION, + conditions: ['property:local'], + label: 'user_card.admin_menu.disable_remote_subscription', + }, + { + check: QUARANTINE, + conditions: ['property:local'], + label: 'user_card.admin_menu.quarantine', + }, + { + separator: true, + }, + { + check: 'action:delete', + label: 'user_card.admin_menu.delete_account', + }, +] const ModerationTools = { - props: ['user'], + props: { + users: { + type: Array, + required: true, + }, + }, + created() { + if (this.users.length !== 1) return + useAdminSettingsStore().getUserData({ user: this.users[0] }) + }, data() { return { - tags: { - FORCE_NSFW, - STRIP_MEDIA, - FORCE_UNLISTED, - DISABLE_REMOTE_SUBSCRIPTION, - DISABLE_ANY_SUBSCRIPTION, - SANDBOX, - QUARANTINE, - }, - showDeleteUserDialog: false, - toggled: false, + open: false, + confirmDialogShow: false, + confirmDialogTitle: null, + confirmDialogContent: null, + confirmDialogConfirm: null, + confirmDialogAction: null, + confirmDialogGroup: null, + confirmDialogName: null, } }, components: { - DialogModal, + ConfirmModal, Popover, }, computed: { + ready() { + return this.users.every((u) => u.adminData) + }, + entries() { + return ENTRIES.map(({ check, label, separator, conditions }) => { + if (separator) return 'separator' + const [, negateToken, group, name] = + /^([!~]?)([a-z-_]+):([a-z-_]+)$/.exec(check) + + const hasTag = this.tagsSet.has(`${group}:${name}`) + const noTag = this.tagsSet.has(`!${group}:${name}`) + const maybeTag = this.tagsSet.has(`~${group}:${name}`) + + // We are checking for condition to show element, i.e. only show "activate" if user is "deactivated" + const checkNegated = negateToken === '!' || negateToken === '~' + + // Naturally, new value should also be the same + const value = checkNegated + + const action = (() => { + switch (group) { + case 'rights': + return () => this.setRight(name, value) + case 'state': + return () => this.setStatus(name, value) + case 'mrf_tag': + return () => this.setTag(`${group}:${name}`, noTag) + case 'action': { + switch (name) { + case 'delete': + return () => this.deleteUsers() + case 'resend_confirmation': + return () => this.resendConfirmationEmail() + case 'disable_mfa': + return () => this.disableMFA() + case 'statuses': + return () => + this.$router.push(`/users/\$${this.users[0].id}/admin_view`) + case 'require_password_change': + return () => this.requirePasswordChange() + default: + throw new Error(`Unknown action group: ${name}`) + } + } + default: + throw new Error(`Unknown moderation group: ${group}`) + } + })() + + let checkboxClass = '' + if (maybeTag) { + checkboxClass = 'menu-checkbox-indeterminate' + } else if (hasTag) { + checkboxClass = 'menu-checkbox-checked' + } + + return { + check, + negateToken, + checkbox: group === 'mrf_tag', + checkboxClass, + conditions, + group, + name, + action, + label, + value, + } + }) + .filter((entry) => { + if (entry === 'separator') return true + const { group, name, value, conditions } = entry + + if (conditions) { + // Checking that all items match positive criteria + const positive = conditions.every((condition) => + this.totalSet.has(condition), + ) + // Checking that there are no items that don't match criteria + const negative = conditions.some((condition) => + this.totalSet.has('!' + condition), + ) + if (!(positive && !negative)) return false + } + + switch (group) { + case 'action': + if (name === 'statuses') return this.privileged('users_read') + else return true + case 'rights': + return this.canGrantRole(name, value) + case 'state': + return this.canChangeState(name, value) + case 'mrf_tag': + return this.canUseTagPolicy + default: { + throw new Error(`Unknown moderation group: ${group}`) + } + } + }) + .reduce((acc, entry, index) => { + // Removing any double separators as well + // as separators at very end and bery beginning + if (entry === 'separator') { + if ( + acc.length === 0 || + last(acc) === 'separator' || + index === ENTRIES.length - 1 + ) { + return acc + } + } + return [...acc, entry] + }, []) + }, + rightsSet() { + return this.users.reduce((acc, user) => { + if (user.rights.admin) { + acc.add('rights:admin') + } else { + acc.add('!rights:admin') + } + if (user.rights.moderator) { + acc.add('rights:moderator') + } else { + acc.add('!rights:moderator') + } + return acc + }, new Set()) + }, + stateSet() { + return this.users.reduce((acc, user) => { + if (!user.deactivated) { + acc.add('state:activated') + } else { + acc.add('!state:activated') + } + if (user.adminData?.is_confirmed) { + acc.add('state:confirmed') + } else { + acc.add('!state:confirmed') + } + if (user.adminData?.is_approved) { + acc.add('state:approved') + } else { + acc.add('!state:approved') + } + if (user.adminData?.is_suggested) { + acc.add('state:suggested') + } else { + acc.add('!state:suggested') + } + return acc + }, new Set()) + }, tagsSet() { - return new Set(this.user.tags) + const present = new Set() + const missing = new Set() + + this.users.forEach((user) => { + TAGS.forEach((tag) => { + if (user.tags.has(tag)) { + present.add(tag) + } else { + missing.add(tag) + } + }) + }) + + const result = new Set() + + // Each tag can have three states for given group of users + TAGS.forEach((tag) => { + if (present.has(tag) && missing.has(tag)) { + // Some users have tag, some don't: "~tag" + result.add(`~${tag}`) + } else if (missing.has(tag)) { + // No users have tag: "!tag" + result.add(`!${tag}`) + } else { + // All users have tag: "tag" + result.add(tag) + } + }) + + return result }, - canGrantRole() { - return ( - this.user.is_local && - !this.user.deactivated && - this.$store.state.users.currentUser.role === 'admin' - ) + propertySet() { + return this.users.reduce((acc, user) => { + if (user.is_local) { + acc.add('property:local') + } else { + acc.add('!property:local') + } + return acc + }, new Set()) }, - canChangeActivationState() { - return this.privileged('users_manage_activation_state') + disabled() { + return !this.ready || this.users.length === 0 + }, + totalSet() { + return new Set([ + ...this.rightsSet, + ...this.stateSet, + ...this.tagsSet, + ...this.propertySet, + `count:${this.users.length}`, + ]) }, canDeleteAccount() { return this.privileged('users_delete') @@ -61,89 +404,266 @@ const ModerationTools = { this.privileged('users_manage_tags') ) }, + isAdmin() { + this.$store.state.users.currentUser.role === 'admin' + }, }, methods: { - hasTag(tagName) { - return this.tagsSet.has(tagName) + canGrantRole(name, value) { + const setEntry = `${value ? '!' : ''}rights:${name}` + + return this.isAdmin && this.totalSet.has(setEntry) + }, + canChangeState(name, value) { + const setEntry = `${value ? '!' : ''}state:${name}` + const privilege = (() => { + switch (name) { + case 'activated': + return 'users_manage_activation_state' + case 'approved': + return 'users_manage_invites' + case 'confirmed': + return 'users_manage_credentials' + default: + return null + } + })() + + return this.privileged(privilege) && this.totalSet.has(setEntry) + }, + doConfirmDialogAction() { + if (typeof this.confirmDialogAction !== 'function') { + console.error('Confirm Dialog action is not a function!!') + } + + this.confirmDialogAction() + this.clearConfirmDialog() + }, + clearConfirmDialog() { + this.confirmDialogShow = false + this.confirmDialogTitle = null + this.confirmDialogContent = null + this.confirmDialogContent2 = null + this.confirmDialogDanger = false + this.confirmDialogConfirm = null + this.confirmDialogAction = null + this.confirmDialogGroup = null + this.confirmDialogName = null }, privileged(privilege) { - return this.$store.state.users.currentUser.privileges.includes(privilege) + if (this.isAdmin) return true + return this.$store.state.users.currentUser.privileges.has(privilege) }, - toggleTag(tag) { - const store = this.$store - if (this.tagsSet.has(tag)) { - store.state.api.backendInteractor - .untagUser({ user: this.user, tag }) - .then((response) => { - if (!response.ok) { - return - } - store.commit('untagUser', { user: this.user, tag }) - }) - } else { - store.state.api.backendInteractor - .tagUser({ user: this.user, tag }) - .then((response) => { - if (!response.ok) { - return - } - store.commit('tagUser', { user: this.user, tag }) - }) - } - }, - toggleRight(right) { - const store = this.$store - if (this.user.rights[right]) { - store.state.api.backendInteractor - .deleteRight({ user: this.user, right }) - .then((response) => { - if (!response.ok) { - return - } - store.commit('updateRight', { - user: this.user, - right, - value: false, - }) - }) - } else { - store.state.api.backendInteractor - .addRight({ user: this.user, right }) - .then((response) => { - if (!response.ok) { - return - } - store.commit('updateRight', { user: this.user, right, value: true }) - }) - } - }, - toggleActivationStatus() { - this.$store.dispatch('toggleActivationStatus', { user: this.user }) - }, - deleteUserDialog(show) { - this.showDeleteUserDialog = show - }, - deleteUser() { - const store = this.$store - const user = this.user - const { id, name } = user - store.state.api.backendInteractor.deleteUser({ user }).then(() => { - this.$store.dispatch( - 'markStatusesAsDeleted', - (status) => user.id === status.user.id, - ) - const isProfile = - this.$route.name === 'external-user-profile' || - this.$route.name === 'user-profile' - const isTargetUser = - this.$route.params.name === name || this.$route.params.id === id - if (isProfile && isTargetUser) { - window.history.back() - } + setTag(tag, value) { + useAdminSettingsStore().setUsersTags({ + users: this.users, + value, + tags: [tag], }) }, - setToggled(value) { - this.toggled = value + setRight(right, value) { + useAdminSettingsStore().setUsersRight({ users: this.users, value, right }) + }, + setStatus(name, value) { + const noun = (() => { + switch (name) { + case 'activated': + return 'Activation' + case 'confirmed': + return 'Confirmation' + case 'approved': + return 'Approval' + case 'suggested': + return 'Suggestion' + } + })() + + useAdminSettingsStore()[`setUsers${noun}Status`]({ + users: this.users, + value, + }) + }, + resendConfirmationEmail() { + useAdminSettingsStore().resendConfirmationEmail({ users: this.users }) + }, + requirePasswordChange() { + useAdminSettingsStore().requirePasswordChange({ users: this.users }) + }, + disableMFA() { + this.users.forEach((user) => { + useAdminSettingsStore().disableMFA({ user }) + }) + }, + deleteUsers() { + const { id, name } = this.users[0] + + useAdminSettingsStore() + .deleteUsers({ users: this.users }) + .then((userIds) => { + if (userIds.length > 1) return + + const isProfile = + this.$route.name === 'external-user-profile' || + this.$route.name === 'user-profile' + const isTargetUser = + this.$route.params.name === name || this.$route.params.id === id + + if (isProfile && isTargetUser) { + window.history.back() + } + }) + }, + setOpen(value) { + this.open = value + }, + maybeShowConfirm(close, { group, name, action, value }) { + close() + this.confirmDialogName = name + this.confirmDialogGroup = group + this.confirmDialogAction = () => action() + + switch (group) { + case 'action': { + switch (name) { + case 'delete': { + this.confirmDialogShow = true + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.delete_title', + ) + this.confirmDialogDanger = true + this.confirmDialogContent = + 'user_card.admin_menu.confirm_modal.delete_content' + this.confirmDialogContent2 = + 'user_card.admin_menu.confirm_modal.delete_content_2' + this.confirmDialogConfirm = this.$t( + 'user_card.admin_menu.confirm_modal.delete', + ) + break + } + case 'resend_confirmation': { + this.confirmDialogShow = true + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.resend_confirmation_title', + ) + this.confirmDialogContent = + 'user_card.admin_menu.confirm_modal.resend_confirmation_content' + this.confirmDialogConfirm = this.$t( + 'user_card.admin_menu.confirm_modal.send', + ) + break + } + case 'disable_mfa': { + this.confirmDialogShow = true + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.disable_mfa_title', + ) + this.confirmDialogContent = + 'user_card.admin_menu.confirm_modal.disable_mfa_content' + this.confirmDialogConfirm = this.$t('settings.confirm') + break + } + case 'require_password_change': { + this.confirmDialogShow = true + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.require_password_change_title', + ) + this.confirmDialogContent = + 'user_card.admin_menu.confirm_modal.require_password_change_content' + this.confirmDialogConfirm = this.$t('settings.confirm') + break + } + } + break + } + case 'state': { + switch (name) { + case 'activated': { + this.confirmDialogShow = true + + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.activate_title', + ) + this.confirmDialogContent = value + ? 'user_card.admin_menu.confirm_modal.activate_content' + : 'user_card.admin_menu.confirm_modal.deactivate_content' + this.confirmDialogConfirm = value + ? this.$t('user_card.admin_menu.confirm_modal.activate') + : this.$t('user_card.admin_menu.confirm_modal.deactivate') + break + } + // Confirmation and Approval statuses cannot be revokedn(no API) + case 'confirmed': { + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.confirm_title', + ) + this.confirmDialogContent = //value + /*?*/ 'user_card.admin_menu.confirm_modal.confirm_content' + //: 'user_card.admin_menu.confirm_modal.confirm_revoke_content' + this.confirmDialogConfirm = value + /*?*/ this.$t('user_card.admin_menu.confirm_modal.confirm') + //: this.$t('user_card.admin_menu.confirm_modal.revoke') + break + } + case 'approved': { + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.approval_title', + ) + this.confirmDialogContent = //value + /*?*/ 'user_card.admin_menu.confirm_modal.approval_content' + //: 'user_card.admin_menu.confirm_modal.approval_revoke_content' + this.confirmDialogConfirm = value + /*?*/ this.$t('user_card.admin_menu.confirm_modal.approve') + //: this.$t('user_card.admin_menu.confirm_modal.revoke') + break + } + case 'suggested': { + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.suggest_title', + ) + this.confirmDialogContent = value + ? 'user_card.admin_menu.confirm_modal.add_suggest_content' + : 'user_card.admin_menu.confirm_modal.remove_suggest_content' + this.confirmDialogConfirm = value + ? this.$t('user_card.admin_menu.confirm_modal.add') + : this.$t('user_card.admin_menu.confirm_modal.remove') + break + } + } + break + } + case 'rights': { + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.confirm_modal.user_rights_title', + ) + this.confirmDialogContent = value + ? 'user_card.admin_menu.confirm_modal.grant_role_content' + : 'user_card.admin_menu.confirm_modal.revoke_role_content' + this.confirmDialogConfirm = value + ? this.$t('user_card.admin_menu.confirm_modal.grant') + : this.$t('user_card.admin_menu.confirm_modal.revoke') + break + } + case 'mrf_tag': { + this.confirmDialogTitle = this.$t( + 'user_card.admin_menu.user_tag_title', + ) + this.confirmDialogContent = value + ? 'user_card.admin_menu.confirm_modal.assign_tag_content' + : 'user_card.admin_menu.confirm_modal.unassign_tag_content' + this.confirmDialogConfirm = value + ? this.$t('user_card.admin_menu.confirm_modal.assign') + : this.$t('user_card.admin_menu.confirm_modal.unassign') + break + } + } + + if (this.users.length > 1) { + this.confirmDialogShow = true + } + + if (!this.confirmDialogShow) { + this.doConfirmDialogAction() + } }, }, } diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index da33b89dc..4da0be0ee 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -3,154 +3,37 @@ - - - diff --git a/src/components/settings_modal/admin_tabs/admin_user_card.js b/src/components/settings_modal/admin_tabs/admin_user_card.js new file mode 100644 index 000000000..ba0f0e9f7 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/admin_user_card.js @@ -0,0 +1,30 @@ +import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' +import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue' + +const AdminUserCard = { + props: { + userId: { + type: String, + }, + }, + components: { + BasicUserCard, + ModerationTools, + }, + computed: { + user() { + return this.$store.getters.findUser(this.userId) + }, + isAdmin() { + return this.user.rights.admin + }, + isModerator() { + return this.user.rights.moderator + }, + isActivated() { + return !this.user.deactivated + }, + }, +} + +export default AdminUserCard diff --git a/src/components/settings_modal/admin_tabs/admin_user_card.scss b/src/components/settings_modal/admin_tabs/admin_user_card.scss new file mode 100644 index 000000000..a1f685aec --- /dev/null +++ b/src/components/settings_modal/admin_tabs/admin_user_card.scss @@ -0,0 +1,29 @@ +.AdminUserCard { + details { + white-space: normal; + + summary { + font-weight: bold; + } + + span { + display: block; + max-height: 6.5em; + overflow-y: auto; + resize: vertical; + } + } + + .right-side { + align-items: baseline; + justify-content: end; + display: flex; + flex-wrap: wrap; + gap: 0.5em; + margin-top: 0.5em; + + .alert { + margin: 0; + } + } +} diff --git a/src/components/settings_modal/admin_tabs/admin_user_card.vue b/src/components/settings_modal/admin_tabs/admin_user_card.vue new file mode 100644 index 000000000..00405dfa1 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/admin_user_card.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/components/settings_modal/admin_tabs/auth_tab.js b/src/components/settings_modal/admin_tabs/auth_tab.js index 627150587..7ccae2ca0 100644 --- a/src/components/settings_modal/admin_tabs/auth_tab.js +++ b/src/components/settings_modal/admin_tabs/auth_tab.js @@ -9,6 +9,8 @@ import SharedComputedObject from '../helpers/shared_computed_object.js' import StringSetting from '../helpers/string_setting.vue' import TupleSetting from '../helpers/tuple_setting.vue' +import { useAdminSettingsStore } from 'src/stores/admin_settings.js' + const AuthTab = { provide() { return { @@ -30,9 +32,7 @@ const AuthTab = { computed: { ...SharedComputedObject(), LDAPEnabled() { - return this.$store.state.adminSettings.draft[':pleroma'][':ldap'][ - ':enabled' - ] + return useAdminSettingsStore().draft[':pleroma'][':ldap'][':enabled'] }, }, } diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js index 115ef519b..56361587d 100644 --- a/src/components/settings_modal/admin_tabs/emoji_tab.js +++ b/src/components/settings_modal/admin_tabs/emoji_tab.js @@ -1,9 +1,9 @@ import Checkbox from 'components/checkbox/checkbox.vue' -import ConfirmModal from 'components/confirm_modal/confirm_modal.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 { assign, clone } from 'lodash' +import { clone } from 'lodash' +import { defineAsyncComponent } from 'vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue' @@ -11,6 +11,8 @@ import ModifiedIndicator from '../helpers/modified_indicator.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import StringSetting from '../helpers/string_setting.vue' +import { useAdminSettingsStore } from 'src/stores/admin_settings.js' +import { useEmojiStore } from 'src/stores/emoji.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInterfaceStore } from 'src/stores/interface.js' @@ -32,7 +34,10 @@ const EmojiTab = { StillImage, Select, Popover, - ConfirmModal, + ConfirmModal: defineAsyncComponent( + () => import('src/components/confirm_modal/confirm_modal.vue'), + ), + ModifiedIndicator, EmojiEditingPopover, }, @@ -94,10 +99,10 @@ const EmojiTab = { methods: { reloadEmoji() { - this.$store.state.api.backendInteractor.reloadEmoji() + useAdminSettingsStore().reloadEmoji() }, importFromFS() { - this.$store.state.api.backendInteractor.importEmojiFromFS() + useAdminSettingsStore().importEmojiFromFS() }, emojiAddr(name) { if (this.pack.remote !== undefined) { @@ -109,7 +114,7 @@ const EmojiTab = { }, createEmojiPack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .createEmojiPack({ name: this.newPackName }) .then((resp) => resp.json()) .then((resp) => { @@ -126,7 +131,7 @@ const EmojiTab = { }) }, deleteEmojiPack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .deleteEmojiPack({ name: this.packName }) .then((resp) => resp.json()) .then((resp) => { @@ -153,7 +158,7 @@ const EmojiTab = { return edited !== def }, savePackMetadata() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }) .then((resp) => resp.json()) .then((resp) => { @@ -174,63 +179,25 @@ const EmojiTab = { this.sortPackFiles(packName) }, - loadPacksPaginated(listFunction) { - const pageSize = 25 - const allPacks = {} - - return listFunction({ - instance: this.remotePackInstance, - page: 1, - pageSize: 0, - }) - .then((data) => data.json()) - .then((data) => { - if (data.error !== undefined) { - return Promise.reject(data.error) - } - - let resultingPromise = Promise.resolve({}) - for (let i = 0; i < Math.ceil(data.count / pageSize); i++) { - resultingPromise = resultingPromise - .then(() => - listFunction({ - instance: this.remotePackInstance, - page: i, - pageSize, - }), - ) - .then((data) => data.json()) - .then((pageData) => { - if (pageData.error !== undefined) { - return Promise.reject(pageData.error) - } - - assign(allPacks, pageData.packs) - }) - } - - return resultingPromise - }) - .then(() => allPacks) - .catch((data) => { - this.displayError(data) - }) - }, - refreshPackList() { - this.loadPacksPaginated( - this.$store.state.api.backendInteractor.listEmojiPacks, - ).then((allPacks) => { - this.knownLocalPacks = allPacks - for (const name of Object.keys(this.knownLocalPacks)) { - this.sortPackFiles(name) - } - }) + useEmojiStore() + .getAdminPacks( + this.remotePackInstance, + useAdminSettingsStore().listEmojiPacks, + ) + .then((allPacks) => { + this.knownLocalPacks = allPacks + for (const name of Object.keys(this.knownLocalPacks)) { + this.sortPackFiles(name) + } + }) }, listRemotePacks() { - this.loadPacksPaginated( - this.$store.state.api.backendInteractor.listRemoteEmojiPacks, - ) + useEmojiStore() + .getAdminPacks( + this.remotePackInstance, + useAdminSettingsStore().listRemoteEmojiPacks, + ) .then((allPacks) => { let inst = this.remotePackInstance if (!inst.startsWith('http')) { @@ -260,7 +227,7 @@ const EmojiTab = { this.remotePackDownloadAs = this.pack.remote.baseName } - this.$store.state.api.backendInteractor + useAdminSettingsStore() .downloadRemoteEmojiPack({ instance: this.pack.remote.instance, packName: this.pack.remote.baseName, @@ -281,7 +248,7 @@ const EmojiTab = { }) }, downloadRemoteURLPack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .downloadRemoteEmojiPackZIP({ url: this.remotePackURL, packName: this.newPackName, @@ -302,7 +269,7 @@ const EmojiTab = { }) }, downloadRemoteFilePack() { - this.$store.state.api.backendInteractor + useAdminSettingsStore() .downloadRemoteEmojiPackZIP({ file: this.remotePackFile[0], packName: this.newPackName, diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss index 2f3b5aa40..fd5b53caa 100644 --- a/src/components/settings_modal/admin_tabs/emoji_tab.scss +++ b/src/components/settings_modal/admin_tabs/emoji_tab.scss @@ -3,6 +3,11 @@ margin: 0.5em 2em; } + .setting-section { + margin-left: 0.5em; + margin-right: 0.5em; + } + .toolbar { display: flex; flex-wrap: wrap; diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.vue b/src/components/settings_modal/admin_tabs/emoji_tab.vue index 038f522b1..b118e0c52 100644 --- a/src/components/settings_modal/admin_tabs/emoji_tab.vue +++ b/src/components/settings_modal/admin_tabs/emoji_tab.vue @@ -56,18 +56,14 @@ + - + diff --git a/src/components/user_profile/user_profile_admin_view.js b/src/components/user_profile/user_profile_admin_view.js new file mode 100644 index 000000000..da2c6c322 --- /dev/null +++ b/src/components/user_profile/user_profile_admin_view.js @@ -0,0 +1,68 @@ +import Checkbox from 'src/components/checkbox/checkbox.vue' +import List from 'src/components/list/list.vue' +import Status from 'src/components/status/status.vue' +import UserCard from 'src/components/user_card/user_card.vue' + +import { useAdminSettingsStore } from 'src/stores/admin_settings.js' +import { useInterfaceStore } from 'src/stores/interface.js' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add(faCircleNotch) + +const UserProfileAdminView = { + data() { + return { + godmode: false, + showReblogs: false, + } + }, + created() { + this.$store.dispatch('fetchUserIfMissing', this.userId) + useInterfaceStore().setForeignProfileBackground(this.user?.background_image) + }, + updated() { + useInterfaceStore().setForeignProfileBackground(this.user?.background_image) + }, + unmounted() { + useInterfaceStore().setForeignProfileBackground(null) + }, + computed: { + fetchOptions() { + return { + pageSize: 20, + godmode: this.godmode, + id: this.userId, + withReblogs: this.showReblogs, + } + }, + user() { + return this.$store.getters.findUser(this.userId) + }, + userId() { + return this.$route.params.id + }, + }, + methods: { + fetchStatuses(page) { + return useAdminSettingsStore().fetchStatuses({ + ...this.fetchOptions, + page, + }) + }, + }, + components: { + UserCard, + List, + Status, + Checkbox, + }, + watch: { + fetchOptions() { + this.$refs.list.reset() + }, + }, +} + +export default UserProfileAdminView diff --git a/src/components/user_profile/user_profile_admin_view.vue b/src/components/user_profile/user_profile_admin_view.vue new file mode 100644 index 000000000..e5118f474 --- /dev/null +++ b/src/components/user_profile/user_profile_admin_view.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js index 896167df0..017a17efa 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.js +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -1,14 +1,17 @@ -import Checkbox from '../checkbox/checkbox.vue' -import List from '../list/list.vue' -import Modal from '../modal/modal.vue' -import Status from '../status/status.vue' -import UserLink from '../user_link/user_link.vue' +import { mapState } from 'pinia' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import List from 'src/components/list/list.vue' +import Modal from 'src/components/modal/modal.vue' +import UserLink from 'src/components/user_link/user_link.vue' + +import { useOAuthStore } from 'src/stores/oauth.js' import { useReportsStore } from 'src/stores/reports.js' +import { reportUser } from 'src/api/user.js' + const UserReportingModal = { components: { - Status, List, Checkbox, Modal, @@ -18,15 +21,12 @@ const UserReportingModal = { return { comment: '', forward: false, - statusIdsToReport: [], + statusIdsToReport: new Set(), processing: false, error: false, } }, computed: { - reportModal() { - return useReportsStore().reportModal - }, isLoggedIn() { return !!this.$store.state.users.currentUser }, @@ -45,31 +45,26 @@ const UserReportingModal = { this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1) ) }, - statuses() { - return this.reportModal.statuses - }, - preTickedIds() { - return this.reportModal.preTickedIds - }, + ...mapState(useReportsStore, ['reportModal']), }, watch: { userId: 'resetState', - preTickedIds(newValue) { - this.statusIdsToReport = newValue - }, }, methods: { resetState() { // Reset state this.comment = '' this.forward = false - this.statusIdsToReport = this.preTickedIds + this.statusIdsToReport = new Set(this.reportModal.preTickedIds) this.processing = false this.error = false }, closeModal() { useReportsStore().closeUserReportingModal() }, + onListSelect(selected) { + this.statusIdsToReport = selected + }, reportUser() { this.processing = true this.error = false @@ -77,10 +72,10 @@ const UserReportingModal = { userId: this.userId, comment: this.comment, forward: this.forward, - statusIds: this.statusIdsToReport, + statusIds: [...this.statusIdsToReport], + credentials: useOAuthStore().token, } - this.$store.state.api.backendInteractor - .reportUser({ ...params }) + reportUser({ ...params }) .then(() => { this.processing = false this.resetState() @@ -94,23 +89,6 @@ const UserReportingModal = { clearError() { this.error = false }, - isChecked(statusId) { - return this.statusIdsToReport.indexOf(statusId) !== -1 - }, - toggleStatus(checked, statusId) { - if (checked === this.isChecked(statusId)) { - return - } - - if (checked) { - this.statusIdsToReport.push(statusId) - } else { - this.statusIdsToReport.splice( - this.statusIdsToReport.indexOf(statusId), - 1, - ) - } - }, resize(e) { const target = e.target || e if (!(target instanceof window.Element)) { diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index aa5704a97..a028ebeb6 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.vue +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -51,19 +51,18 @@
- +
@@ -136,20 +135,6 @@ overflow-y: auto; } - &-sitem { - display: flex; - justify-content: space-between; - - /* TODO cleanup this */ - > .Status { - flex: 1; - } - - > .checkbox { - margin: 0.75em; - } - } - @media all and (width >= 801px) { .panel-body { flex-direction: row; diff --git a/src/components/user_timed_filter_modal/user_timed_filter_modal.js b/src/components/user_timed_filter_modal/user_timed_filter_modal.js index 528f92ddf..f0c150999 100644 --- a/src/components/user_timed_filter_modal/user_timed_filter_modal.js +++ b/src/components/user_timed_filter_modal/user_timed_filter_modal.js @@ -1,5 +1,6 @@ +import { defineAsyncComponent } from 'vue' + import Checkbox from 'src/components/checkbox/checkbox.vue' -import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' import Select from 'src/components/select/select.vue' import { useMergedConfigStore } from 'src/stores/merged_config.js' @@ -36,7 +37,10 @@ const UserTimedFilterModal = { } }, components: { - ConfirmModal, + ConfirmModal: defineAsyncComponent( + () => import('src/components/confirm_modal/confirm_modal.vue'), + ), + Select, Checkbox, }, diff --git a/src/components/user_timed_filter_modal/user_timed_filter_modal.scss b/src/components/user_timed_filter_modal/user_timed_filter_modal.scss index 24388868f..961656442 100644 --- a/src/components/user_timed_filter_modal/user_timed_filter_modal.scss +++ b/src/components/user_timed_filter_modal/user_timed_filter_modal.scss @@ -10,4 +10,15 @@ .footer-left-checkbox { width: max-content; } + + .expirationTime { + display: inline-flex; + white-space: nowrap; + padding: 0.5em; + } + + .dialog-modal-content .checkbox { + vertical-align: middle; + padding: 0.5em; + } } diff --git a/src/components/user_timed_filter_modal/user_timed_filter_modal.vue b/src/components/user_timed_filter_modal/user_timed_filter_modal.vue index 91c05ad77..f9b23076c 100644 --- a/src/components/user_timed_filter_modal/user_timed_filter_modal.vue +++ b/src/components/user_timed_filter_modal/user_timed_filter_modal.vue @@ -1,5 +1,5 @@ diff --git a/src/components/who_to_follow/who_to_follow.js b/src/components/who_to_follow/who_to_follow.js index 720b15041..28b63c973 100644 --- a/src/components/who_to_follow/who_to_follow.js +++ b/src/components/who_to_follow/who_to_follow.js @@ -1,7 +1,8 @@ -import apiService from '../../services/api/api.service.js' -import FollowCard from '../follow_card/follow_card.vue' +import FollowCard from 'src/components/follow_card/follow_card.vue' -import { useInstanceStore } from 'src/stores/instance.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchUser, suggestions } from 'src/api/public.js' const WhoToFollow = { components: { @@ -17,21 +18,22 @@ const WhoToFollow = { }, methods: { showWhoToFollow(reply) { - reply.forEach((i) => { - this.$store.state.api.backendInteractor - .fetchUser({ id: i.acct }) - .then((externalUser) => { - if (!externalUser.error) { - this.$store.commit('addNewUsers', [externalUser]) - this.users.push(externalUser) - } - }) + reply.forEach(({ id }) => { + fetchUser({ + id, + credentials: useOAuthStore().token, + }).then(({ data: externalUser }) => { + if (!externalUser.error) { + this.$store.commit('addNewUsers', [externalUser]) + this.users.push(externalUser) + } + }) }) }, getWhoToFollow() { - const credentials = this.$store.state.users.currentUser.credentials + const credentials = useOAuthStore().token if (credentials) { - apiService.suggestions({ credentials }).then((reply) => { + suggestions({ credentials }).then(({ data: reply }) => { this.showWhoToFollow(reply) }) } 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 cd0524ecf..7a65ba39d 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,10 +1,10 @@ import { shuffle } from 'lodash' -import apiService from '../../services/api/api.service.js' - import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' +import { useOAuthStore } from 'src/stores/oauth.js' +import { fetchUser, suggestions } from 'src/api/public.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' function showWhoToFollow(panel, reply) { @@ -18,14 +18,15 @@ function showWhoToFollow(panel, reply) { toFollow.img = img toFollow.name = name - panel.$store.state.api.backendInteractor - .fetchUser({ id: name }) - .then((externalUser) => { - if (!externalUser.error) { - panel.$store.commit('addNewUsers', [externalUser]) - toFollow.id = externalUser.id - } - }) + fetchUser({ + id: name, + credentials: useOAuthStore().token, + }).then(({ data: externalUser }) => { + if (!externalUser.error) { + panel.$store.commit('addNewUsers', [externalUser]) + toFollow.id = externalUser.id + } + }) }) } @@ -35,7 +36,7 @@ function getWhoToFollow(panel) { panel.usersToFollow.forEach((toFollow) => { toFollow.name = 'Loading...' }) - apiService.suggestions({ credentials }).then((reply) => { + suggestions({ credentials }).then(({ data: reply }) => { showWhoToFollow(panel, reply) }) } diff --git a/src/hocs/with_load_more/with_load_more.jsx b/src/hocs/with_load_more/with_load_more.jsx deleted file mode 100644 index 9cb9d78c4..000000000 --- a/src/hocs/with_load_more/with_load_more.jsx +++ /dev/null @@ -1,119 +0,0 @@ -// eslint-disable-next-line no-unused - -import isEmpty from 'lodash/isEmpty' -import { h } from 'vue' - -import { getComponentProps } from '../../services/component_utils/component_utils' -import './with_load_more.scss' - -import { library } from '@fortawesome/fontawesome-svg-core' -import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' - -library.add(faCircleNotch) - -const withLoadMore = - ({ - fetch, // function to fetch entries and return a promise - select, // function to select data from store - unmounted, // function called at "destroyed" lifecycle - childPropName = 'entries', // name of the prop to be passed into the wrapped component - additionalPropNames = [], // additional prop name list of the wrapper component - }) => - (WrappedComponent) => { - const originalProps = Object.keys(getComponentProps(WrappedComponent)) - const props = originalProps - .filter((v) => v !== childPropName) - .concat(additionalPropNames) - - return { - props, - data() { - return { - loading: false, - bottomedOut: false, - error: false, - entries: [], - } - }, - created() { - window.addEventListener('scroll', this.scrollLoad) - if (this.entries.length === 0) { - this.fetchEntries() - } - }, - unmounted() { - window.removeEventListener('scroll', this.scrollLoad) - unmounted && unmounted(this.$props, this.$store) - }, - methods: { - // Entries is not a computed because computed can't track the dynamic - // selector for changes and won't trigger after fetch. - updateEntries() { - this.entries = select(this.$props, this.$store) || [] - }, - fetchEntries() { - if (!this.loading) { - this.loading = true - this.error = false - fetch(this.$props, this.$store) - .then((newEntries) => { - this.loading = false - this.bottomedOut = isEmpty(newEntries) - }) - .catch(() => { - this.loading = false - this.error = true - }) - .finally(() => { - this.updateEntries() - }) - } - }, - scrollLoad(e) { - const bodyBRect = document.body.getBoundingClientRect() - const height = Math.max(bodyBRect.height, -bodyBRect.y) - if ( - this.loading === false && - this.bottomedOut === false && - this.$el.offsetHeight > 0 && - window.innerHeight + window.pageYOffset >= height - 750 - ) { - this.fetchEntries() - } - }, - }, - render() { - const props = { - ...this.$props, - [childPropName]: this.entries, - } - const children = this.$slots - return ( -
- {children} - -
- ) - }, - } - } - -export default withLoadMore diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss deleted file mode 100644 index 6b007ac2e..000000000 --- a/src/hocs/with_load_more/with_load_more.scss +++ /dev/null @@ -1,16 +0,0 @@ -.with-load-more { - &-footer { - padding: 0.9em; - text-align: center; - border-top: 1px solid; - border-top-color: var(--border); - - .error { - font-size: 1rem; - } - - a { - cursor: pointer; - } - } -} diff --git a/src/hocs/with_subscription/with_subscription.jsx b/src/hocs/with_subscription/with_subscription.jsx deleted file mode 100644 index 198be6862..000000000 --- a/src/hocs/with_subscription/with_subscription.jsx +++ /dev/null @@ -1,94 +0,0 @@ -// eslint-disable-next-line no-unused - -import isEmpty from 'lodash/isEmpty' -import { h } from 'vue' - -import { getComponentProps } from '../../services/component_utils/component_utils' -import './with_subscription.scss' - -import { library } from '@fortawesome/fontawesome-svg-core' -import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' - -library.add(faCircleNotch) - -const withSubscription = - ({ - fetch, // function to fetch entries and return a promise - select, // function to select data from store - childPropName = 'content', // name of the prop to be passed into the wrapped component - additionalPropNames = [], // additional prop name list of the wrapper component - }) => - (WrappedComponent) => { - const originalProps = Object.keys(getComponentProps(WrappedComponent)) - const props = originalProps - .filter((v) => v !== childPropName) - .concat(additionalPropNames) - - return { - props: [ - ...props, - 'refresh', // boolean saying to force-fetch data whenever created - ], - data() { - return { - loading: false, - error: false, - } - }, - computed: { - fetchedData() { - return select(this.$props, this.$store) - }, - }, - created() { - if (this.refresh || isEmpty(this.fetchedData)) { - this.fetchData() - } - }, - methods: { - fetchData() { - if (!this.loading) { - this.loading = true - this.error = false - fetch(this.$props, this.$store) - .then(() => { - this.loading = false - }) - .catch(() => { - this.error = true - this.loading = false - }) - } - }, - }, - render() { - if (!this.error && !this.loading) { - const props = { - ...this.$props, - [childPropName]: this.fetchedData, - } - const children = this.$slots - return ( -
- {children} -
- ) - } else { - return ( -
- {this.error ? ( - - {this.$t('general.generic_error')} - - ) : ( - - )} -
- ) - } - }, - } - } - -export default withSubscription diff --git a/src/hocs/with_subscription/with_subscription.scss b/src/hocs/with_subscription/with_subscription.scss deleted file mode 100644 index 8c0af336c..000000000 --- a/src/hocs/with_subscription/with_subscription.scss +++ /dev/null @@ -1,10 +0,0 @@ -.with-subscription { - &-loading { - padding: 0.7em; - text-align: center; - - .error { - font-size: 1rem; - } - } -} diff --git a/src/i18n/en.json b/src/i18n/en.json index 9b5a7439d..4a35b48f5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -86,10 +86,15 @@ "apply": "Apply", "submit": "Submit", "more": "More", + "no_more": "No more items", "loading": "Loading…", "generic_error": "An error occured", "generic_error_message": "An error occured: {0}", + "generic_error_details": "Technical info:", "error_retry": "Please try again", + "refresh_required": "Refresh required", + "refresh_required_content": "Failed to load UI code. Most likely frontend was updated on server, you'll need to refresh the page.", + "refresh_required_refresh": "Refresh page", "retry": "Try again", "optional": "optional", "show_more": "Show more", @@ -105,6 +110,9 @@ "undo": "Undo", "yes": "Yes", "no": "No", + "none": "None", + "not_applicable": "N/A", + "not_available": "N/A", "peek": "Peek", "scroll_to_top": "Scroll to top", "role": { @@ -584,6 +592,9 @@ "move_account_error": "Error moving account: {error}", "discoverable": "Allow discovery of this account in search results and other services", "domain_mutes": "Domains", + "domain_mutes2": "Excluded domains", + "user_mutes2": "Muted users", + "user_blocks": "Blocked users", "avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels. Recommended aspect ratio is 1:1", "banner_size_instruction": "The recommended minimum size for banner images is 450x150 pixels. Recommended aspect ratio is 3:1", "pad_emoji": "Pad emoji with spaces when adding from picker", @@ -636,6 +647,7 @@ "navbar_column_stretch": "Stretch navbar to columns width", "always_show_post_button": "Always show floating New Post button", "hide_wallpaper": "Hide instance wallpaper", + "foreign_user_background": "Allow other user's profiles to override wallpaper", "preload_images": "Preload images", "use_one_click_nsfw": "Open NSFW attachments with just one click", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)", @@ -1155,6 +1167,7 @@ "tabs": { "nodb": "No DB Config", "instance": "Instance", + "users": "Users", "limits": "Limits", "frontends": "Front-ends", "mailer": "EMails", @@ -1283,6 +1296,56 @@ "adapter": "Mailing Adapter", "auth": "Authentication" }, + "users": { + "title": "Users", + "local_id": "Local ID", + "no_users_found": "No users found", + "labels": { + "query": "Search", + "nickname": "{'@'}handle", + "name": "Display Name", + "name_colon": "Name:", + "email": "Email", + "email_colon": "Email:", + "handle_colon": "Handle:", + "origin": "Origin", + "activity": "Activity", + "privileges": "Privileges" + }, + "tags": { + "add_new": "Add New Tag", + "new_title": "Enter New Tag And Confirm", + "yes": "Add", + "no": "Abort" + }, + "options": { + "all": "All", + "only_local": "Only Local", + "only_external": "Only External", + "only_active": "Only Active", + "only_deactivated": "Only Deactivated", + "only_admins": "Only Admins", + "only_privileged": "Only Privileged", + "only_moderators": "Only Moderators", + "only_unapproved": "Exclude Approved", + "only_unconfirmed": "Exclude Confirmed" + }, + "filters": { + "show_direct": "Show Direct Messages", + "show_reblogs": "Show Reblogs" + }, + "indicator": { + "admin": "Admin", + "moderator": "Moderator", + "active": "Active", + "deactivated": "Deactivated", + "confirmed": "Confirmed", + "unconfirmed": "Pending confirmation", + "approved": "Approved", + "suggested": "Suggested", + "unapproved": "Pending approval" + } + }, "limits": { "arbitrary_limits": "Arbitrary limits", "posts": "Post limits", @@ -1417,10 +1480,36 @@ "description": "Number of retries for making a connection CONFIRM" } }, + ":hackney_pools": { + ":rich_media": { + "label": "Rich media", + "description": "idk", + ":max_connections": { + "label": "Max connections", + "description": "Number workers in the pool." + }, + ":timeout": { + "label": "Timeout", + "description": "Timeout while `hackney` will wait for response." + } + } + }, ":pools": { ":rich_media": { "label": "Rich media", - "description": "idk" + "description": "idk", + ":size": { + "label": "Size", + "description": "Maximum number of concurrent requests in the pool." + }, + ":max_waiting": { + "label": "Max waiting", + "description": "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errors when a new request is made" + }, + ":recv_timeout": { + "label": "Recv timeout", + "description": "Timeout for the pool while gun will wait for response" + } } }, ":rate_limit": { @@ -1602,6 +1691,7 @@ "show_all_attachments": "Show all attachments", "show_attachment_in_modal": "Show in media modal", "show_attachment_description": "Preview description (open attachment for full description)", + "attachment_description": "Attachment description", "hide_attachment": "Hide attachment", "remove_attachment": "Remove attachment", "attachment_stop_flash": "Stop Flash player", @@ -1626,7 +1716,10 @@ "invisible_quote": "Quoted status unavailable: {link}", "more_actions": "More actions on this status", "loading": "Loading...", - "load_error": "Unable to load status: {error}" + "load_error": "Unable to load status: {error}", + "admin_change_scope": "Change visibility", + "mark_as_sensitive": "Sensitive", + "mark_as_non-sensitive": "Non-sensitive" }, "user_card": { "approve": "Approve", @@ -1713,15 +1806,29 @@ "bot": "Bot", "group": "Group", "birthday": "Born {birthday}", + "joined": "Joined", + "admin_data": { + "data": "Administrative info", + "registration_reason": "Registration reason", + "tags": "Tags" + }, "admin_menu": { "moderation": "Moderation", "grant_admin": "Grant Admin", "revoke_admin": "Revoke Admin", "grant_moderator": "Grant Moderator", "revoke_moderator": "Revoke Moderator", - "activate_account": "Activate account", - "deactivate_account": "Deactivate account", - "delete_account": "Delete account", + "activate_account": "Activate", + "deactivate_account": "Deactivate", + "delete_account": "Delete", + "suggest_account": "Add to suggested", + "remove_suggested_account": "Remove from suggested", + "approve_account": "Approve", + "confirm_account": "Confirm", + "show_statuses": "Show all posts", + + "disable_mfa": "Disable MFA", + "force_nsfw": "Mark all posts as NSFW", "strip_media": "Remove media from posts", "force_unlisted": "Force posts to be unlisted", @@ -1729,8 +1836,48 @@ "disable_remote_subscription": "Disallow following user from remote instances", "disable_any_subscription": "Disallow following user at all", "quarantine": "Disallow user posts from federating", - "delete_user": "Delete user", - "delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?" + + "require_password_change": "Require Password Change", + "resend_confirmation": "Resend Confirmation Email", + "confirm_modal": { + "delete_title": "User deletion", + "delete_content": "Delete user {user}? | Delete {count} users?", + "delete_content_2": "This will permanently delete the data from this accounts and deactivate it. Are you absolutely sure?", + "activate_title": "User activation", + "activate_content": "Activate user {user}? | Activate {count} users?", + "deactivate_content": "Dectivate user {user}? | Dectivate {count} users?", + "approval_title": "Approve users", + "approval_content": "Approve user {user}? | Approve {count} users?", + "confirm_title": "Confirm users", + "confirm_content": "Approve user {user}? | Approve {count} users?", + "suggest_title": "Suggest users", + "add_suggest_content": "Add user {user} to suggested users list? | Add {count} users to suggested users list?", + "remove_suggest_content": "Remove user {user} from suggested users list? | Add {count} users to suggested users list?", + "rights_title": "Promote users", + "grant_rights_content": "Grant user {user} {name} role? | Grant {count} users {name} role?", + "revoke_rights_content": "Revoke {name} role from user {user}? | Revoke {name} from {count} users?", + "tag_title": "Assign user policy", + "assign_tag_content": "Assign {user} a {name} policy? | Assign {name} policy to {count} users?", + "unassign_tag_content": "Unassign policy {name} from {user}? | Unassign policy {name} from {count} users?", + "resend_confirmation_title": "Email confirmation resend", + "resend_confirmation_content": "Resend confirmation email to {count} users?", + "disable_mfa_title": "Disable MFA", + "disable_mfa_content": "Disable Mult-Factor Authentication for {count} users?", + "require_password_change_title": "Force password change", + "require_password_change_content": "Force {count} users to change password on next login?", + "add": "Add", + "remove": "Remove", + "delete": "Delete", + "activate": "Activate", + "deactivate": "Deactivate", + "grant": "Grant", + "revoke": "Revoke", + "approve": "Approve", + "confirm": "Confirm", + "assign": "Assign", + "unassign": "Unassign", + "send": "Send" + } }, "highlight_new": { "disabled": "Don't highlight", diff --git a/src/i18n/messages.js b/src/i18n/messages.js index df851c88f..76d6b5386 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -16,10 +16,11 @@ const ULTIMATE_FALLBACK_LOCALE = 'en' const hasLanguageFile = (code) => languages.includes(code) -const languageFileMap = import.meta.glob('./*.json') +const languageFileMap = import.meta.glob(['./*.json', '!./en.json']) const loadLanguageFile = (code) => { const jsonName = langCodeToJsonName(code) + if (jsonName === 'en') return Promise.resolve({ default: enMessages }) return languageFileMap[`./${jsonName}.json`]() } diff --git a/src/lib/language.js b/src/lib/language.js index af53eeffd..dc5121452 100644 --- a/src/lib/language.js +++ b/src/lib/language.js @@ -1,5 +1,3 @@ -import Cookies from 'js-cookie' - import { useEmojiStore } from 'src/stores/emoji.js' import { useI18nStore } from 'src/stores/i18n.js' diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index f7e2b0b08..f6375dfed 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -1,5 +1,4 @@ -import { cloneDeep, each, get, set } from 'lodash' -import merge from 'lodash.merge' +import { cloneDeep, each, get, merge, set } from 'lodash' import { storage } from './storage.js' diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js deleted file mode 100644 index 33cc8a595..000000000 --- a/src/modules/adminSettings.js +++ /dev/null @@ -1,294 +0,0 @@ -import { cloneDeep, differenceWith, flatten, get, isEqual, set } from 'lodash' - -export const defaultState = { - frontends: [], - loaded: false, - needsReboot: null, - config: null, - modifiedPaths: null, - descriptions: null, - draft: null, - dbConfigEnabled: null, -} - -export const newUserFlags = { - ...defaultState.flagStorage, -} - -const adminSettingsStorage = { - state: { - ...cloneDeep(defaultState), - }, - mutations: { - setInstanceAdminNoDbConfig(state) { - state.loaded = false - state.dbConfigEnabled = false - }, - setAvailableFrontends(state, { frontends }) { - state.frontends = frontends.map((f) => { - f.installedRefs = f.installed_refs - if (f.name === 'pleroma-fe') { - f.refs = ['master', 'develop'] - } else { - f.refs = [f.ref] - } - return f - }) - }, - updateAdminSettings(state, { config, modifiedPaths }) { - state.loaded = true - state.dbConfigEnabled = true - state.config = config - state.modifiedPaths = modifiedPaths - }, - updateAdminDescriptions(state, { descriptions }) { - state.descriptions = descriptions - }, - updateAdminDraft(state, { path, value }) { - const [group, key, subkey] = path - const parent = [group, key, subkey] - - set(state.draft, path, value) - - // force-updating grouped draft to trigger refresh of group settings - if (path.length > parent.length) { - set(state.draft, parent, cloneDeep(get(state.draft, parent))) - } - }, - resetAdminDraft(state) { - state.draft = cloneDeep(state.config) - }, - }, - actions: { - loadFrontendsStuff({ rootState, commit }) { - rootState.api.backendInteractor - .fetchAvailableFrontends() - .then((frontends) => commit('setAvailableFrontends', { frontends })) - }, - loadAdminStuff({ state, rootState, dispatch, commit }) { - rootState.api.backendInteractor - .fetchInstanceDBConfig() - .then((backendDbConfig) => { - if (backendDbConfig.error) { - if (backendDbConfig.error.status === 400) { - backendDbConfig.error.json().then((errorJson) => { - if (/configurable_from_database/.test(errorJson.error)) { - commit('setInstanceAdminNoDbConfig') - } - }) - } - } else { - dispatch('setInstanceAdminSettings', { backendDbConfig }) - } - }) - if (state.descriptions === null) { - rootState.api.backendInteractor - .fetchInstanceConfigDescriptions() - .then((backendDescriptions) => - dispatch('setInstanceAdminDescriptions', { backendDescriptions }), - ) - } - }, - setInstanceAdminSettings({ state, commit }, { backendDbConfig }) { - const config = state.config || {} - const modifiedPaths = new Set() - - backendDbConfig.configs.forEach((c) => { - const path = [c.group, c.key] - if (c.db) { - // Path elements can contain dot, therefore we use ' -> ' as a separator instead - // Using strings for modified paths for easier searching - c.db.forEach((x) => modifiedPaths.add([...path, x].join(' -> '))) - } - - // we need to preserve tuples on second level only, possibly third - // but it's not a case right now. - const convert = (value, preserveTuples, preserveTuplesLv2) => { - if (Array.isArray(value) && value.length > 0 && value[0].tuple) { - if (!preserveTuples) { - return value.reduce((acc, c) => { - if (c.tuple == null) { - return { - ...acc, - [c]: c, - } - } - return { - ...acc, - [c.tuple[0]]: convert(c.tuple[1], preserveTuplesLv2), - } - }, {}) - } else { - return value.map((x) => x.tuple) - } - } else { - if (!preserveTuples) { - return value - } else { - return value.tuple - } - } - } - // for most stuff we want maps since those are more convenient - // however this doesn't allow for multiple values per same key - // so for those cases we want to preserve tuples as-is - // right now it's made exclusively for :pleroma.:rate_limit - // so it might not work properly elsewhere - const preserveTuples = path.find((x) => x === ':rate_limit') - set(config, path, convert(c.value, false, preserveTuples)) - }) - // patching http adapter config to be easier to handle - const adapter = config[':pleroma'][':http'][':adapter'] - if (Array.isArray(adapter)) { - config[':pleroma'][':http'][':adapter'] = { - [':ssl_options']: { - [':versions']: [], - }, - } - } - commit('updateAdminSettings', { config, modifiedPaths }) - commit('resetAdminDraft') - }, - setInstanceAdminDescriptions({ commit }, { backendDescriptions }) { - const convert = ( - { children, description, label, key = '', group, suggestions }, - path, - acc, - ) => { - const newPath = group ? [group, key] : [key] - const obj = { description, label, suggestions } - if (Array.isArray(children)) { - children.forEach((c) => { - convert(c, newPath, obj) - }) - } - set(acc, newPath, obj) - } - - const descriptions = {} - - backendDescriptions.forEach((d) => convert(d, '', descriptions)) - commit('updateAdminDescriptions', { descriptions }) - }, - - // This action takes draft state, diffs it with live config state and then pushes - // only differences between the two. Difference detection only work up to subkey (third) level. - pushAdminDraft({ rootState, state, dispatch }) { - // TODO cleanup paths in modifiedPaths - const convert = (value) => { - if (typeof value !== 'object') { - return value - } else if (Array.isArray(value)) { - return value.map(convert) - } else { - return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] })) - } - } - - // Getting all group-keys used in config - const allGroupKeys = flatten( - Object.entries(state.config).map(([group, lv1data]) => - Object.keys(lv1data).map((key) => ({ group, key })), - ), - ) - - // Only using group-keys where there are changes detected - const changedGroupKeys = allGroupKeys.filter(({ group, key }) => { - return !isEqual(state.config[group][key], state.draft[group][key]) - }) - - // Here we take all changed group-keys and get all changed subkeys - const changed = changedGroupKeys.map(({ group, key }) => { - const config = state.config[group][key] - const draft = state.draft[group][key] - - // We convert group-key value into entries arrays - const eConfig = Object.entries(config) - const eDraft = Object.entries(draft) - - // Then those entries array we diff so only changed subkey entries remain - // We use the diffed array to reconstruct the object and then shove it into convert() - return { - group, - key, - value: convert( - Object.fromEntries(differenceWith(eDraft, eConfig, isEqual)), - ), - } - }) - - rootState.api.backendInteractor - .pushInstanceDBConfig({ - payload: { - configs: changed, - }, - }) - .then(() => rootState.api.backendInteractor.fetchInstanceDBConfig()) - .then((backendDbConfig) => - dispatch('setInstanceAdminSettings', { backendDbConfig }), - ) - }, - pushAdminSetting({ rootState, dispatch }, { path, value }) { - const [group, key, ...rest] = Array.isArray(path) - ? path - : path.split(/\./g) - const clone = {} // not actually cloning the entire thing to avoid excessive writes - set(clone, rest, value) - - // TODO cleanup paths in modifiedPaths - const convert = (value) => { - if (typeof value !== 'object') { - return value - } else if (Array.isArray(value)) { - return value.map(convert) - } else { - return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] })) - } - } - - rootState.api.backendInteractor - .pushInstanceDBConfig({ - payload: { - configs: [ - { - group, - key, - value: convert(clone), - }, - ], - }, - }) - .then(() => rootState.api.backendInteractor.fetchInstanceDBConfig()) - .then((backendDbConfig) => - dispatch('setInstanceAdminSettings', { backendDbConfig }), - ) - }, - resetAdminSetting({ rootState, state, dispatch }, { path }) { - const [group, key, subkey] = Array.isArray(path) - ? path - : path.split(/\./g) - - state.modifiedPaths.delete(path) - - return rootState.api.backendInteractor - .pushInstanceDBConfig({ - payload: { - configs: [ - { - group, - key, - delete: true, - subkeys: [subkey], - }, - ], - }, - }) - .then(() => rootState.api.backendInteractor.fetchInstanceDBConfig()) - .then((backendDbConfig) => - dispatch('setInstanceAdminSettings', { backendDbConfig }), - ) - }, - }, -} - -export default adminSettingsStorage diff --git a/src/modules/api.js b/src/modules/api.js index 6f4b8b15f..232d1776d 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -1,20 +1,27 @@ import { Socket } from 'phoenix' -import { WSConnectionStatus } from '../services/api/api.service.js' -import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useShoutStore } from 'src/stores/shout.js' +import { fetchTimeline } from 'src/api/timelines.js' +import { + getMastodonSocketURI, + ProcessedWS, + WSConnectionStatus, +} from 'src/api/websocket.js' +import followRequestFetcher from 'src/services/follow_request_fetcher/follow_request_fetcher.service' +import notificationsFetcher from 'src/services/notifications_fetcher/notifications_fetcher.service.js' +import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js' + const retryTimeout = (multiplier) => 1000 * multiplier const api = { state: { retryMultiplier: 1, - backendInteractor: backendInteractorService(), fetchers: {}, socket: null, mastoUserSocket: null, @@ -25,9 +32,6 @@ const api = { followRequestCount: (state) => state.followRequests.length, }, mutations: { - setBackendInteractor(state, backendInteractor) { - state.backendInteractor = backendInteractor - }, addFetcher(state, { fetcherName, fetcher }) { state.fetchers[fetcherName] = fetcher }, @@ -91,9 +95,16 @@ const api = { try { const { state, commit, dispatch, rootState } = store const timelineData = rootState.statuses.timelines.friends - state.mastoUserSocket = state.backendInteractor.startUserSocket({ - store, + + const credentials = useOAuthStore().token + const url = getMastodonSocketURI({ credentials }) + + state.mastoUserSocket = ProcessedWS({ + url, + id: 'Unified', + credentials, }) + state.mastoUserSocket.addEventListener( 'pleroma:authenticated', () => { @@ -245,7 +256,7 @@ const api = { return if (store.state.fetchers[timeline]) return - const fetcher = store.state.backendInteractor.startFetchingTimeline({ + const fetcher = timelineFetcher.startFetching({ timeline, store, userId, @@ -253,7 +264,9 @@ const api = { statusId, bookmarkFolderId, tag, + credentials: useOAuthStore().token, }) + store.commit('addFetcher', { fetcherName: timeline, fetcher }) }, stopFetchingTimeline(store, timeline) { @@ -261,19 +274,22 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: timeline, fetcher }) }, + fetchTimeline(store, { timeline, ...rest }) { - store.state.backendInteractor.fetchTimeline({ + fetchTimeline({ store, timeline, ...rest, + credentials: useOAuthStore().token, }) }, // Notifications startFetchingNotifications(store) { if (store.state.fetchers.notifications) return - const fetcher = store.state.backendInteractor.startFetchingNotifications({ + const fetcher = notificationsFetcher.startFetching({ store, + credentials: useOAuthStore().token, }) store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) }, @@ -282,19 +298,14 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) }, - fetchNotifications(store, { ...rest }) { - store.state.backendInteractor.fetchNotifications({ - store, - ...rest, - }) - }, // Follow requests startFetchingFollowRequests(store) { if (store.state.fetchers.followRequests) return - const fetcher = store.state.backendInteractor.startFetchingFollowRequests( - { store }, - ) + const fetcher = followRequestFetcher.startFetching({ + store, + credentials: useOAuthStore().token, + }) store.commit('addFetcher', { fetcherName: 'followRequests', fetcher }) }, @@ -303,39 +314,6 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher }) }, - removeFollowRequest(store, request) { - const requests = store.state.followRequests.filter((it) => it !== request) - store.commit('setFollowRequests', requests) - }, - - // Lists - startFetchingLists(store) { - if (store.state.fetchers.lists) return - const fetcher = store.state.backendInteractor.startFetchingLists({ - store, - }) - store.commit('addFetcher', { fetcherName: 'lists', fetcher }) - }, - stopFetchingLists(store) { - const fetcher = store.state.fetchers.lists - if (!fetcher) return - store.commit('removeFetcher', { fetcherName: 'lists', fetcher }) - }, - - // Bookmark folders - startFetchingBookmarkFolders(store) { - if (store.state.fetchers.bookmarkFolders) return - if (!useInstanceCapabilitiesStore().pleromaBookmarkFoldersAvailable) - return - const fetcher = - store.state.backendInteractor.startFetchingBookmarkFolders({ store }) - store.commit('addFetcher', { fetcherName: 'bookmarkFolders', fetcher }) - }, - stopFetchingBookmarkFolders(store) { - const fetcher = store.state.fetchers.bookmarkFolders - if (!fetcher) return - store.commit('removeFetcher', { fetcherName: 'bookmarkFolders', fetcher }) - }, // Pleroma websocket setWsToken(store, token) { diff --git a/src/modules/chats.js b/src/modules/chats.js index 308b2cb27..abc035f09 100644 --- a/src/modules/chats.js +++ b/src/modules/chats.js @@ -9,6 +9,10 @@ import { } from '../services/entity_normalizer/entity_normalizer.service.js' import { promiseInterval } from '../services/promise_interval/promise_interval.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { chats, deleteChatMessage, readChat } from 'src/api/chats.js' + const emptyChatList = () => ({ data: [], idStore: {}, @@ -36,7 +40,7 @@ const unreadChatCount = (state) => { return sumBy(state.chatList.data, 'unread') } -const chats = { +const chatsModule = { state: { ...defaultState }, getters: { currentChat: (state) => state.openedChats[state.currentChatId], @@ -51,7 +55,6 @@ const chats = { // Chat list startFetchingChats({ dispatch, commit }) { const fetcher = () => dispatch('fetchChats', { latest: true }) - fetcher() commit('setChatListFetcher', { fetcher: () => promiseInterval(fetcher, 5000), }) @@ -60,8 +63,10 @@ const chats = { commit('setChatListFetcher', { fetcher: undefined }) }, fetchChats({ dispatch, rootState }) { - return rootState.api.backendInteractor.chats().then(({ chats }) => { - dispatch('addNewChats', { chats }) + return chats({ + credentials: useOAuthStore().token, + }).then(({ chatList }) => { + dispatch('addNewChats', { chats: chatList }) return chats }) }, @@ -113,11 +118,18 @@ const chats = { commit('readChat', { id, lastReadId }) if (isNewMessage) { - rootState.api.backendInteractor.readChat({ id, lastReadId }) + readChat({ + id, + lastReadId, + credentials: useOAuthStore().token, + }) } }, deleteChatMessage({ rootState, commit }, value) { - rootState.api.backendInteractor.deleteChatMessage(value) + deleteChatMessage({ + ...value, + credentials: useOAuthStore().token, + }) commit('deleteChatMessage', { commit, ...value }) }, resetChats({ commit, dispatch }) { @@ -262,4 +274,4 @@ const chats = { }, } -export default chats +export default chatsModule diff --git a/src/modules/default_config_state.js b/src/modules/default_config_state.js index 208dc548b..ec190af28 100644 --- a/src/modules/default_config_state.js +++ b/src/modules/default_config_state.js @@ -1,4 +1,4 @@ -import { get, set } from 'lodash' +import { get } from 'lodash' const browserLocale = (navigator.language || 'en').split('-')[0] @@ -140,6 +140,10 @@ export const INSTANCE_DEFAULT_CONFIG_DEFINITIONS = { description: 'Hide Instance-specific panel', default: false, }, + allowForeignUserBackground: { + description: "Allow other user's profiles to override wallpaper", + default: true, + }, hideInstanceWallpaper: { description: 'Hide Instance default background', default: false, diff --git a/src/modules/drafts.js b/src/modules/drafts.js index 42bb3b313..3cde4f574 100644 --- a/src/modules/drafts.js +++ b/src/modules/drafts.js @@ -44,7 +44,7 @@ const saveDraftToStorage = async (draft) => { const deleteDraftFromStorage = async (ids) => { const currentData = await getStorageData() - ids.forEach(id => { + ids.forEach((id) => { delete currentData[id] }) await storage.setItem(storageKey, currentData) diff --git a/src/modules/index.js b/src/modules/index.js index 76bdbf7f3..e42260c06 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -1,4 +1,3 @@ -import adminSettings from './adminSettings.js' import api from './api.js' import chats from './chats.js' import drafts from './drafts.js' @@ -13,7 +12,6 @@ export default { users, api, profileConfig, - adminSettings, drafts, chats, } diff --git a/src/modules/notifications.js b/src/modules/notifications.js index bf1e4b9c0..b77683811 100644 --- a/src/modules/notifications.js +++ b/src/modules/notifications.js @@ -1,4 +1,3 @@ -import apiService from '../services/api/api.service.js' import { closeAllDesktopNotifications, closeDesktopNotification, @@ -9,10 +8,14 @@ import { maybeShowNotification, } from '../services/notification_utils/notification_utils.js' +import { useI18nStore } from 'src/stores/i18n.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { useReportsStore } from 'src/stores/reports.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' +import { dismissNotification, markNotificationsAsSeen } from 'src/api/user.js' + const emptyNotifications = () => ({ desktopNotificationSilence: true, maxId: 0, @@ -123,6 +126,7 @@ export const notifications = { useMergedConfigStore().mergedConfig.notificationVisibility, Object.values(useSyncConfigStore().prefsStorage.simple.muteFilters), notification, + useI18nStore().i18n, ) } else if (notification.seen) { state.idStore[notification.id].seen = true @@ -152,33 +156,32 @@ export const notifications = { }, markNotificationsAsSeen({ rootState, state, commit }) { commit('markNotificationsAsSeen') - apiService - .markNotificationsAsSeen({ - id: state.maxId, - credentials: rootState.users.currentUser.credentials, - }) - .then(() => { - closeAllDesktopNotifications(rootState) - }) + markNotificationsAsSeen({ + id: state.maxId, + credentials: rootState.users.currentUser.credentials, + }).then(() => { + closeAllDesktopNotifications(rootState) + }) }, markSingleNotificationAsSeen({ rootState, commit }, { id }) { commit('markSingleNotificationAsSeen', { id }) - apiService - .markNotificationsAsSeen({ - single: true, - id, - credentials: rootState.users.currentUser.credentials, - }) - .then(() => { - closeDesktopNotification(rootState, { id }) - }) + markNotificationsAsSeen({ + single: true, + id, + credentials: rootState.users.currentUser.credentials, + }).then(() => { + closeDesktopNotification(rootState, { id }) + }) }, dismissNotificationLocal({ commit }, { id }) { commit('dismissNotification', { id }) }, dismissNotification({ rootState, commit }, { id }) { commit('dismissNotification', { id }) - rootState.api.backendInteractor.dismissNotification({ id }) + dismissNotification({ + id, + credentials: useOAuthStore().token, + }) }, updateNotification({ commit }, { id, updater }) { commit('updateNotification', { id, updater }) diff --git a/src/modules/profileConfig.js b/src/modules/profileConfig.js index 90571e21d..3fca7d6a8 100644 --- a/src/modules/profileConfig.js +++ b/src/modules/profileConfig.js @@ -1,28 +1,34 @@ import { get, set } from 'lodash' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { updateNotificationSettings, updateProfile } from 'src/api/user.js' + const defaultApi = ({ rootState, commit }, { path, value }) => { const params = {} set(params, path, value) - return rootState.api.backendInteractor - .updateProfile({ params }) - .then((result) => { - commit('addNewUsers', [result]) - commit('setCurrentUser', result) - }) + return updateProfile({ + params, + credentials: useOAuthStore().token, + }).then(({ data: result }) => { + commit('addNewUsers', [result]) + commit('setCurrentUser', result) + }) } const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => { const settings = {} set(settings, path, value) - return rootState.api.backendInteractor - .updateNotificationSettings({ settings }) - .then((result) => { - if (result.status === 'success') { - commit('confirmProfileOption', { name, value }) - } else { - commit('confirmProfileOption', { name, value: oldValue }) - } - }) + return updateNotificationSettings({ + settings, + credentials: useOAuthStore().token, + }).then(({ data: result }) => { + if (result.status === 'success') { + commit('confirmProfileOption', { name, value }) + } else { + commit('confirmProfileOption', { name, value: oldValue }) + } + }) } /** diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 339b6193a..5bec225ce 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -13,10 +13,36 @@ import { slice, } from 'lodash' -import apiService from '../services/api/api.service.js' - import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + fetchEmojiReactions, + fetchFavoritedByUsers, + fetchPinnedStatuses, + fetchRebloggedByUsers, + fetchScrobbles, + fetchStatus, + fetchStatusHistory, + fetchStatusSource, + search2, +} from 'src/api/public.js' +import { + bookmarkStatus, + deleteStatus, + favorite, + muteConversation, + pinOwnStatus, + reactWithEmoji, + retweet, + unbookmarkStatus, + unfavorite, + unmuteConversation, + unpinOwnStatus, + unreactWithEmoji, + unretweet, +} from 'src/api/user.js' const emptyTl = (userId = 0) => ({ statuses: [], @@ -131,9 +157,8 @@ const getLatestScrobble = (state, user) => { state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000 if (!scrobblesSupport) return - apiService - .fetchScrobbles({ accountId: user.id }) - .then((scrobbles) => { + fetchScrobbles({ accountId: user.id }) + .then(({ data: scrobbles }) => { if (scrobbles?.error) { useInstanceCapabilitiesStore().set('pleromaScrobblesAvailable', false) return @@ -348,8 +373,7 @@ const addNewStatuses = ( // NOOP, it is known status but we don't do anything about it for now }, default: (unknown) => { - console.warn('unknown status type') - console.warn(unknown) + console.warn('unknown status type', unknown) }, } @@ -592,7 +616,7 @@ const statuses = { pagination, }, ) { - commit('addNewStatuses', { + return commit('addNewStatuses', { statuses, showImmediately, timeline, @@ -603,25 +627,24 @@ const statuses = { }) }, fetchStatus({ rootState, dispatch }, id) { - return rootState.api.backendInteractor - .fetchStatus({ id }) - .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + return fetchStatus({ id }).then(({ data: status }) => + dispatch('addNewStatuses', { statuses: [status] }), + ) }, fetchStatusSource({ rootState }, status) { - return apiService.fetchStatusSource({ + return fetchStatusSource({ id: status.id, - credentials: rootState.users.currentUser.credentials, - }) + credentials: useOAuthStore().token, + }).then(({ data }) => data) }, fetchStatusHistory(_, status) { - return apiService.fetchStatusHistory({ status }) + return fetchStatusHistory({ status }).then(({ data }) => data) }, deleteStatus({ rootState, commit }, status) { - apiService - .deleteStatus({ - id: status.id, - credentials: rootState.users.currentUser.credentials, - }) + deleteStatus({ + id: status.id, + credentials: useOAuthStore().token, + }) .then(() => { commit('setDeleted', { status }) }) @@ -644,99 +667,115 @@ const statuses = { favorite({ rootState, commit }, status) { // Optimistic favoriting... commit('setFavorited', { status, value: true }) - rootState.api.backendInteractor - .favorite({ id: status.id }) - .then((status) => - commit('setFavoritedConfirm', { - status, - user: rootState.users.currentUser, - }), - ) + favorite({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + commit('setFavoritedConfirm', { + status, + user: rootState.users.currentUser, + }), + ) }, unfavorite({ rootState, commit }, status) { // Optimistic unfavoriting... commit('setFavorited', { status, value: false }) - rootState.api.backendInteractor - .unfavorite({ id: status.id }) - .then((status) => - commit('setFavoritedConfirm', { - status, - user: rootState.users.currentUser, - }), - ) + unfavorite({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + commit('setFavoritedConfirm', { + status, + user: rootState.users.currentUser, + }), + ) }, fetchPinnedStatuses({ rootState, dispatch }, userId) { - rootState.api.backendInteractor - .fetchPinnedStatuses({ id: userId }) - .then((statuses) => - dispatch('addNewStatuses', { - statuses, - timeline: 'user', - userId, - showImmediately: true, - noIdUpdate: true, - }), - ) + fetchPinnedStatuses({ + id: userId, + credentials: useOAuthStore().token, + }).then(({ data: statuses }) => + dispatch('addNewStatuses', { + statuses, + timeline: 'user', + userId, + showImmediately: true, + noIdUpdate: true, + }), + ) }, pinStatus({ rootState, dispatch }, statusId) { - return rootState.api.backendInteractor - .pinOwnStatus({ id: statusId }) - .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + return pinOwnStatus({ + id: statusId, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + dispatch('addNewStatuses', { statuses: [status] }), + ) }, unpinStatus({ rootState, dispatch }, statusId) { - rootState.api.backendInteractor - .unpinOwnStatus({ id: statusId }) - .then((status) => dispatch('addNewStatuses', { statuses: [status] })) + return unpinOwnStatus({ + id: statusId, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + dispatch('addNewStatuses', { statuses: [status] }), + ) }, muteConversation({ rootState, commit }, { id: statusId }) { - return rootState.api.backendInteractor - .muteConversation({ id: statusId }) - .then((status) => commit('setMutedStatus', status)) + return muteConversation({ + id: statusId, + credentials: useOAuthStore().token, + }).then(({ data: status }) => commit('setMutedStatus', status)) }, unmuteConversation({ rootState, commit }, { id: statusId }) { - return rootState.api.backendInteractor - .unmuteConversation({ id: statusId }) - .then((status) => commit('setMutedStatus', status)) + return unmuteConversation({ + id: statusId, + credentials: useOAuthStore().token, + }).then(({ data: status }) => commit('setMutedStatus', status)) }, retweet({ rootState, commit }, status) { // Optimistic retweeting... commit('setRetweeted', { status, value: true }) - rootState.api.backendInteractor - .retweet({ id: status.id }) - .then((status) => - commit('setRetweetedConfirm', { - status: status.retweeted_status, - user: rootState.users.currentUser, - }), - ) + retweet({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + commit('setRetweetedConfirm', { + status: status.retweeted_status, + user: rootState.users.currentUser, + }), + ) }, unretweet({ rootState, commit }, status) { // Optimistic unretweeting... commit('setRetweeted', { status, value: false }) - rootState.api.backendInteractor - .unretweet({ id: status.id }) - .then((status) => - commit('setRetweetedConfirm', { - status, - user: rootState.users.currentUser, - }), - ) + unretweet({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => + commit('setRetweetedConfirm', { + status, + user: rootState.users.currentUser, + }), + ) }, bookmark({ rootState, commit }, status) { commit('setBookmarked', { status, value: true }) - rootState.api.backendInteractor - .bookmarkStatus({ id: status.id, folder_id: status.bookmark_folder_id }) - .then((status) => { - commit('setBookmarkedConfirm', { status }) - }) + bookmarkStatus({ + id: status.id, + folder_id: status.bookmark_folder_id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { + commit('setBookmarkedConfirm', { status }) + }) }, unbookmark({ rootState, commit }, status) { commit('setBookmarked', { status, value: false }) - rootState.api.backendInteractor - .unbookmarkStatus({ id: status.id }) - .then((status) => { - commit('setBookmarkedConfirm', { status }) - }) + unbookmarkStatus({ + id: status.id, + credentials: useOAuthStore().token, + }).then(({ data: status }) => { + commit('setBookmarkedConfirm', { status }) + }) }, queueFlush({ commit }, { timeline, id }) { commit('queueFlush', { timeline, id }) @@ -746,8 +785,14 @@ const statuses = { }, fetchFavsAndRepeats({ rootState, commit }, id) { Promise.all([ - rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), - rootState.api.backendInteractor.fetchRebloggedByUsers({ id }), + fetchFavoritedByUsers({ + id, + credentials: useOAuthStore().token, + }).then(({ data }) => data), + fetchRebloggedByUsers({ + id, + credentials: useOAuthStore().token, + }).then(({ data }) => data), ]).then(([favoritedByUsers, rebloggedByUsers]) => { commit('addFavs', { id, @@ -766,7 +811,11 @@ const statuses = { if (!currentUser) return commit('addOwnReaction', { id, emoji, currentUser }) - rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(() => { + reactWithEmoji({ + id, + emoji, + credentials: useOAuthStore().token, + }).then(() => { dispatch('fetchEmojiReactionsBy', id) }) }, @@ -775,57 +824,74 @@ const statuses = { if (!currentUser) return commit('removeOwnReaction', { id, emoji, currentUser }) - rootState.api.backendInteractor - .unreactWithEmoji({ id, emoji }) - .then(() => { - dispatch('fetchEmojiReactionsBy', id) - }) + unreactWithEmoji({ + id, + emoji, + currentUser: rootState.users.currentUser, + }).then(() => { + dispatch('fetchEmojiReactionsBy', id) + }) }, fetchEmojiReactionsBy({ rootState, commit }, id) { - return rootState.api.backendInteractor - .fetchEmojiReactions({ id }) - .then((emojiReactions) => { - commit('addEmojiReactionsBy', { - id, - emojiReactions, - currentUser: rootState.users.currentUser, - }) + return fetchEmojiReactions({ + id, + credentials: useOAuthStore().token, + }).then(({ data: emojiReactions }) => { + commit('addEmojiReactionsBy', { + id, + emojiReactions, + currentUser: rootState.users.currentUser, }) + }) }, fetchFavs({ rootState, commit }, id) { - rootState.api.backendInteractor - .fetchFavoritedByUsers({ id }) - .then((favoritedByUsers) => - commit('addFavs', { - id, - favoritedByUsers, - currentUser: rootState.users.currentUser, - }), - ) + fetchFavoritedByUsers({ + id, + credentials: useOAuthStore().token, + }).then(({ data: favoritedByUsers }) => + commit('addFavs', { + id, + favoritedByUsers, + currentUser: rootState.users.currentUser, + }), + ) }, fetchRepeats({ rootState, commit }, id) { - rootState.api.backendInteractor - .fetchRebloggedByUsers({ id }) - .then((rebloggedByUsers) => - commit('addRepeats', { - id, - rebloggedByUsers, - currentUser: rootState.users.currentUser, - }), - ) + fetchRebloggedByUsers({ + id, + credentials: useOAuthStore().token, + }).then(({ data: rebloggedByUsers }) => + commit('addRepeats', { + id, + rebloggedByUsers, + currentUser: rootState.users.currentUser, + }), + ) }, search(store, { q, resolve, limit, offset, following, type }) { - return store.rootState.api.backendInteractor - .search2({ q, resolve, limit, offset, following, type }) - .then((data) => { - store.commit('addNewUsers', data.accounts) - store.commit( - 'addNewUsers', - data.statuses.map((s) => s.user).filter((u) => u), - ) - store.commit('addNewStatuses', { statuses: data.statuses }) - return data + return search2({ + q, + resolve, + limit, + offset, + following, + type, + credentials: useOAuthStore().token, + }).then(({ data }) => { + store.commit('addNewUsers', data.accounts) + store.commit( + 'addNewUsers', + data.statuses.map((s) => s.user).filter((u) => u), + ) + store.commit('addNewStatuses', { + statuses: data.statuses, }) + + data.statuses = data.statuses.map( + (s) => store.state.allStatusesObject[s.id], + ) + return data + }) }, setVirtualHeight({ commit }, { statusId, height }) { commit('setVirtualHeight', { statusId, height }) diff --git a/src/modules/users.js b/src/modules/users.js index db5b16ccc..d6b1adaaa 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,3 +1,4 @@ +import Cookies from 'js-cookie' import { compact, concat, @@ -9,9 +10,6 @@ import { uniq, } from 'lodash' -import apiService from '../services/api/api.service.js' -import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' -import oauthApi from '../services/new_api/oauth.js' import { registerPushNotifications, unregisterPushNotifications, @@ -21,15 +19,42 @@ import { windowWidth, } from '../services/window_utils/window_utils' +import { useAnnouncementsStore } from 'src/stores/announcements.js' +import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' import { useEmojiStore } from 'src/stores/emoji.js' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useListsStore } from 'src/stores/lists.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useOAuthStore } from 'src/stores/oauth.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js' +import { revokeToken } from 'src/api/oauth.js' +import { + fetchFollowers, + fetchFriends, + fetchUser, + fetchUserByName, + getCaptcha, + register, + searchUsers, + verifyCredentials, +} from 'src/api/public.js' +import { + blockUser as apiBlockUser, + muteUser as apiMuteUser, + unblockUser as apiUnblockUser, + unmuteUser as apiUnmuteUser, + fetchBlocks, + fetchDomainMutes, + fetchMutes, + fetchUserInLists, + fetchUserRelationship, + followUser, +} from 'src/api/user.js' + // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { if (!item) { @@ -72,43 +97,35 @@ const blockUser = (store, args) => { store.commit('updateUserRelationship', [predictedRelationship]) store.commit('addBlockId', id) - return store.rootState.api.backendInteractor - .blockUser({ id, expiresIn }) - .then((relationship) => { - store.commit('updateUserRelationship', [relationship]) - store.commit('addBlockId', id) + return apiBlockUser({ id, expiresIn }).then(({ data: relationship }) => { + store.commit('updateUserRelationship', [relationship]) + store.commit('addBlockId', id) - store.commit('removeStatus', { timeline: 'friends', userId: id }) - store.commit('removeStatus', { timeline: 'public', userId: id }) - store.commit('removeStatus', { - timeline: 'publicAndExternal', - userId: id, - }) + store.commit('removeStatus', { timeline: 'friends', userId: id }) + store.commit('removeStatus', { timeline: 'public', userId: id }) + store.commit('removeStatus', { + timeline: 'publicAndExternal', + userId: id, }) + }) } const unblockUser = (store, id) => { - return store.rootState.api.backendInteractor - .unblockUser({ id }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return apiUnblockUser({ id }).then(({ data: relationship }) => + store.commit('updateUserRelationship', [relationship]), + ) } const removeUserFromFollowers = (store, id) => { - return store.rootState.api.backendInteractor - .removeUserFromFollowers({ id }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return removeUserFromFollowers({ id }).then((relationship) => + store.commit('updateUserRelationship', [relationship]), + ) } const editUserNote = (store, { id, comment }) => { - return store.rootState.api.backendInteractor - .editUserNote({ id, comment }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return editUserNote({ id, comment }).then((relationship) => + store.commit('updateUserRelationship', [relationship]), + ) } const muteUser = (store, args) => { @@ -119,12 +136,14 @@ const muteUser = (store, args) => { store.commit('updateUserRelationship', [predictedRelationship]) store.commit('addMuteId', id) - return store.rootState.api.backendInteractor - .muteUser({ id, expiresIn }) - .then((relationship) => { - store.commit('updateUserRelationship', [relationship]) - store.commit('addMuteId', id) - }) + return apiMuteUser({ + id, + expiresIn, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => { + store.commit('updateUserRelationship', [relationship]) + store.commit('addMuteId', id) + }) } const unmuteUser = (store, id) => { @@ -132,53 +151,53 @@ const unmuteUser = (store, id) => { predictedRelationship.muting = false store.commit('updateUserRelationship', [predictedRelationship]) - return store.rootState.api.backendInteractor - .unmuteUser({ id }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return apiUnmuteUser({ id }).then(({ data: relationship }) => + store.commit('updateUserRelationship', [relationship]), + ) } const hideReblogs = (store, userId) => { - return store.rootState.api.backendInteractor - .followUser({ id: userId, reblogs: false }) - .then((relationship) => { - store.commit('updateUserRelationship', [relationship]) - }) + return followUser({ + id: userId, + reblogs: false, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => + store.commit('updateUserRelationship', [relationship]), + ) } const showReblogs = (store, userId) => { - return store.rootState.api.backendInteractor - .followUser({ id: userId, reblogs: true }) - .then((relationship) => - store.commit('updateUserRelationship', [relationship]), - ) + return followUser({ + id: userId, + reblogs: true, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => + store.commit('updateUserRelationship', [relationship]), + ) } const muteDomain = (store, domain) => { - return store.rootState.api.backendInteractor - .muteDomain({ domain }) - .then(() => store.commit('addDomainMute', domain)) + return muteDomain({ + domain, + credentials: useOAuthStore().token, + }).then(() => store.commit('addDomainMute', domain)) } const unmuteDomain = (store, domain) => { - return store.rootState.api.backendInteractor - .unmuteDomain({ domain }) - .then(() => store.commit('removeDomainMute', domain)) + return unmuteDomain({ + domain, + credentials: useOAuthStore().token, + }).then(() => store.commit('removeDomainMute', domain)) } export const mutations = { tagUser(state, { user: { id }, tag }) { const user = state.usersObject[id] - const tags = user.tags || [] - const newTags = tags.concat([tag]) - user.tags = newTags + user.tags.add(tag) }, untagUser(state, { user: { id }, tag }) { const user = state.usersObject[id] - const tags = user.tags || [] - const newTags = tags.filter((t) => t !== tag) - user.tags = newTags + user.tags.delete(tag) }, updateRight(state, { user: { id }, right, value }) { const user = state.usersObject[id] @@ -186,9 +205,12 @@ export const mutations = { newRights[right] = value user.rights = newRights }, - updateActivationStatus(state, { user: { id }, deactivated }) { - const user = state.usersObject[id] - user.deactivated = deactivated + updateUserAdminData(state, { user }) { + const { id } = user + const localUser = state.usersObject[id] + localUser.adminData = user + localUser.deactivated = !user.is_active + localUser.tags = new Set(user.tags) }, setCurrentUser(state, user) { state.lastLoginName = user.screen_name @@ -369,60 +391,87 @@ const users = { getters, actions: { fetchUserIfMissing(store, id) { - if (!store.getters.findUser(id)) { - store.dispatch('fetchUser', id) + const user = store.getters.findUser(id) + if (!user) { + return store.dispatch('fetchUser', id) + } else { + return Promise.resolve(user) } }, - fetchUser(store, id) { - return store.rootState.api.backendInteractor - .fetchUser({ id }) + updateUserAdminData(store, { userAdminData }) { + return store + .dispatch('fetchUserIfMissing', userAdminData.id) .then((user) => { + user.adminData = userAdminData store.commit('addNewUsers', [user]) return user }) }, + fetchUser(store, id) { + return fetchUser({ + id, + credentials: useOAuthStore().token, + }) + .then(({ data: user }) => { + store.commit('addNewUsers', [user]) + return user + }) + .catch((error) => { + if (error.statusCode === 404) { + console.warn(`User ${id} not found`) + } else { + throw error + } + }) + }, fetchUserByName(store, name) { - return store.rootState.api.backendInteractor - .fetchUserByName({ name }) - .then((user) => { - store.commit('addNewUsers', [user]) - return user - }) + return fetchUserByName({ + name, + credentials: useOAuthStore().token, + }).then(({ data: user }) => { + store.commit('addNewUsers', [user]) + return user + }) }, fetchUserRelationship(store, id) { if (store.state.currentUser) { - store.rootState.api.backendInteractor - .fetchUserRelationship({ id }) - .then((relationships) => - store.commit('updateUserRelationship', relationships), - ) + fetchUserRelationship({ + id, + credentials: useOAuthStore().token, + }).then(({ data: relationships }) => + store.commit('updateUserRelationship', relationships), + ) } }, fetchUserInLists(store, id) { if (store.state.currentUser) { - store.rootState.api.backendInteractor - .fetchUserInLists({ id }) - .then((inLists) => store.commit('updateUserInLists', { id, inLists })) + fetchUserInLists({ + id, + credentials: useOAuthStore().token, + }).then(({ data: inLists }) => + store.commit('updateUserInLists', { id, inLists }), + ) } }, fetchBlocks(store, args) { const { reset } = args || {} const maxId = store.state.currentUser.blockIdsMaxId - return store.rootState.api.backendInteractor - .fetchBlocks({ maxId }) - .then((blocks) => { - if (reset) { - store.commit('saveBlockIds', map(blocks, 'id')) - } else { - map(blocks, 'id').map((id) => store.commit('addBlockId', id)) - } - if (blocks.length) { - store.commit('setBlockIdsMaxId', last(blocks).id) - } - store.commit('addNewUsers', blocks) - return blocks - }) + return fetchBlocks({ + maxId, + credentials: useOAuthStore().token, + }).then(({ data: blocks }) => { + if (reset) { + store.commit('saveBlockIds', map(blocks, 'id')) + } else { + map(blocks, 'id').map((id) => store.commit('addBlockId', id)) + } + if (blocks.length) { + store.commit('setBlockIdsMaxId', last(blocks).id) + } + store.commit('addNewUsers', blocks) + return blocks + }) }, blockUser(store, data) { return blockUser(store, data) @@ -446,20 +495,21 @@ const users = { const { reset } = args || {} const maxId = store.state.currentUser.muteIdsMaxId - return store.rootState.api.backendInteractor - .fetchMutes({ maxId }) - .then((mutes) => { - if (reset) { - store.commit('saveMuteIds', map(mutes, 'id')) - } else { - map(mutes, 'id').map((id) => store.commit('addMuteId', id)) - } - if (mutes.length) { - store.commit('setMuteIdsMaxId', last(mutes).id) - } - store.commit('addNewUsers', mutes) - return mutes - }) + return fetchMutes({ + maxId, + credentials: useOAuthStore().token, + }).then(({ data: mutes }) => { + if (reset) { + store.commit('saveMuteIds', map(mutes, 'id')) + } else { + map(mutes, 'id').map((id) => store.commit('addMuteId', id)) + } + if (mutes.length) { + store.commit('setMuteIdsMaxId', last(mutes).id) + } + store.commit('addNewUsers', mutes) + return mutes + }) }, muteUser(store, data) { return muteUser(store, data) @@ -480,12 +530,12 @@ const users = { return Promise.all(ids.map((d) => unmuteUser(store, d))) }, fetchDomainMutes(store) { - return store.rootState.api.backendInteractor - .fetchDomainMutes() - .then((domainMutes) => { - store.commit('saveDomainMutes', domainMutes) - return domainMutes - }) + return fetchDomainMutes({ + credentials: useOAuthStore().token, + }).then(({ data: domainMutes }) => { + store.commit('saveDomainMutes', domainMutes) + return domainMutes + }) }, muteDomain(store, domain) { return muteDomain(store, domain) @@ -502,24 +552,28 @@ const users = { fetchFriends({ rootState, commit }, id) { const user = rootState.users.usersObject[id] const maxId = last(user.friendIds) - return rootState.api.backendInteractor - .fetchFriends({ id, maxId }) - .then((friends) => { - commit('addNewUsers', friends) - commit('saveFriendIds', { id, friendIds: map(friends, 'id') }) - return friends - }) + return fetchFriends({ + id, + maxId, + credentials: useOAuthStore().token, + }).then(({ data: friends }) => { + commit('addNewUsers', friends) + commit('saveFriendIds', { id, friendIds: map(friends, 'id') }) + return friends + }) }, fetchFollowers({ rootState, commit }, id) { const user = rootState.users.usersObject[id] const maxId = last(user.followerIds) - return rootState.api.backendInteractor - .fetchFollowers({ id, maxId }) - .then((followers) => { - commit('addNewUsers', followers) - commit('saveFollowerIds', { id, followerIds: map(followers, 'id') }) - return followers - }) + return fetchFollowers({ + id, + maxId, + credentials: useOAuthStore().token, + }).then(({ data: followers }) => { + commit('addNewUsers', followers) + commit('saveFollowerIds', { id, followerIds: map(followers, 'id') }) + return followers + }) }, clearFriends({ commit }, userId) { commit('clearFriends', userId) @@ -528,27 +582,22 @@ const users = { commit('clearFollowers', userId) }, subscribeUser({ rootState, commit }, id) { - return rootState.api.backendInteractor - .followUser({ id, notify: true }) - .then((relationship) => - commit('updateUserRelationship', [relationship]), - ) + return followUser({ + id, + notify: true, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => + commit('updateUserRelationship', [relationship]), + ) }, unsubscribeUser({ rootState, commit }, id) { - return rootState.api.backendInteractor - .followUser({ id, notify: false }) - .then((relationship) => - commit('updateUserRelationship', [relationship]), - ) - }, - toggleActivationStatus({ rootState, commit }, { user }) { - const api = user.deactivated - ? rootState.api.backendInteractor.activateUser - : rootState.api.backendInteractor.deactivateUser - api({ user }).then((user) => { - const deactivated = !user.is_active - commit('updateActivationStatus', { user, deactivated }) - }) + return followUser({ + id, + notify: false, + credentials: useOAuthStore().token, + }).then(({ data: relationship }) => + commit('updateUserRelationship', [relationship]), + ) }, registerPushNotifications(store) { const token = store.state.currentUser.credentials @@ -609,12 +658,13 @@ const users = { }) }, searchUsers({ rootState, commit }, { query }) { - return rootState.api.backendInteractor - .searchUsers({ query }) - .then((users) => { - commit('addNewUsers', users) - return users - }) + return searchUsers({ + query, + credentials: useOAuthStore().token, + }).then(({ data: users }) => { + commit('addNewUsers', users) + return users + }) }, async signUp(store, userInfo) { const oauthStore = useOAuthStore() @@ -622,7 +672,7 @@ const users = { try { const token = await oauthStore.ensureAppToken() - const data = await apiService.register({ + const { data } = await register({ credentials: token, params: { ...userInfo }, }) @@ -643,8 +693,10 @@ const users = { throw e } }, - async getCaptcha(store) { - return store.rootState.api.backendInteractor.getCaptcha() + getCaptcha(store) { + return getCaptcha({ + credentials: useOAuthStore().token, + }).then(({ data }) => data) }, logout(store) { @@ -661,24 +713,21 @@ const users = { token: oauth.userToken, } - return oauthApi.revokeToken(params) + return revokeToken(params) }) .then(() => { store.commit('clearCurrentUser') store.dispatch('disconnectFromSocket') - oauth.clearToken() store.dispatch('stopFetchingTimeline', 'friends') - store.commit( - 'setBackendInteractor', - backendInteractorService(oauth.getToken), - ) store.dispatch('stopFetchingNotifications') - store.dispatch('stopFetchingLists') - store.dispatch('stopFetchingBookmarkFolders') + useListsStore().stopFetching() + useBookmarkFoldersStore().stopFetching() store.dispatch('stopFetchingFollowRequests') store.commit('clearNotifications') store.commit('resetStatuses') store.dispatch('resetChats') + oauth.clearToken() + Cookies.remove('__Host-pleroma_key', { path: '/' }) useInterfaceStore().setLastTimeline('public-timeline') useInterfaceStore().setLayoutWidth(windowWidth()) useInterfaceStore().setLayoutHeight(windowHeight()) @@ -688,141 +737,137 @@ const users = { return new Promise((resolve, reject) => { const commit = store.commit const dispatch = store.dispatch + commit('beginLogin') - store.rootState.api.backendInteractor - .verifyCredentials(accessToken) - .then((data) => { - if (!data.error) { - const user = data - // user.credentials = userCredentials - user.credentials = accessToken - user.blockIds = [] - user.muteIds = [] - user.domainMutes = [] - commit('setCurrentUser', user) - useSyncConfigStore() - .initSyncConfig(user) - .then(() => { - useInterfaceStore() - .applyTheme() - .catch((e) => { - console.error('Error setting theme', e) - }) - }) - useUserHighlightStore().initUserHighlight(user) - commit('addNewUsers', [user]) + verifyCredentials({ + credentials: useOAuthStore().token, + }) + .then(({ data: user }) => { + // user.credentials = userCredentials + user.credentials = accessToken + user.blockIds = [] + user.muteIds = [] + user.domainMutes = [] + commit('setCurrentUser', user) - useEmojiStore().fetchEmoji() - - getNotificationPermission().then((permission) => - useInterfaceStore().setNotificationPermission(permission), - ) - - // Set our new backend interactor - commit( - 'setBackendInteractor', - backendInteractorService(accessToken), - ) - - // Do server-side storage migrations - - // Debug snippet to clean up storage and reset migrations - /* - // Reset wordfilter - Object.keys( - useSyncConfigStore().prefsStorage.simple.muteFilters - ).forEach(key => { - useSyncConfigStore().unsetSimplePrefAndSave({ path: 'muteFilters.' + key, value: null }) + useSyncConfigStore() + .initSyncConfig(user) + .then(() => { + useInterfaceStore() + .applyTheme() + .catch((e) => { + console.error('Error setting theme', e) + }) }) + useUserHighlightStore().initUserHighlight(user) + commit('addNewUsers', [user]) - // Reset flag to 0 to re-run migrations - useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 }) - /**/ + useEmojiStore().fetchEmoji() - if (user.token) { - dispatch('setWsToken', user.token) + getNotificationPermission().then((permission) => + useInterfaceStore().setNotificationPermission(permission), + ) - // Initialize the shout socket. - dispatch('initializeSocket') - } + // Do server-side storage migrations - const startPolling = () => { - // Start getting fresh posts. - dispatch('startFetchingTimeline', { timeline: 'friends' }) + // Debug snippet to clean up storage and reset migrations + /* + // Reset wordfilter + Object.keys( + useSyncConfigStore().prefsStorage.simple.muteFilters + ).forEach(key => { + useSyncConfigStore().unsetSimplePrefAndSave({ path: 'muteFilters.' + key, value: null }) + }) - // Start fetching notifications - dispatch('startFetchingNotifications') + // Reset flag to 0 to re-run migrations + useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 }) + /**/ - if ( - useInstanceCapabilitiesStore().pleromaChatMessagesAvailable - ) { - // Start fetching chats - dispatch('startFetchingChats') - } - } + if (user.token) { + dispatch('setWsToken', user.token) - dispatch('startFetchingLists') - dispatch('startFetchingBookmarkFolders') + // Initialize the shout socket. + dispatch('initializeSocket') + } - if (user.locked) { - dispatch('startFetchingFollowRequests') - } + const startPolling = () => { + // Start getting fresh posts. + dispatch('startFetchingTimeline', { timeline: 'friends' }) - if (useMergedConfigStore().mergedConfig.useStreamingApi) { - dispatch('fetchTimeline', { timeline: 'friends', since: null }) - dispatch('fetchNotifications', { since: null }) - dispatch('enableMastoSockets', true) - .catch((error) => { - console.error( - 'Failed initializing MastoAPI Streaming socket', - error, - ) - }) - .then(() => { - dispatch('fetchChats', { latest: true }) - setTimeout( - () => dispatch('setNotificationsSilence', false), - 10000, - ) - }) - } else { - startPolling() - } + // Start fetching notifications + dispatch('startFetchingNotifications') - // Get user mutes - dispatch('fetchMutes') - - useInterfaceStore().setLayoutWidth(windowWidth()) - useInterfaceStore().setLayoutHeight(windowHeight()) - - // Fetch our friends - store.rootState.api.backendInteractor - .fetchFriends({ id: user.id }) - .then((friends) => commit('addNewUsers', friends)) - } else { - const response = data.error - // Authentication failed - commit('endLogin') - - // remove authentication token on client/authentication errors - if ([400, 401, 403, 422].includes(response.status)) { - useOAuthStore().clearToken() - } - - if (response.status === 401) { - reject(new Error('Wrong username or password')) - } else { - reject(new Error('An error occurred, please try again')) + if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) { + // Start fetching chats + dispatch('startFetchingChats') } } + + useListsStore().startFetching() + useBookmarkFoldersStore().startFetching() + + if (user.locked) { + dispatch('startFetchingFollowRequests') + } + + if (useMergedConfigStore().mergedConfig.useStreamingApi) { + dispatch('fetchTimeline', { + timeline: 'friends', + sinceId: null, + }) + dispatch('fetchNotifications', { sinceId: null }) + dispatch('enableMastoSockets', true) + .catch((error) => { + console.error( + 'Failed initializing MastoAPI Streaming socket', + error, + ) + }) + .then(() => { + dispatch('fetchChats', { latest: true }) + setTimeout( + () => dispatch('setNotificationsSilence', false), + 10000, + ) + }) + } else { + startPolling() + } + + // Start fetching things that don't need to block the UI + useAnnouncementsStore().startFetchingAnnouncements() + + dispatch('fetchMutes') + dispatch('loadDrafts') + + useInterfaceStore().setLayoutWidth(windowWidth()) + useInterfaceStore().setLayoutHeight(windowHeight()) + + // Fetch our friends + fetchFriends({ id: user.id }).then(({ data: friends }) => + commit('addNewUsers', friends), + ) commit('endLogin') resolve() }) .catch((error) => { console.error(error) + + // Authentication failed commit('endLogin') - reject(new Error('Failed to connect to server, try again')) + + // remove authentication token on client/authentication errors + if ([400, 401, 403, 422].includes(error.statusCode)) { + useOAuthStore().clearToken() + } + + commit('endLogin') + if (error.tatusCode === 401) { + throw new Error('Wrong username or password', error) + } else { + throw new Error('An error occurred, please try again', error) + } }) }) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js deleted file mode 100644 index 2e2ca5a5c..000000000 --- a/src/services/api/api.service.js +++ /dev/null @@ -1,2351 +0,0 @@ -import { concat, each, get, last, map } from 'lodash' - -import { - parseAttachment, - parseChat, - parseLinkHeaderPagination, - parseNotification, - parseSource, - parseStatus, - parseUser, -} from '../entity_normalizer/entity_normalizer.service.js' -import { RegistrationError, StatusCodeError } from '../errors/errors' - -/* eslint-env browser */ -const MUTES_IMPORT_URL = '/api/pleroma/mutes_import' -const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' -const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' -const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' -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 TAG_USER_URL = '/api/pleroma/admin/users/tag' -const PERMISSION_GROUP_URL = (screenName, right) => - `/api/pleroma/admin/users/${screenName}/permission_group/${right}` -const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate' -const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate' -const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users' -const SUGGESTIONS_URL = '/api/v1/suggestions' -const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' -const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' - -const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' -const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' - -const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp' -const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp' -const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp' - -const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' -const MASTODON_REGISTRATION_URL = '/api/v1/accounts' -const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' -const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' -const MASTODON_DISMISS_NOTIFICATION_URL = (id) => - `/api/v1/notifications/${id}/dismiss` -const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite` -const MASTODON_UNFAVORITE_URL = (id) => `/api/v1/statuses/${id}/unfavourite` -const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog` -const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog` -const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}` -const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow` -const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow` -const MASTODON_FOLLOWING_URL = (id) => `/api/v1/accounts/${id}/following` -const MASTODON_FOLLOWERS_URL = (id) => `/api/v1/accounts/${id}/followers` -const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests' -const MASTODON_APPROVE_USER_URL = (id) => - `/api/v1/follow_requests/${id}/authorize` -const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject` -const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct' -const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public' -const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' -const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}` -const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context` -const MASTODON_STATUS_SOURCE_URL = (id) => `/api/v1/statuses/${id}/source` -const MASTODON_STATUS_HISTORY_URL = (id) => `/api/v1/statuses/${id}/history` -const MASTODON_USER_URL = '/api/v1/accounts' -const MASTODON_USER_LOOKUP_URL = '/api/v1/accounts/lookup' -const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' -const MASTODON_USER_TIMELINE_URL = (id) => `/api/v1/accounts/${id}/statuses` -const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists` -const MASTODON_LIST_URL = (id) => `/api/v1/lists/${id}` -const MASTODON_LIST_TIMELINE_URL = (id) => `/api/v1/timelines/list/${id}` -const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts` -const MASTODON_TAG_TIMELINE_URL = (tag) => `/api/v1/timelines/tag/${tag}` -const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks' -const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble' -const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/' -const MASTODON_USER_MUTES_URL = '/api/v1/mutes/' -const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block` -const MASTODON_UNBLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/unblock` -const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute` -const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute` -const MASTODON_REMOVE_USER_FROM_FOLLOWERS = (id) => - `/api/v1/accounts/${id}/remove_from_followers` -const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note` -const MASTODON_BOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark` -const MASTODON_UNBOOKMARK_STATUS_URL = (id) => - `/api/v1/statuses/${id}/unbookmark` -const MASTODON_POST_STATUS_URL = '/api/v1/statuses' -const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' -const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes` -const MASTODON_POLL_URL = (id) => `/api/v1/polls/${id}` -const MASTODON_STATUS_FAVORITEDBY_URL = (id) => - `/api/v1/statuses/${id}/favourited_by` -const MASTODON_STATUS_REBLOGGEDBY_URL = (id) => - `/api/v1/statuses/${id}/reblogged_by` -const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' -const MASTODON_REPORT_USER_URL = '/api/v1/reports' -const MASTODON_PIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/pin` -const MASTODON_UNPIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/unpin` -const MASTODON_MUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/mute` -const MASTODON_UNMUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/unmute` -const MASTODON_SEARCH_2 = '/api/v2/search' -const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search' -const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' -const MASTODON_LISTS_URL = '/api/v1/lists' -const MASTODON_STREAMING = '/api/v1/streaming' -const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' -const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements' -const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) => - `/api/v1/announcements/${id}/dismiss` -const PLEROMA_EMOJI_REACTIONS_URL = (id) => - `/api/v1/pleroma/statuses/${id}/reactions` -const PLEROMA_EMOJI_REACT_URL = (id, emoji) => - `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` -const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => - `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` -const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats' -const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}` -const PLEROMA_CHAT_MESSAGES_URL = (id) => `/api/v1/pleroma/chats/${id}/messages` -const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read` -const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => - `/api/v1/pleroma/chats/${chatId}/messages/${messageId}` -const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports' -const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups' -const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements' -const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' -const PLEROMA_EDIT_ANNOUNCEMENT_URL = (id) => - `/api/v1/pleroma/admin/announcements/${id}` -const PLEROMA_DELETE_ANNOUNCEMENT_URL = (id) => - `/api/v1/pleroma/admin/announcements/${id}` -const PLEROMA_SCROBBLES_URL = (id) => `/api/v1/pleroma/accounts/${id}/scrobbles` -const PLEROMA_STATUS_QUOTES_URL = (id) => - `/api/v1/pleroma/statuses/${id}/quotes` -const PLEROMA_USER_FAVORITES_TIMELINE_URL = (id) => - `/api/v1/pleroma/accounts/${id}/favourites` -const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders' -const PLEROMA_BOOKMARK_FOLDER_URL = (id) => - `/api/v1/pleroma/bookmark_folders/${id}` - -const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config' -const PLEROMA_ADMIN_DESCRIPTIONS_URL = - '/api/v1/pleroma/admin/config/descriptions' -const PLEROMA_ADMIN_FRONTENDS_URL = '/api/v1/pleroma/admin/frontends' -const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = - '/api/v1/pleroma/admin/frontends/install' - -const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' -const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' -const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => - `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}` -const PLEROMA_EMOJI_PACK_URL = (name) => - `/api/v1/pleroma/emoji/pack?name=${name}` -const PLEROMA_EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download' -const PLEROMA_EMOJI_PACKS_DL_REMOTE_ZIP_URL = - '/api/v1/pleroma/emoji/packs/download_zip' -const PLEROMA_EMOJI_PACKS_LS_REMOTE_URL = (url, page, pageSize) => - `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}` -const PLEROMA_EMOJI_UPDATE_FILE_URL = (name) => - `/api/v1/pleroma/emoji/packs/files?name=${name}` - -const oldfetch = window.fetch - -const fetch = (url, options) => { - options = options || {} - const baseUrl = '' - const fullUrl = baseUrl + url - options.credentials = 'same-origin' - return oldfetch(fullUrl, options) -} - -const promisedRequest = ({ - method, - url, - params, - payload, - credentials, - headers = {}, -}) => { - const options = { - method, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...headers, - }, - } - if (params) { - url += - '?' + - Object.entries(params) - .map( - ([key, value]) => - encodeURIComponent(key) + '=' + encodeURIComponent(value), - ) - .join('&') - } - if (payload) { - options.body = JSON.stringify(payload) - } - if (credentials) { - options.headers = { - ...options.headers, - ...authHeaders(credentials), - } - } - return fetch(url, options).then((response) => { - return new Promise((resolve, reject) => - response - .json() - .then((json) => { - if (!response.ok) { - return reject( - new StatusCodeError( - response.status, - json, - { url, options }, - response, - ), - ) - } - return resolve(json) - }) - .catch((error) => { - return reject( - new StatusCodeError( - response.status, - error, - { url, options }, - response, - ), - ) - }), - ) - }) -} - -const updateNotificationSettings = ({ credentials, settings }) => { - const form = new FormData() - - each(settings, (value, key) => { - form.append(key, value) - }) - - return fetch( - `${NOTIFICATION_SETTINGS_URL}?${new URLSearchParams(settings)}`, - { - headers: authHeaders(credentials), - method: 'PUT', - body: form, - }, - ).then((data) => data.json()) -} - -const updateProfileImages = ({ - credentials, - avatar = null, - avatarName = null, - banner = null, - background = null, -}) => { - const form = new FormData() - if (avatar !== null) { - if (avatarName !== null) { - form.append('avatar', avatar, avatarName) - } else { - form.append('avatar', avatar) - } - } - if (banner !== null) form.append('header', banner) - if (background !== null) form.append('pleroma_background_image', background) - return fetch(MASTODON_PROFILE_UPDATE_URL, { - headers: authHeaders(credentials), - method: 'PATCH', - body: form, - }) - .then((data) => data.json()) - .then((data) => { - if (data.error) { - throw new Error(data.error) - } - return parseUser(data) - }) -} - -const updateProfile = ({ credentials, params }) => { - const formData = new FormData() - - for (const name in params) { - if (name === 'fields_attributes') { - params[name].forEach((param, i) => { - formData.append(name + `[${i}][name]`, param.name) - formData.append(name + `[${i}][value]`, param.value) - }) - } else { - if (typeof params[name] === 'object') { - console.warn( - 'Object detected in updateProfile API call. This will not work, use updateProfileJSON instead.', - ) - console.warn('Object:\n' + JSON.stringify(params[name], null, 2)) - } - formData.append(name, params[name]) - } - } - - return fetch(MASTODON_PROFILE_UPDATE_URL, { - headers: authHeaders(credentials), - method: 'PATCH', - body: formData, - }) - .then((data) => data.json()) - .then((data) => parseUser(data)) -} - -const updateProfileJSON = ({ credentials, params }) => { - return promisedRequest({ - url: MASTODON_PROFILE_UPDATE_URL, - credentials, - payload: params, - method: 'PATCH', - }).then((data) => parseUser(data)) -} - -// Params needed: -// nickname -// email -// fullname -// password -// password_confirm -// -// Optional -// bio -// homepage -// location -// token -// language -const register = ({ params, credentials }) => { - const { nickname, ...rest } = params - return fetch(MASTODON_REGISTRATION_URL, { - method: 'POST', - headers: { - ...authHeaders(credentials), - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - nickname, - locale: 'en_US', - agreement: true, - ...rest, - }), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return response.json().then((error) => { - throw new RegistrationError(error) - }) - } - }) -} - -const getCaptcha = () => - fetch('/api/pleroma/captcha').then((resp) => resp.json()) - -const authHeaders = (accessToken) => { - if (accessToken) { - return { Authorization: `Bearer ${accessToken}` } - } else { - return {} - } -} - -const followUser = ({ id, credentials, ...options }) => { - const url = MASTODON_FOLLOW_URL(id) - const form = {} - if (options.reblogs !== undefined) { - form.reblogs = options.reblogs - } - if (options.notify !== undefined) { - form.notify = options.notify - } - return fetch(url, { - body: JSON.stringify(form), - headers: { - ...authHeaders(credentials), - 'Content-Type': 'application/json', - }, - method: 'POST', - }).then((data) => data.json()) -} - -const unfollowUser = ({ id, credentials }) => { - const url = MASTODON_UNFOLLOW_URL(id) - return fetch(url, { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const fetchUserInLists = ({ id, credentials }) => { - const url = MASTODON_USER_IN_LISTS(id) - return fetch(url, { - headers: authHeaders(credentials), - }).then((data) => data.json()) -} - -const pinOwnStatus = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_PIN_OWN_STATUS(id), - credentials, - method: 'POST', - }).then((data) => parseStatus(data)) -} - -const unpinOwnStatus = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNPIN_OWN_STATUS(id), - credentials, - method: 'POST', - }).then((data) => parseStatus(data)) -} - -const muteConversation = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_MUTE_CONVERSATION(id), - credentials, - method: 'POST', - }).then((data) => parseStatus(data)) -} - -const unmuteConversation = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNMUTE_CONVERSATION(id), - credentials, - method: 'POST', - }).then((data) => parseStatus(data)) -} - -const blockUser = ({ id, expiresIn, credentials }) => { - const payload = {} - if (expiresIn) { - payload.duration = expiresIn - } - - return promisedRequest({ - url: MASTODON_BLOCK_USER_URL(id), - credentials, - method: 'POST', - payload, - }) -} - -const unblockUser = ({ id, credentials }) => { - return fetch(MASTODON_UNBLOCK_USER_URL(id), { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const removeUserFromFollowers = ({ id, credentials }) => { - return fetch(MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const editUserNote = ({ id, credentials, comment }) => { - return promisedRequest({ - url: MASTODON_USER_NOTE_URL(id), - credentials, - payload: { - comment, - }, - method: 'POST', - }) -} - -const approveUser = ({ id, credentials }) => { - const url = MASTODON_APPROVE_USER_URL(id) - return fetch(url, { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const denyUser = ({ id, credentials }) => { - const url = MASTODON_DENY_USER_URL(id) - return fetch(url, { - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const fetchUser = ({ id, credentials }) => { - const url = `${MASTODON_USER_URL}/${id}` - return promisedRequest({ url, credentials }).then((data) => parseUser(data)) -} - -const fetchUserByName = ({ name, credentials }) => { - return promisedRequest({ - url: MASTODON_USER_LOOKUP_URL, - credentials, - params: { acct: name }, - }) - .then((data) => data.id) - .catch((error) => { - if (error && error.statusCode === 404) { - // Either the backend does not support lookup endpoint, - // or there is no user with such name. Fallback and treat name as id. - return name - } else { - throw error - } - }) - .then((id) => fetchUser({ id, credentials })) -} - -const fetchUserRelationship = ({ id, credentials }) => { - const url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}` - return fetch(url, { headers: authHeaders(credentials) }).then((response) => { - return new Promise((resolve, reject) => - response.json().then((json) => { - if (!response.ok) { - return reject( - new StatusCodeError(response.status, json, { url }, response), - ) - } - return resolve(json) - }), - ) - }) -} - -const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => { - let url = MASTODON_FOLLOWING_URL(id) - const args = [ - maxId && `max_id=${maxId}`, - sinceId && `since_id=${sinceId}`, - limit && `limit=${limit}`, - 'with_relationships=true', - ] - .filter((_) => _) - .join('&') - - url = url + (args ? '?' + args : '') - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => data.map(parseUser)) -} - -const exportFriends = ({ id, credentials }) => { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO refactor this - return new Promise(async (resolve, reject) => { - try { - let friends = [] - let more = true - while (more) { - const maxId = friends.length > 0 ? last(friends).id : undefined - const users = await fetchFriends({ id, maxId, credentials }) - friends = concat(friends, users) - if (users.length === 0) { - more = false - } - } - resolve(friends) - } catch (err) { - reject(err) - } - }) -} - -const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => { - let url = MASTODON_FOLLOWERS_URL(id) - const args = [ - maxId && `max_id=${maxId}`, - sinceId && `since_id=${sinceId}`, - limit && `limit=${limit}`, - 'with_relationships=true', - ] - .filter((_) => _) - .join('&') - - url += args ? '?' + args : '' - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => data.map(parseUser)) -} - -const fetchFollowRequests = ({ credentials }) => { - const url = MASTODON_FOLLOW_REQUESTS_URL - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => data.map(parseUser)) -} - -const fetchLists = ({ credentials }) => { - const url = MASTODON_LISTS_URL - return fetch(url, { headers: authHeaders(credentials) }).then((data) => - data.json(), - ) -} - -const createList = ({ title, credentials }) => { - const url = MASTODON_LISTS_URL - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'POST', - body: JSON.stringify({ title }), - }).then((data) => data.json()) -} - -const getList = ({ listId, credentials }) => { - const url = MASTODON_LIST_URL(listId) - return fetch(url, { headers: authHeaders(credentials) }).then((data) => - data.json(), - ) -} - -const updateList = ({ listId, title, credentials }) => { - const url = MASTODON_LIST_URL(listId) - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'PUT', - body: JSON.stringify({ title }), - }) -} - -const getListAccounts = ({ listId, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(listId) - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => data.map(({ id }) => id)) -} - -const addAccountsToList = ({ listId, accountIds, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(listId) - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'POST', - body: JSON.stringify({ account_ids: accountIds }), - }) -} - -const removeAccountsFromList = ({ listId, accountIds, credentials }) => { - const url = MASTODON_LIST_ACCOUNTS_URL(listId) - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'DELETE', - body: JSON.stringify({ account_ids: accountIds }), - }) -} - -const deleteList = ({ listId, credentials }) => { - const url = MASTODON_LIST_URL(listId) - return fetch(url, { - method: 'DELETE', - headers: authHeaders(credentials), - }) -} - -const fetchConversation = ({ id, credentials }) => { - const urlContext = MASTODON_STATUS_CONTEXT_URL(id) - return fetch(urlContext, { headers: authHeaders(credentials) }) - .then((data) => { - if (data.ok) { - return data - } - throw new Error('Error fetching timeline', data) - }) - .then((data) => data.json()) - .then(({ ancestors, descendants }) => ({ - ancestors: ancestors.map(parseStatus), - descendants: descendants.map(parseStatus), - })) -} - -const fetchStatus = ({ id, credentials }) => { - const url = MASTODON_STATUS_URL(id) - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => { - if (data.ok) { - return data - } - throw new Error('Error fetching timeline', { cause: data }) - }) - .then((data) => data.json()) - .then((data) => parseStatus(data)) -} - -const fetchStatusSource = ({ id, credentials }) => { - const url = MASTODON_STATUS_SOURCE_URL(id) - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => { - if (data.ok) { - return data - } - throw new Error('Error fetching source', { cause: data }) - }) - .then((data) => data.json()) - .then((data) => parseSource(data)) -} - -const fetchStatusHistory = ({ status, credentials }) => { - const url = MASTODON_STATUS_HISTORY_URL(status.id) - return promisedRequest({ url, credentials }).then((data) => { - data.reverse() - return data.map((item) => { - item.originalStatus = status - return parseStatus(item) - }) - }) -} - -const tagUser = ({ tag, credentials, user }) => { - const screenName = user.screen_name - const form = { - nicknames: [screenName], - tags: [tag], - } - - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(TAG_USER_URL, { - method: 'PUT', - headers, - body: JSON.stringify(form), - }) -} - -const untagUser = ({ tag, credentials, user }) => { - const screenName = user.screen_name - const body = { - nicknames: [screenName], - tags: [tag], - } - - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(TAG_USER_URL, { - method: 'DELETE', - headers, - body: JSON.stringify(body), - }) -} - -const addRight = ({ right, credentials, user }) => { - const screenName = user.screen_name - - return fetch(PERMISSION_GROUP_URL(screenName, right), { - method: 'POST', - headers: authHeaders(credentials), - body: {}, - }) -} - -const deleteRight = ({ right, credentials, user }) => { - const screenName = user.screen_name - - return fetch(PERMISSION_GROUP_URL(screenName, right), { - method: 'DELETE', - headers: authHeaders(credentials), - body: {}, - }) -} - -const activateUser = ({ credentials, user: { screen_name: nickname } }) => { - return promisedRequest({ - url: ACTIVATE_USER_URL, - method: 'PATCH', - credentials, - payload: { - nicknames: [nickname], - }, - }).then((response) => get(response, 'users.0')) -} - -const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => { - return promisedRequest({ - url: DEACTIVATE_USER_URL, - method: 'PATCH', - credentials, - payload: { - nicknames: [nickname], - }, - }).then((response) => get(response, 'users.0')) -} - -const deleteUser = ({ credentials, user }) => { - const screenName = user.screen_name - const headers = authHeaders(credentials) - - return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, { - method: 'DELETE', - headers, - }) -} - -const fetchTimeline = ({ - timeline, - credentials, - since = false, - minId = false, - until = false, - userId = false, - listId = false, - statusId = false, - tag = false, - withMuted = false, - replyVisibility = 'all', - includeTypes = [], - bookmarkFolderId = false, -}) => { - const timelineUrls = { - public: MASTODON_PUBLIC_TIMELINE, - friends: MASTODON_USER_HOME_TIMELINE_URL, - dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, - notifications: MASTODON_USER_NOTIFICATIONS_URL, - publicAndExternal: MASTODON_PUBLIC_TIMELINE, - user: MASTODON_USER_TIMELINE_URL, - media: MASTODON_USER_TIMELINE_URL, - list: MASTODON_LIST_TIMELINE_URL, - favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, - publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL, - tag: MASTODON_TAG_TIMELINE_URL, - bookmarks: MASTODON_BOOKMARK_TIMELINE_URL, - quotes: PLEROMA_STATUS_QUOTES_URL, - bubble: AKKOMA_BUBBLE_TIMELINE_URL, - } - const isNotifications = timeline === 'notifications' - const params = [] - - let url = timelineUrls[timeline] - - if (timeline === 'favorites' && userId) { - url = timelineUrls.publicFavorites(userId) - } - - if (timeline === 'user' || timeline === 'media') { - url = url(userId) - } - - if (timeline === 'list') { - url = url(listId) - } - - if (timeline === 'quotes') { - url = url(statusId) - } - - if (minId) { - params.push(['min_id', minId]) - } - if (since) { - params.push(['since_id', since]) - } - if (until) { - params.push(['max_id', until]) - } - if (tag) { - url = url(tag) - } - if (timeline === 'media') { - params.push(['only_media', 1]) - } - if (timeline === 'public') { - params.push(['local', true]) - } - if (timeline === 'public' || timeline === 'publicAndExternal') { - params.push(['only_media', false]) - } - if (timeline !== 'favorites' && timeline !== 'bookmarks') { - params.push(['with_muted', withMuted]) - } - if (replyVisibility !== 'all') { - params.push(['reply_visibility', replyVisibility]) - } - if (includeTypes.size > 0) { - includeTypes.forEach((type) => { - params.push(['include_types[]', type]) - }) - } - if (timeline === 'bookmarks' && bookmarkFolderId) { - params.push(['folder_id', bookmarkFolderId]) - } - - params.push(['limit', 20]) - - const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join( - '&', - ) - url += `?${queryString}` - - return fetch(url, { headers: authHeaders(credentials) }).then( - async (response) => { - const success = response.ok - - const data = await response.json() - - if (success && !data.errors) { - const pagination = parseLinkHeaderPagination( - response.headers.get('Link'), - { - flakeId: timeline !== 'bookmarks' && timeline !== 'notifications', - }, - ) - - return { - data: data.map(isNotifications ? parseNotification : parseStatus), - pagination, - } - } else { - data.errors ||= [] - data.status = response.status - data.statusText = response.statusText - return data - } - }, - ) -} - -const fetchPinnedStatuses = ({ id, credentials }) => { - const url = MASTODON_USER_TIMELINE_URL(id) + '?pinned=true' - return promisedRequest({ url, credentials }).then((data) => - data.map(parseStatus), - ) -} - -const verifyCredentials = (user) => { - return fetch(MASTODON_LOGIN_URL, { - headers: authHeaders(user), - }) - .then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) - .then((data) => (data.error ? data : parseUser(data))) -} - -const favorite = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_FAVORITE_URL(id), - method: 'POST', - credentials, - }).then((data) => parseStatus(data)) -} - -const unfavorite = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNFAVORITE_URL(id), - method: 'POST', - credentials, - }).then((data) => parseStatus(data)) -} - -const retweet = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_RETWEET_URL(id), - method: 'POST', - credentials, - }).then((data) => parseStatus(data)) -} - -const unretweet = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNRETWEET_URL(id), - method: 'POST', - credentials, - }).then((data) => parseStatus(data)) -} - -const bookmarkStatus = ({ id, credentials, ...options }) => { - return promisedRequest({ - url: MASTODON_BOOKMARK_STATUS_URL(id), - headers: authHeaders(credentials), - method: 'POST', - payload: { - folder_id: options.folder_id, - }, - }) -} - -const unbookmarkStatus = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNBOOKMARK_STATUS_URL(id), - headers: authHeaders(credentials), - method: 'POST', - }) -} - -const postStatus = ({ - credentials, - status, - spoilerText, - visibility, - sensitive, - poll, - mediaIds = [], - inReplyToStatusId, - quoteId, - contentType, - preview, - idempotencyKey, -}) => { - const form = new FormData() - const pollOptions = poll.options || [] - - form.append('status', status) - form.append('source', 'Pleroma FE') - if (spoilerText) form.append('spoiler_text', spoilerText) - if (visibility) form.append('visibility', visibility) - if (sensitive) form.append('sensitive', sensitive) - if (contentType) form.append('content_type', contentType) - mediaIds.forEach((val) => { - form.append('media_ids[]', val) - }) - if (pollOptions.some((option) => option !== '')) { - const normalizedPoll = { - expires_in: parseInt(poll.expiresIn, 10), - multiple: poll.multiple, - } - Object.keys(normalizedPoll).forEach((key) => { - form.append(`poll[${key}]`, normalizedPoll[key]) - }) - - pollOptions.forEach((option) => { - form.append('poll[options][]', option) - }) - } - if (inReplyToStatusId) { - form.append('in_reply_to_id', inReplyToStatusId) - } - if (quoteId) { - form.append('quote_id', quoteId) - } - if (preview) { - form.append('preview', 'true') - } - - const postHeaders = authHeaders(credentials) - if (idempotencyKey) { - postHeaders['idempotency-key'] = idempotencyKey - } - - return fetch(MASTODON_POST_STATUS_URL, { - body: form, - method: 'POST', - headers: postHeaders, - }) - .then((response) => { - return response.json() - }) - .then((data) => (data.error ? data : parseStatus(data))) -} - -const editStatus = ({ - id, - credentials, - status, - spoilerText, - sensitive, - poll, - mediaIds = [], - contentType, -}) => { - const form = new FormData() - const pollOptions = poll.options || [] - - form.append('status', status) - if (spoilerText) form.append('spoiler_text', spoilerText) - if (sensitive) form.append('sensitive', sensitive) - if (contentType) form.append('content_type', contentType) - mediaIds.forEach((val) => { - form.append('media_ids[]', val) - }) - - if (pollOptions.some((option) => option !== '')) { - const normalizedPoll = { - expires_in: parseInt(poll.expiresIn, 10), - multiple: poll.multiple, - } - Object.keys(normalizedPoll).forEach((key) => { - form.append(`poll[${key}]`, normalizedPoll[key]) - }) - - pollOptions.forEach((option) => { - form.append('poll[options][]', option) - }) - } - - const putHeaders = authHeaders(credentials) - - return fetch(MASTODON_STATUS_URL(id), { - body: form, - method: 'PUT', - headers: putHeaders, - }) - .then((response) => { - return response.json() - }) - .then((data) => (data.error ? data : parseStatus(data))) -} - -const deleteStatus = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_DELETE_URL(id), - credentials, - method: 'DELETE', - }) -} - -const uploadMedia = ({ formData, credentials }) => { - return fetch(MASTODON_MEDIA_UPLOAD_URL, { - body: formData, - method: 'POST', - headers: authHeaders(credentials), - }) - .then((data) => data.json()) - .then((data) => parseAttachment(data)) -} - -const setMediaDescription = ({ id, description, credentials }) => { - return promisedRequest({ - url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`, - method: 'PUT', - headers: authHeaders(credentials), - payload: { - description, - }, - }).then((data) => parseAttachment(data)) -} - -const importMutes = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return fetch(MUTES_IMPORT_URL, { - body: formData, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.ok) -} - -const importBlocks = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return fetch(BLOCKS_IMPORT_URL, { - body: formData, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.ok) -} - -const importFollows = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return fetch(FOLLOW_IMPORT_URL, { - body: formData, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.ok) -} - -const deleteAccount = ({ credentials, password }) => { - const form = new FormData() - - form.append('password', password) - - return fetch(DELETE_ACCOUNT_URL, { - body: form, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const changeEmail = ({ credentials, email, password }) => { - const form = new FormData() - - form.append('email', email) - form.append('password', password) - - return fetch(CHANGE_EMAIL_URL, { - body: form, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const moveAccount = ({ credentials, password, targetAccount }) => { - const form = new FormData() - - form.append('password', password) - form.append('target_account', targetAccount) - - return fetch(MOVE_ACCOUNT_URL, { - body: form, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const addAlias = ({ credentials, alias }) => { - return promisedRequest({ - url: ALIASES_URL, - method: 'PUT', - credentials, - payload: { alias }, - }) -} - -const deleteAlias = ({ credentials, alias }) => { - return promisedRequest({ - url: ALIASES_URL, - method: 'DELETE', - credentials, - payload: { alias }, - }) -} - -const listAliases = ({ credentials }) => { - return promisedRequest({ - url: ALIASES_URL, - method: 'GET', - credentials, - params: { - _cacheBooster: new Date().getTime(), - }, - }) -} - -const changePassword = ({ - credentials, - password, - newPassword, - newPasswordConfirmation, -}) => { - const form = new FormData() - - form.append('password', password) - form.append('new_password', newPassword) - form.append('new_password_confirmation', newPasswordConfirmation) - - return fetch(CHANGE_PASSWORD_URL, { - body: form, - method: 'POST', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const settingsMFA = ({ credentials }) => { - return fetch(MFA_SETTINGS_URL, { - headers: authHeaders(credentials), - method: 'GET', - }).then((data) => data.json()) -} - -const mfaDisableOTP = ({ credentials, password }) => { - const form = new FormData() - - form.append('password', password) - - return fetch(MFA_DISABLE_OTP_URL, { - body: form, - method: 'DELETE', - headers: authHeaders(credentials), - }).then((response) => response.json()) -} - -const mfaConfirmOTP = ({ credentials, password, token }) => { - const form = new FormData() - - form.append('password', password) - form.append('code', token) - - return fetch(MFA_CONFIRM_OTP_URL, { - body: form, - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} -const mfaSetupOTP = ({ credentials }) => { - return fetch(MFA_SETUP_OTP_URL, { - headers: authHeaders(credentials), - method: 'GET', - }).then((data) => data.json()) -} -const generateMfaBackupCodes = ({ credentials }) => { - return fetch(MFA_BACKUP_CODES_URL, { - headers: authHeaders(credentials), - method: 'GET', - }).then((data) => data.json()) -} - -const fetchMutes = ({ maxId, credentials }) => { - const query = new URLSearchParams({ with_relationships: true }) - if (maxId) { - query.append('max_id', maxId) - } - return promisedRequest({ - url: `${MASTODON_USER_MUTES_URL}?${query.toString()}`, - credentials, - }).then((users) => users.map(parseUser)) -} - -const muteUser = ({ id, expiresIn, credentials }) => { - const payload = {} - if (expiresIn) { - payload.expires_in = expiresIn - } - - return promisedRequest({ - url: MASTODON_MUTE_USER_URL(id), - credentials, - method: 'POST', - payload, - }) -} - -const unmuteUser = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_UNMUTE_USER_URL(id), - credentials, - method: 'POST', - }) -} - -const fetchBlocks = ({ maxId, credentials }) => { - const query = new URLSearchParams({ with_relationships: true }) - if (maxId) { - query.append('max_id', maxId) - } - return promisedRequest({ - url: `${MASTODON_USER_BLOCKS_URL}?${query.toString()}`, - credentials, - }).then((users) => users.map(parseUser)) -} - -const addBackup = ({ credentials }) => { - return promisedRequest({ - url: PLEROMA_BACKUP_URL, - method: 'POST', - credentials, - }) -} - -const listBackups = ({ credentials }) => { - return promisedRequest({ - url: PLEROMA_BACKUP_URL, - method: 'GET', - credentials, - params: { - _cacheBooster: new Date().getTime(), - }, - }) -} - -const fetchOAuthTokens = ({ credentials }) => { - const url = '/api/oauth_tokens.json' - - return fetch(url, { - headers: authHeaders(credentials), - }).then((data) => { - if (data.ok) { - return data.json() - } - throw new Error('Error fetching auth tokens', data) - }) -} - -const revokeOAuthToken = ({ id, credentials }) => { - const url = `/api/oauth_tokens/${id}` - - return fetch(url, { - headers: authHeaders(credentials), - method: 'DELETE', - }) -} - -const suggestions = ({ credentials }) => { - return fetch(SUGGESTIONS_URL, { - headers: authHeaders(credentials), - }).then((data) => data.json()) -} - -const markNotificationsAsSeen = ({ id, credentials, single = false }) => { - const body = new FormData() - - if (single) { - body.append('id', id) - } else { - body.append('max_id', id) - } - - return fetch(NOTIFICATION_READ_URL, { - body, - headers: authHeaders(credentials), - method: 'POST', - }).then((data) => data.json()) -} - -const vote = ({ pollId, choices, credentials }) => { - const form = new FormData() - form.append('choices', choices) - - return promisedRequest({ - url: MASTODON_VOTE_URL(encodeURIComponent(pollId)), - method: 'POST', - credentials, - payload: { - choices, - }, - }) -} - -const fetchPoll = ({ pollId, credentials }) => { - return promisedRequest({ - url: MASTODON_POLL_URL(encodeURIComponent(pollId)), - method: 'GET', - credentials, - }) -} - -const fetchFavoritedByUsers = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_STATUS_FAVORITEDBY_URL(id), - method: 'GET', - credentials, - }).then((users) => users.map(parseUser)) -} - -const fetchRebloggedByUsers = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_STATUS_REBLOGGEDBY_URL(id), - method: 'GET', - credentials, - }).then((users) => users.map(parseUser)) -} - -const fetchEmojiReactions = ({ id, credentials }) => { - return promisedRequest({ - url: PLEROMA_EMOJI_REACTIONS_URL(id), - credentials, - }).then((reactions) => - reactions.map((r) => { - r.accounts = r.accounts.map(parseUser) - return r - }), - ) -} - -const reactWithEmoji = ({ id, emoji, credentials }) => { - return promisedRequest({ - url: PLEROMA_EMOJI_REACT_URL(id, emoji), - method: 'PUT', - credentials, - }).then(parseStatus) -} - -const unreactWithEmoji = ({ id, emoji, credentials }) => { - return promisedRequest({ - url: PLEROMA_EMOJI_UNREACT_URL(id, emoji), - method: 'DELETE', - credentials, - }).then(parseStatus) -} - -const reportUser = ({ credentials, userId, statusIds, comment, forward }) => { - return promisedRequest({ - url: MASTODON_REPORT_USER_URL, - method: 'POST', - payload: { - account_id: userId, - status_ids: statusIds, - comment, - forward, - }, - credentials, - }) -} - -const searchUsers = ({ credentials, query }) => { - return promisedRequest({ - url: MASTODON_USER_SEARCH_URL, - params: { - q: query, - resolve: true, - }, - credentials, - }).then((data) => data.map(parseUser)) -} - -const search2 = ({ - credentials, - q, - resolve, - limit, - offset, - following, - type, -}) => { - let url = MASTODON_SEARCH_2 - const params = [] - - if (q) { - params.push(['q', encodeURIComponent(q)]) - } - - if (resolve) { - params.push(['resolve', resolve]) - } - - if (limit) { - params.push(['limit', limit]) - } - - if (offset) { - params.push(['offset', offset]) - } - - if (following) { - params.push(['following', true]) - } - - if (type) { - params.push(['type', type]) - } - - params.push(['with_relationships', true]) - - const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join( - '&', - ) - url += `?${queryString}` - - return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => { - if (data.ok) { - return data - } - throw new Error('Error fetching search result', data) - }) - .then((data) => { - return data.json() - }) - .then((data) => { - data.accounts = data.accounts.slice(0, limit).map((u) => parseUser(u)) - data.statuses = data.statuses.slice(0, limit).map((s) => parseStatus(s)) - return data - }) -} - -const fetchKnownDomains = ({ credentials }) => { - return promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) -} - -const fetchDomainMutes = ({ credentials }) => { - return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials }) -} - -const muteDomain = ({ domain, credentials }) => { - return promisedRequest({ - url: MASTODON_DOMAIN_BLOCKS_URL, - method: 'POST', - payload: { domain }, - credentials, - }) -} - -const unmuteDomain = ({ domain, credentials }) => { - return promisedRequest({ - url: MASTODON_DOMAIN_BLOCKS_URL, - method: 'DELETE', - payload: { domain }, - credentials, - }) -} - -const dismissNotification = ({ credentials, id }) => { - return promisedRequest({ - url: MASTODON_DISMISS_NOTIFICATION_URL(id), - method: 'POST', - payload: { id }, - credentials, - }) -} - -const adminFetchAnnouncements = ({ credentials }) => { - return promisedRequest({ url: PLEROMA_ANNOUNCEMENTS_URL, credentials }) -} - -const fetchAnnouncements = ({ credentials }) => { - return promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials }) -} - -const dismissAnnouncement = ({ id, credentials }) => { - return promisedRequest({ - url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id), - credentials, - method: 'POST', - }) -} - -const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => { - const payload = { content } - - if (typeof startsAt !== 'undefined') { - payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null - } - - if (typeof endsAt !== 'undefined') { - payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null - } - - if (typeof allDay !== 'undefined') { - payload.all_day = allDay - } - - return payload -} - -const postAnnouncement = ({ - credentials, - content, - startsAt, - endsAt, - allDay, -}) => { - return promisedRequest({ - url: PLEROMA_POST_ANNOUNCEMENT_URL, - credentials, - method: 'POST', - payload: announcementToPayload({ content, startsAt, endsAt, allDay }), - }) -} - -const editAnnouncement = ({ - id, - credentials, - content, - startsAt, - endsAt, - allDay, -}) => { - return promisedRequest({ - url: PLEROMA_EDIT_ANNOUNCEMENT_URL(id), - credentials, - method: 'PATCH', - payload: announcementToPayload({ content, startsAt, endsAt, allDay }), - }) -} - -const deleteAnnouncement = ({ id, credentials }) => { - return promisedRequest({ - url: PLEROMA_DELETE_ANNOUNCEMENT_URL(id), - credentials, - method: 'DELETE', - }) -} - -export const getMastodonSocketURI = ( - { credentials, stream, args = {} }, - base, -) => { - const url = new URL(MASTODON_STREAMING, base) - if (credentials) { - url.searchParams.append('access_token', credentials) - } - if (stream) { - url.searchParams.append('stream', stream) - } - Object.entries(args).forEach(([key, val]) => { - url.searchParams.append(key, val) - }) - return url -} - -const MASTODON_STREAMING_EVENTS = new Set([ - 'update', - 'notification', - 'delete', - 'filters_changed', - 'status.update', -]) - -const PLEROMA_STREAMING_EVENTS = new Set([ - 'pleroma:chat_update', - 'pleroma:respond', -]) - -// A thin wrapper around WebSocket API that allows adding a pre-processor to it -// Uses EventTarget and a CustomEvent to proxy events -export const ProcessedWS = ({ - url, - preprocessor = handleMastoWS, - id = 'Unknown', - credentials, -}) => { - const eventTarget = new EventTarget() - const socket = new WebSocket(url) - if (!socket) throw new Error(`Failed to create socket ${id}`) - const proxy = (original, eventName, processor = (a) => a) => { - original.addEventListener(eventName, (eventData) => { - eventTarget.dispatchEvent( - new CustomEvent(eventName, { detail: processor(eventData) }), - ) - }) - } - socket.addEventListener('open', (wsEvent) => { - console.debug(`[WS][${id}] Socket connected`, wsEvent) - if (credentials) { - socket.send( - JSON.stringify({ - type: 'pleroma:authenticate', - token: credentials, - }), - ) - } - }) - socket.addEventListener('error', (wsEvent) => { - console.debug(`[WS][${id}] Socket errored`, wsEvent) - }) - socket.addEventListener('close', (wsEvent) => { - console.debug( - `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, - wsEvent, - ) - }) - // Commented code reason: very spammy, uncomment to enable message debug logging - /* - socket.addEventListener('message', (wsEvent) => { - console.debug( - `[WS][${id}] Message received`, - wsEvent - ) - }) - /**/ - - const onAuthenticated = () => { - eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated')) - } - - proxy(socket, 'open') - proxy(socket, 'close') - proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated })) - proxy(socket, 'error') - - // 1000 = Normal Closure - eventTarget.close = () => { - socket.close(1000, 'Shutting down socket') - } - eventTarget.getState = () => socket.readyState - eventTarget.subscribe = (stream, args = {}) => { - console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args) - socket.send( - JSON.stringify({ - type: 'subscribe', - stream, - ...args, - }), - ) - } - eventTarget.unsubscribe = (stream, args = {}) => { - console.debug( - `[WS][${id}] Unsubscribing from stream ${stream} with args`, - args, - ) - socket.send( - JSON.stringify({ - type: 'unsubscribe', - stream, - ...args, - }), - ) - } - - return eventTarget -} - -export const handleMastoWS = ( - wsEvent, - { - onAuthenticated = () => { - /* no-op */ - }, - } = {}, -) => { - const { data } = wsEvent - if (!data) return - const parsedEvent = JSON.parse(data) - const { event, payload } = parsedEvent - if ( - MASTODON_STREAMING_EVENTS.has(event) || - PLEROMA_STREAMING_EVENTS.has(event) - ) { - // MastoBE and PleromaBE both send payload for delete as a PLAIN string - if (event === 'delete') { - return { event, id: payload } - } - const data = payload ? JSON.parse(payload) : null - if (event === 'pleroma:respond') { - if (data.type === 'pleroma:authenticate') { - if (data.result === 'success') { - console.debug('[WS] Successfully authenticated') - onAuthenticated() - } else { - console.error('[WS] Unable to authenticate:', data.error) - wsEvent.target.close() - } - } - return null - } else if (event === 'update') { - return { event, status: parseStatus(data) } - } else if (event === 'status.update') { - return { event, status: parseStatus(data) } - } else if (event === 'notification') { - return { event, notification: parseNotification(data) } - } else if (event === 'pleroma:chat_update') { - return { event, chatUpdate: parseChat(data) } - } - } else { - console.warn('Unknown event', wsEvent) - return null - } -} - -export const WSConnectionStatus = Object.freeze({ - JOINED: 1, - CLOSED: 2, - ERROR: 3, - DISABLED: 4, - STARTING: 5, - STARTING_INITIAL: 6, -}) - -const chats = ({ credentials }) => { - return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) }) - .then((data) => data.json()) - .then((data) => { - return { chats: data.map(parseChat).filter((c) => c) } - }) -} - -const getOrCreateChat = ({ accountId, credentials }) => { - return promisedRequest({ - url: PLEROMA_CHAT_URL(accountId), - method: 'POST', - credentials, - }) -} - -const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => { - let url = PLEROMA_CHAT_MESSAGES_URL(id) - const args = [ - maxId && `max_id=${maxId}`, - sinceId && `since_id=${sinceId}`, - limit && `limit=${limit}`, - ] - .filter((_) => _) - .join('&') - - url = url + (args ? '?' + args : '') - - return promisedRequest({ - url, - method: 'GET', - credentials, - }) -} - -const sendChatMessage = ({ - id, - content, - mediaId = null, - idempotencyKey, - credentials, -}) => { - const payload = { - content, - } - - if (mediaId) { - payload.media_id = mediaId - } - - const headers = {} - - if (idempotencyKey) { - headers['idempotency-key'] = idempotencyKey - } - - return promisedRequest({ - url: PLEROMA_CHAT_MESSAGES_URL(id), - method: 'POST', - payload, - credentials, - headers, - }) -} - -const readChat = ({ id, lastReadId, credentials }) => { - return promisedRequest({ - url: PLEROMA_CHAT_READ_URL(id), - method: 'POST', - payload: { - last_read_id: lastReadId, - }, - credentials, - }) -} - -const deleteChatMessage = ({ chatId, messageId, credentials }) => { - return promisedRequest({ - url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId), - method: 'DELETE', - credentials, - }) -} - -const setReportState = ({ id, state, credentials }) => { - // TODO: Can't use promisedRequest because on OK this does not return json - // See https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1322 - return fetch(PLEROMA_ADMIN_REPORTS, { - headers: { - ...authHeaders(credentials), - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - method: 'PATCH', - body: JSON.stringify({ - reports: [ - { - id, - state, - }, - ], - }), - }) - .then((data) => { - if (data.status >= 500) { - throw Error(data.statusText) - } else if (data.status >= 400) { - return data.json() - } - return data - }) - .then((data) => { - if (data.errors) { - throw Error(data.errors[0].message) - } - }) -} - -// ADMIN STUFF // EXPERIMENTAL -const fetchInstanceDBConfig = ({ credentials }) => { - return fetch(PLEROMA_ADMIN_CONFIG_URL, { - headers: authHeaders(credentials), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const fetchInstanceConfigDescriptions = ({ credentials }) => { - return fetch(PLEROMA_ADMIN_DESCRIPTIONS_URL, { - headers: authHeaders(credentials), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const fetchAvailableFrontends = ({ credentials }) => { - return fetch(PLEROMA_ADMIN_FRONTENDS_URL, { - headers: authHeaders(credentials), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const pushInstanceDBConfig = ({ credentials, payload }) => { - return fetch(PLEROMA_ADMIN_CONFIG_URL, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...authHeaders(credentials), - }, - method: 'POST', - body: JSON.stringify(payload), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const installFrontend = ({ credentials, payload }) => { - return fetch(PLEROMA_ADMIN_FRONTENDS_INSTALL_URL, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...authHeaders(credentials), - }, - method: 'POST', - body: JSON.stringify(payload), - }).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const fetchScrobbles = ({ accountId, limit = 1 }) => { - let url = PLEROMA_SCROBBLES_URL(accountId) - const params = [['limit', limit]] - const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join( - '&', - ) - url += `?${queryString}` - return fetch(url, {}).then((response) => { - if (response.ok) { - return response.json() - } else { - return { - error: response, - } - } - }) -} - -const deleteEmojiPack = ({ name }) => { - return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'DELETE' }) -} - -const reloadEmoji = () => { - return fetch(PLEROMA_EMOJI_RELOAD_URL, { method: 'POST' }) -} - -const importEmojiFromFS = () => { - return fetch(PLEROMA_EMOJI_IMPORT_FS_URL) -} - -const createEmojiPack = ({ name }) => { - return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'POST' }) -} - -const listEmojiPacks = ({ page, pageSize }) => { - return fetch(PLEROMA_EMOJI_PACKS_URL(page, pageSize)) -} - -const listRemoteEmojiPacks = ({ instance, page, pageSize }) => { - if (!instance.startsWith('http')) { - instance = 'https://' + instance - } - - return fetch(PLEROMA_EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize), { - headers: { 'Content-Type': 'application/json' }, - }) -} - -const downloadRemoteEmojiPack = ({ instance, packName, as }) => { - return fetch(PLEROMA_EMOJI_PACKS_DL_REMOTE_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - url: instance, - name: packName, - as, - }), - }) -} - -const downloadRemoteEmojiPackZIP = ({ url, packName, file }) => { - const data = new FormData() - if (file) data.set('file', file) - if (url) data.set('url', url) - data.set('name', packName) - - return fetch(PLEROMA_EMOJI_PACKS_DL_REMOTE_ZIP_URL, { - method: 'POST', - body: data, - }) -} - -const saveEmojiPackMetadata = ({ name, newData }) => { - return fetch(PLEROMA_EMOJI_PACK_URL(name), { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ metadata: newData }), - }) -} - -const addNewEmojiFile = ({ packName, file, shortcode, filename }) => { - const data = new FormData() - if (filename.trim() !== '') { - data.set('filename', filename) - } - if (shortcode.trim() !== '') { - data.set('shortcode', shortcode) - } - data.set('file', file) - - return fetch(PLEROMA_EMOJI_UPDATE_FILE_URL(packName), { - method: 'POST', - body: data, - }) -} - -const updateEmojiFile = ({ - packName, - shortcode, - newShortcode, - newFilename, - force, -}) => { - return fetch(PLEROMA_EMOJI_UPDATE_FILE_URL(packName), { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - shortcode, - new_shortcode: newShortcode, - new_filename: newFilename, - force, - }), - }) -} - -const deleteEmojiFile = ({ packName, shortcode }) => { - return fetch( - `${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, - { method: 'DELETE' }, - ) -} - -const fetchBookmarkFolders = ({ credentials }) => { - const url = PLEROMA_BOOKMARK_FOLDERS_URL - return fetch(url, { headers: authHeaders(credentials) }).then((data) => - data.json(), - ) -} - -const createBookmarkFolder = ({ name, emoji, credentials }) => { - const url = PLEROMA_BOOKMARK_FOLDERS_URL - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'POST', - body: JSON.stringify({ name, emoji }), - }).then((data) => data.json()) -} - -const updateBookmarkFolder = ({ folderId, name, emoji, credentials }) => { - const url = PLEROMA_BOOKMARK_FOLDER_URL(folderId) - const headers = authHeaders(credentials) - headers['Content-Type'] = 'application/json' - - return fetch(url, { - headers, - method: 'PATCH', - body: JSON.stringify({ name, emoji }), - }).then((data) => data.json()) -} - -const deleteBookmarkFolder = ({ folderId, credentials }) => { - const url = PLEROMA_BOOKMARK_FOLDER_URL(folderId) - return fetch(url, { - method: 'DELETE', - headers: authHeaders(credentials), - }) -} - -const apiService = { - verifyCredentials, - fetchTimeline, - fetchPinnedStatuses, - fetchConversation, - fetchStatus, - fetchStatusSource, - fetchStatusHistory, - fetchFriends, - exportFriends, - fetchFollowers, - followUser, - unfollowUser, - pinOwnStatus, - unpinOwnStatus, - muteConversation, - unmuteConversation, - blockUser, - unblockUser, - removeUserFromFollowers, - editUserNote, - fetchUser, - fetchUserByName, - fetchUserRelationship, - favorite, - unfavorite, - retweet, - unretweet, - bookmarkStatus, - unbookmarkStatus, - postStatus, - editStatus, - deleteStatus, - uploadMedia, - setMediaDescription, - fetchMutes, - muteUser, - unmuteUser, - fetchBlocks, - fetchOAuthTokens, - revokeOAuthToken, - tagUser, - untagUser, - deleteUser, - addRight, - deleteRight, - activateUser, - deactivateUser, - register, - getCaptcha, - updateProfileImages, - updateProfile, - updateProfileJSON, - importMutes, - importBlocks, - importFollows, - deleteAccount, - changeEmail, - moveAccount, - addAlias, - deleteAlias, - listAliases, - changePassword, - settingsMFA, - mfaDisableOTP, - generateMfaBackupCodes, - mfaSetupOTP, - mfaConfirmOTP, - addBackup, - listBackups, - fetchFollowRequests, - fetchLists, - createList, - getList, - updateList, - getListAccounts, - addAccountsToList, - removeAccountsFromList, - deleteList, - approveUser, - denyUser, - suggestions, - markNotificationsAsSeen, - dismissNotification, - vote, - fetchPoll, - fetchFavoritedByUsers, - fetchRebloggedByUsers, - fetchEmojiReactions, - reactWithEmoji, - unreactWithEmoji, - reportUser, - updateNotificationSettings, - search2, - searchUsers, - fetchKnownDomains, - fetchDomainMutes, - muteDomain, - unmuteDomain, - chats, - getOrCreateChat, - chatMessages, - sendChatMessage, - readChat, - deleteChatMessage, - setReportState, - fetchUserInLists, - fetchAnnouncements, - dismissAnnouncement, - postAnnouncement, - editAnnouncement, - deleteAnnouncement, - fetchScrobbles, - adminFetchAnnouncements, - fetchInstanceDBConfig, - fetchInstanceConfigDescriptions, - fetchAvailableFrontends, - pushInstanceDBConfig, - installFrontend, - importEmojiFromFS, - reloadEmoji, - listEmojiPacks, - createEmojiPack, - deleteEmojiPack, - saveEmojiPackMetadata, - addNewEmojiFile, - updateEmojiFile, - deleteEmojiFile, - listRemoteEmojiPacks, - downloadRemoteEmojiPack, - downloadRemoteEmojiPackZIP, - fetchBookmarkFolders, - createBookmarkFolder, - updateBookmarkFolder, - deleteBookmarkFolder, -} - -export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js deleted file mode 100644 index adc18ef7f..000000000 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ /dev/null @@ -1,75 +0,0 @@ -import bookmarkFoldersFetcher from '../../services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js' -import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' -import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' -import apiService, { - getMastodonSocketURI, - ProcessedWS, -} from '../api/api.service.js' -import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' -import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js' - -import { useInstanceStore } from 'src/stores/instance.js' - -const backendInteractorService = (credentials) => ({ - startFetchingTimeline({ - timeline, - store, - userId = false, - listId = false, - statusId = false, - bookmarkFolderId = false, - tag, - }) { - return timelineFetcher.startFetching({ - timeline, - store, - credentials, - userId, - listId, - statusId, - bookmarkFolderId, - tag, - }) - }, - - fetchTimeline(args) { - return timelineFetcher.fetchAndUpdate({ ...args, credentials }) - }, - - startFetchingNotifications({ store }) { - return notificationsFetcher.startFetching({ store, credentials }) - }, - - fetchNotifications(args) { - return notificationsFetcher.fetchAndUpdate({ ...args, credentials }) - }, - - startFetchingFollowRequests({ store }) { - return followRequestFetcher.startFetching({ store, credentials }) - }, - - startFetchingLists({ store }) { - return listsFetcher.startFetching({ store, credentials }) - }, - - startFetchingBookmarkFolders({ store }) { - return bookmarkFoldersFetcher.startFetching({ store, credentials }) - }, - - startUserSocket({ store }) { - const serv = useInstanceStore().server.replace('http', 'ws') - const url = getMastodonSocketURI({}, serv) - return ProcessedWS({ url, id: 'Unified', credentials }) - }, - - ...Object.entries(apiService).reduce((acc, [key, func]) => { - return { - ...acc, - [key]: (args) => func({ credentials, ...args }), - } - }, {}), - - verifyCredentials: apiService.verifyCredentials, -}) - -export default backendInteractorService diff --git a/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js b/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js deleted file mode 100644 index 7b81c19dc..000000000 --- a/src/services/bookmark_folders_fetcher/bookmark_folders_fetcher.service.js +++ /dev/null @@ -1,32 +0,0 @@ -import apiService from '../api/api.service.js' -import { promiseInterval } from '../promise_interval/promise_interval.js' - -import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' - -const fetchAndUpdate = ({ credentials }) => { - return apiService - .fetchBookmarkFolders({ credentials }) - .then( - (bookmarkFolders) => { - useBookmarkFoldersStore().setBookmarkFolders(bookmarkFolders) - }, - (rej) => { - console.error(rej) - }, - ) - .catch((e) => { - console.error(e) - }) -} - -const startFetching = ({ credentials, store }) => { - const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) - boundFetchAndUpdate() - return promiseInterval(boundFetchAndUpdate, 240000) -} - -const bookmarkFoldersFetcher = { - startFetching, -} - -export default bookmarkFoldersFetcher diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index f6d381d6e..eec267dde 100644 --- a/src/services/chat_service/chat_service.js +++ b/src/services/chat_service/chat_service.js @@ -1,4 +1,4 @@ -import _ from 'lodash' +import { maxBy, minBy, orderBy, sortBy, uniqueId } from 'lodash' const empty = (chatId) => { return { @@ -42,12 +42,12 @@ const deleteMessage = (storage, messageId) => { delete storage.idIndex[messageId] if (storage.maxId === messageId) { - const lastMessage = _.maxBy(storage.messages, 'id') + const lastMessage = maxBy(storage.messages, 'id') storage.maxId = lastMessage.id } if (storage.minId === messageId) { - const firstMessage = _.minBy(storage.messages, 'id') + const firstMessage = minBy(storage.messages, 'id') storage.minId = firstMessage.id } } @@ -57,7 +57,7 @@ const cullOlderMessages = (storage) => { const minIndex = maxIndex - 50 if (maxIndex <= 50) return - storage.messages = _.sortBy(storage.messages, ['id']) + storage.messages = sortBy(storage.messages, ['id']) storage.minId = storage.messages[minIndex].id for (const message of storage.messages) { if (message.id < storage.minId) { @@ -78,7 +78,7 @@ const handleMessageError = (storage, fakeId, isRetry) => { fakeMessage.pending = false if (!isRetry) { // Ensure the failed message doesn't stay at the bottom of the list. - const lastPersistedMessage = _.orderBy( + const lastPersistedMessage = orderBy( storage.messages, ['pending', 'id'], ['asc', 'desc'], @@ -166,11 +166,7 @@ const getView = (storage) => { } const result = [] - const messages = _.orderBy( - storage.messages, - ['pending', 'id'], - ['asc', 'asc'], - ) + const messages = orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc']) const firstMessage = messages[0] let previousMessage = messages[messages.length - 1] let currentMessageChainId @@ -228,7 +224,7 @@ const getView = (storage) => { previousMessage.data.account_id) !== message.account_id || afterDate ) { - currentMessageChainId = _.uniqueId() + currentMessageChainId = uniqueId() object.isHead = true object.messageChainId = currentMessageChainId } diff --git a/src/services/component_utils/component_utils.js b/src/services/component_utils/component_utils.js index 49a110860..1973a7923 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/isFunction' +import { isFunction } from 'lodash' 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 0ea25014b..94a5681be 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -1,8 +1,9 @@ import { parseLinkHeader } from '@web3-storage/parse-link-header' import escapeHtml from 'escape-html' +import { unescape as lodashUnescape } from 'lodash' import punycode from 'punycode.js' -import fileTypeService from '../file_type/file_type.service.js' +import { fileType } from '../file_type/file_type.service.js' import { isStatusNotification } from '../notification_utils/notification_utils.js' /** NOTICE! ** @@ -14,234 +15,154 @@ import { isStatusNotification } from '../notification_utils/notification_utils.j * it would be reverted back to [] */ -const qvitterStatusType = (status) => { - if (status.is_post_verb) { - return 'status' - } - - if (status.retweeted_status) { - return 'retweet' - } - - if ( - (typeof status.uri === 'string' && - status.uri.match(/(fave|objectType=Favourite)/)) || - (typeof status.text === 'string' && status.text.match(/favorited/)) - ) { - return 'favorite' - } - - if ( - status.text.match(/deleted notice {{tag/) || - status.qvitter_delete_notice - ) { - return 'deletion' - } - - if ( - status.text.match(/started following/) || - status.activity_type === 'follow' - ) { - return 'follow' - } - - return 'unknown' -} - export const parseUser = (data) => { const output = {} - const masto = Object.hasOwn(data, 'acct') + output._original = data // used for server-side settings + // case for users in "mentions" property for statuses in MastoAPI - const mastoShort = masto && !Object.hasOwn(data, 'avatar') + const mastoShort = !Object.hasOwn(data, 'avatar') output.inLists = null output.id = String(data.id) - output._original = data // used for server-side settings - if (masto) { - output.screen_name = data.acct - output.fqn = data.fqn - output.statusnet_profile_url = data.url + output.screen_name = data.acct + output.fqn = data.fqn + output.statusnet_profile_url = data.url - if (Object.hasOwn(data, 'mute_expires_at')) { - output.mute_expires_at = - data.mute_expires_at == null ? false : data.mute_expires_at + if (Object.hasOwn(data, 'mute_expires_at')) { + output.mute_expires_at = + data.mute_expires_at == null ? false : data.mute_expires_at + } + + if (Object.hasOwn(data, 'block_expires_at')) { + output.block_expires_at = + data.block_expires_at == null ? false : data.block_expires_at + } + + // There's nothing else to get + if (mastoShort) { + return output + } + + output.emoji = data.emojis + output.name = escapeHtml(data.display_name) + output.name_html = output.name + output.name_unescaped = data.display_name + + output.description = data.note + // TODO cleanup this shit, output.description is overriden with source data + output.description_html = data.note + + output.fields = data.fields + output.fields_html = data.fields.map((field) => { + return { + name: escapeHtml(field.name), + value: field.value, + } + }) + output.fields_text = data.fields.map((field) => { + return { + name: unescape(field.name.replace(/<[^>]*>/g, '')), + value: unescape(field.value.replace(/<[^>]*>/g, '')), + } + }) + + // Utilize avatar_static for gif avatars? + output.profile_image_url = data.avatar + output.profile_image_url_original = data.avatar + + // Same, utilize header_static? + output.cover_photo = data.header + + output.friends_count = data.following_count + + output.bot = data.bot + + output.privileges = [] + + if (data.pleroma) { + if (data.pleroma.settings_store) { + output.storage = data.pleroma.settings_store['pleroma-fe'] + output.user_highlight = data.pleroma.settings_store['user_highlight'] + } + const relationship = data.pleroma.relationship + + output.background_image = data.pleroma.background_image + output.favicon = data.pleroma.favicon + output.token = data.pleroma.chat_token + + if (relationship) { + output.relationship = relationship } - if (Object.hasOwn(data, 'block_expires_at')) { - output.block_expires_at = - data.block_expires_at == null ? false : data.block_expires_at + output.allow_following_move = data.pleroma.allow_following_move + + output.hide_favorites = data.pleroma.hide_favorites + output.hide_follows = data.pleroma.hide_follows + output.hide_followers = data.pleroma.hide_followers + output.hide_follows_count = data.pleroma.hide_follows_count + output.hide_followers_count = data.pleroma.hide_followers_count + + output.rights = { + moderator: data.pleroma.is_moderator, + admin: data.pleroma.is_admin, + } + // TODO: Clean up in UI? This is duplication from what BE does for qvitterapi + if (output.rights.admin) { + output.role = 'admin' + } else if (output.rights.moderator) { + output.role = 'moderator' + } else { + output.role = 'member' } - // There's nothing else to get - if (mastoShort) { - return output - } + output.birthday = data.pleroma.birthday - output.emoji = data.emojis - output.name = escapeHtml(data.display_name) - output.name_html = output.name - output.name_unescaped = data.display_name - - output.description = data.note - // TODO cleanup this shit, output.description is overriden with source data - output.description_html = data.note - - output.fields = data.fields - output.fields_html = data.fields.map((field) => { - return { - name: escapeHtml(field.name), - value: field.value, - } - }) - output.fields_text = data.fields.map((field) => { - return { - name: unescape(field.name.replace(/<[^>]*>/g, '')), - value: unescape(field.value.replace(/<[^>]*>/g, '')), - } - }) - - // Utilize avatar_static for gif avatars? - output.profile_image_url = data.avatar - output.profile_image_url_original = data.avatar - - // Same, utilize header_static? - output.cover_photo = data.header - - output.friends_count = data.following_count - - output.bot = data.bot - - output.privileges = [] - - if (data.pleroma) { - if (data.pleroma.settings_store) { - output.storage = data.pleroma.settings_store['pleroma-fe'] - output.user_highlight = data.pleroma.settings_store['user_highlight'] - } - const relationship = data.pleroma.relationship - - output.background_image = data.pleroma.background_image - output.favicon = data.pleroma.favicon - output.token = data.pleroma.chat_token - - if (relationship) { - output.relationship = relationship - } - - output.allow_following_move = data.pleroma.allow_following_move - - output.hide_favorites = data.pleroma.hide_favorites - output.hide_follows = data.pleroma.hide_follows - output.hide_followers = data.pleroma.hide_followers - output.hide_follows_count = data.pleroma.hide_follows_count - output.hide_followers_count = data.pleroma.hide_followers_count - - output.rights = { - moderator: data.pleroma.is_moderator, - admin: data.pleroma.is_admin, - } - // TODO: Clean up in UI? This is duplication from what BE does for qvitterapi - if (output.rights.admin) { - output.role = 'admin' - } else if (output.rights.moderator) { - output.role = 'moderator' - } else { - output.role = 'member' - } - - output.birthday = data.pleroma.birthday - - if (data.pleroma.privileges) { - output.privileges = data.pleroma.privileges - } else if (data.pleroma.is_admin) { - output.privileges = [ - 'users_read', - 'users_manage_invites', - 'users_manage_activation_state', - 'users_manage_tags', - 'users_manage_credentials', - 'users_delete', - 'messages_read', - 'messages_delete', - 'instances_delete', - 'reports_manage_reports', - 'moderation_log_read', - 'announcements_manage_announcements', - 'emoji_manage_emoji', - 'statistics_read', - ] - } else if (data.pleroma.is_moderator) { - output.privileges = ['messages_delete', 'reports_manage_reports'] - } else { - output.privileges = [] - } - } - - if (data.source) { - output.description = data.source.note - output.default_scope = data.source.privacy - output.fields = data.source.fields - if (data.source.pleroma) { - output.no_rich_text = data.source.pleroma.no_rich_text - output.show_role = data.source.pleroma.show_role - output.discoverable = data.source.pleroma.discoverable - output.show_birthday = data.pleroma.show_birthday - output.actor_type = data.source.pleroma.actor_type - } - } - - // TODO: handle is_local - output.is_local = !output.screen_name.includes('@') - } else { - output.screen_name = data.screen_name - - output.name = data.name - output.name_html = data.name_html - - output.description = data.description - output.description_html = data.description_html - - output.profile_image_url = data.profile_image_url - output.profile_image_url_original = data.profile_image_url_original - - output.cover_photo = data.cover_photo - - output.friends_count = data.friends_count - - // output.bot = ??? missing - - output.statusnet_profile_url = data.statusnet_profile_url - - output.is_local = data.is_local - output.role = data.role - output.show_role = data.show_role - - if (data.rights) { - output.rights = { - moderator: data.rights.delete_others_notice, - admin: data.rights.admin, - } - } - output.no_rich_text = data.no_rich_text - output.default_scope = data.default_scope - output.hide_follows = data.hide_follows - output.hide_followers = data.hide_followers - output.hide_follows_count = data.hide_follows_count - output.hide_followers_count = data.hide_followers_count - output.background_image = data.background_image - // Websocket token - output.token = data.token - - // Convert relationsip data to expected format - output.relationship = { - muting: data.muted, - blocking: data.statusnet_blocking, - followed_by: data.follows_you, - following: data.following, + if (data.pleroma.privileges) { + output.privileges = new Set(data.pleroma.privileges) + } else if (data.pleroma.is_admin) { + output.privileges = new Set([ + 'users_read', + 'users_manage_invites', + 'users_manage_activation_state', + 'users_manage_tags', + 'users_manage_credentials', + 'users_delete', + 'messages_read', + 'messages_delete', + 'instances_delete', + 'reports_manage_reports', + 'moderation_log_read', + 'announcements_manage_announcements', + 'emoji_manage_emoji', + 'statistics_read', + ]) + } else if (data.pleroma.is_moderator) { + output.privileges = new Set(['messages_delete', 'reports_manage_reports']) + } else { + output.privileges = new Set() } } + if (data.source) { + output.description = data.source.note + output.default_scope = data.source.privacy + output.fields = data.source.fields + if (data.source.pleroma) { + output.no_rich_text = data.source.pleroma.no_rich_text + output.show_role = + typeof data.source.pleroma.show_role === 'boolean' + ? data.source.pleroma.show_role + : true + output.discoverable = data.source.pleroma.discoverable + output.show_birthday = data.pleroma.show_birthday + output.actor_type = data.source.pleroma.actor_type + } + } + + // TODO: handle is_local + output.is_local = !output.screen_name.includes('@') + output.created_at = new Date(data.created_at) output.locked = data.locked output.last_status_at = new Date(data.last_status_at) @@ -251,7 +172,7 @@ export const parseUser = (data) => { if (data.pleroma) { output.follow_request_count = data.pleroma.follow_request_count - output.tags = data.pleroma.tags + output.tags = new Set(data.pleroma.tags) // deactivated was changed to is_active in Pleroma 2.3.0 // so check if is_active is present @@ -264,7 +185,7 @@ export const parseUser = (data) => { output.unread_chat_count = data.pleroma.unread_chat_count } - output.tags = output.tags || [] + output.tags = output.tags || new Set() output.rights = output.rights || {} output.notification_settings = output.notification_settings || {} @@ -288,26 +209,21 @@ export const parseUser = (data) => { export const parseAttachment = (data) => { const output = {} - const masto = !Object.hasOwn(data, 'oembed') - if (masto) { - // Not exactly same... - output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type - output.meta = data.meta // not present in BE yet - output.id = data.id - } else { - output.mimetype = data.mimetype - // output.meta = ??? missing - } + // Not exactly same... + output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type + output.meta = data.meta // not present in BE yet + output.id = data.id if (data.type !== 'unknown') { - output.type = data.type + // treat gifv like it is "video" + output.type = data.type === 'gifv' ? 'video' : data.type } else { - output.type = fileTypeService.fileType(output.mimetype) + output.type = fileType(output.mimetype) } output.url = data.url output.large_thumb_url = data.preview_url - output.description = data.description + output.description = lodashUnescape(data.description) return output } @@ -324,116 +240,76 @@ export const parseSource = (data) => { export const parseStatus = (data) => { const output = {} - const masto = Object.hasOwn(data, 'account') - if (masto) { - output.favorited = data.favourited - output.fave_num = data.favourites_count + output.favorited = data.favourited + output.fave_num = data.favourites_count - output.repeated = data.reblogged - output.repeat_num = data.reblogs_count + output.repeated = data.reblogged + output.repeat_num = data.reblogs_count - output.bookmarked = data.bookmarked + output.bookmarked = data.bookmarked - output.type = data.reblog ? 'retweet' : 'status' - output.nsfw = data.sensitive + output.type = data.reblog ? 'retweet' : 'status' + output.nsfw = data.sensitive - output.raw_html = data.content - output.emojis = data.emojis + output.raw_html = data.content + output.emojis = data.emojis - output.tags = data.tags + output.tags = data.tags - output.edited_at = data.edited_at + output.edited_at = data.edited_at - const { pleroma } = data + const { pleroma } = data - if (data.pleroma) { - output.text = pleroma.content - ? data.pleroma.content['text/plain'] - : data.content - output.summary = pleroma.spoiler_text - ? data.pleroma.spoiler_text['text/plain'] - : data.spoiler_text - output.statusnet_conversation_id = data.pleroma.conversation_id - output.is_local = pleroma.local - output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct - output.thread_muted = pleroma.thread_muted - output.emoji_reactions = pleroma.emoji_reactions - output.parent_visible = - pleroma.parent_visible === undefined ? true : pleroma.parent_visible - output.quote_visible = pleroma.quote_visible || true - output.quotes_count = pleroma.quotes_count - output.bookmark_folder_id = pleroma.bookmark_folder - } else { - output.text = data.content - output.summary = data.spoiler_text - } - - const quoteRaw = pleroma?.quote || data.quote - const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined - output.quote = quoteData - output.quote_id = - data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma?.quote_id - output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma?.quote_url - - output.in_reply_to_status_id = data.in_reply_to_id - output.in_reply_to_user_id = data.in_reply_to_account_id - output.replies_count = data.replies_count - - if (output.type === 'retweet') { - output.retweeted_status = parseStatus(data.reblog) - } - - output.summary_raw_html = escapeHtml(data.spoiler_text) - output.external_url = data.uri || data.url - output.poll = data.poll - if (output.poll) { - output.poll.options = (output.poll.options || []).map((field) => ({ - ...field, - title_html: escapeHtml(field.title), - })) - } - output.pinned = data.pinned - output.muted = data.muted + if (data.pleroma) { + output.text = pleroma.content + ? data.pleroma.content['text/plain'] + : data.content + output.summary = pleroma.spoiler_text + ? data.pleroma.spoiler_text['text/plain'] + : data.spoiler_text + output.statusnet_conversation_id = data.pleroma.conversation_id + output.is_local = pleroma.local + output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct + output.thread_muted = pleroma.thread_muted + output.emoji_reactions = pleroma.emoji_reactions + output.parent_visible = + pleroma.parent_visible === undefined ? true : pleroma.parent_visible + output.quote_visible = pleroma.quote_visible || true + output.quotes_count = pleroma.quotes_count + output.bookmark_folder_id = pleroma.bookmark_folder } else { - output.favorited = data.favorited - output.fave_num = data.fave_num - - output.repeated = data.repeated - output.repeat_num = data.repeat_num - - // catchall, temporary - // Object.assign(output, data) - - output.type = qvitterStatusType(data) - - if (data.nsfw === undefined) { - output.nsfw = isNsfw(data) - if (data.retweeted_status) { - output.nsfw = data.retweeted_status.nsfw - } - } else { - output.nsfw = data.nsfw - } - - output.raw_html = data.statusnet_html - output.text = data.text - - output.in_reply_to_status_id = data.in_reply_to_status_id - output.in_reply_to_user_id = data.in_reply_to_user_id - output.in_reply_to_screen_name = data.in_reply_to_screen_name - output.statusnet_conversation_id = data.statusnet_conversation_id - - if (output.type === 'retweet') { - output.retweeted_status = parseStatus(data.retweeted_status) - } - - output.summary = data.summary - output.summary_html = data.summary_html - output.external_url = data.external_url - output.is_local = data.is_local + output.text = data.content + output.summary = data.spoiler_text } + const quoteRaw = pleroma?.quote || data.quote + const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined + output.quote = quoteData + output.quote_id = + data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma?.quote_id + output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma?.quote_url + + output.in_reply_to_status_id = data.in_reply_to_id + output.in_reply_to_user_id = data.in_reply_to_account_id + output.replies_count = data.replies_count + + if (output.type === 'retweet') { + output.retweeted_status = parseStatus(data.reblog) + } + + output.summary_raw_html = escapeHtml(data.spoiler_text) + output.external_url = data.uri || data.url + output.poll = data.poll + if (output.poll) { + output.poll.options = (output.poll.options || []).map((field) => ({ + ...field, + title_html: escapeHtml(field.title), + })) + } + output.pinned = data.pinned + output.muted = data.muted + output.id = String(data.id) output.visibility = data.visibility output.card = data.card @@ -447,17 +323,13 @@ export const parseStatus = (data) => { ? String(output.in_reply_to_user_id) : null - output.user = parseUser(masto ? data.account : data.user) + output.user = parseUser(data.account) - output.attentions = ((masto ? data.mentions : data.attentions) || []).map( - parseUser, - ) + output.attentions = (data.mentions || []).map(parseUser) - output.attachments = ( - (masto ? data.media_attachments : data.attachments) || [] - ).map(parseAttachment) + output.attachments = (data.media_attachments || []).map(parseAttachment) - const retweetedStatus = masto ? data.reblog : data.retweeted_status + const retweetedStatus = data.reblog if (retweetedStatus) { output.retweeted_status = parseStatus(retweetedStatus) } @@ -477,42 +349,26 @@ export const parseNotification = (data) => { favourite: 'like', reblog: 'repeat', } - const masto = !Object.hasOwn(data, 'ntype') const output = {} - if (masto) { - output.type = mastoDict[data.type] || data.type - output.seen = data.pleroma.is_seen - // TODO: null check should be a temporary fix, I guess. - // Investigate why backend does this. - output.status = - isStatusNotification(output.type) && data.status !== null - ? parseStatus(data.status) - : null - output.target = output.type !== 'move' ? null : parseUser(data.target) - output.from_profile = parseUser(data.account) - output.emoji = data.emoji - output.emoji_url = data.emoji_url - if (data.report) { - output.report = data.report - output.report.content = data.report.content - output.report.acct = parseUser(data.report.account) - output.report.actor = parseUser(data.report.actor) - output.report.statuses = data.report.statuses.map(parseStatus) - } - } else { - const parsedNotice = parseStatus(data.notice) - output.type = data.ntype - output.seen = Boolean(data.is_seen) - output.status = - output.type === 'like' - ? parseStatus(data.notice.favorited_status) - : parsedNotice - output.action = parsedNotice - output.from_profile = - output.type === 'pleroma:chat_mention' - ? parseUser(data.account) - : parseUser(data.from_profile) + output.type = mastoDict[data.type] || data.type + output.seen = data.pleroma.is_seen + // TODO: null check should be a temporary fix, I guess. + // Investigate why backend does this. + output.status = + isStatusNotification(output.type) && data.status !== null + ? parseStatus(data.status) + : null + output.target = output.type !== 'move' ? null : parseUser(data.target) + output.from_profile = parseUser(data.account) + output.emoji = data.emoji + output.emoji_url = data.emoji_url + if (data.report) { + output.report = data.report + output.report.content = data.report.content + output.report.acct = parseUser(data.report.account) + output.report.actor = parseUser(data.report.actor) + output.report.statuses = data.report.statuses.map(parseStatus) } output.created_at = new Date(data.created_at) @@ -521,14 +377,6 @@ export const parseNotification = (data) => { return output } -const isNsfw = (status) => { - const nsfwRegex = /#nsfw/i - return ( - (status.tags || []).includes('nsfw') || - !!(status.text || '').match(nsfwRegex) - ) -} - export const parseLinkHeaderPagination = (linkHeader, opts = {}) => { const flakeId = opts.flakeId const parsedLinkHeader = parseLinkHeader(linkHeader) diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js index 41829eb19..ccbae9b3e 100644 --- a/src/services/errors/errors.js +++ b/src/services/errors/errors.js @@ -13,8 +13,10 @@ function humanizeErrors(errors) { export function StatusCodeError(statusCode, body, options, response) { this.name = 'StatusCodeError' this.statusCode = statusCode - this.message = - statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body) + this.statusText = body.error || body.errors || body + this.details = JSON && JSON.stringify ? JSON.stringify(body) : body + this.errorData = body.error || body.errors + this.message = this.statusCode + ' - ' + this.statusText this.error = body // legacy attribute this.options = options this.response = response diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index 209a5f0c0..759ba67d9 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -1,9 +1,19 @@ +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + fetchUserRelationship, + followUser, + unfollowUser, +} from 'src/api/user.js' + const fetchRelationship = (attempt, userId, store) => new Promise((resolve, reject) => { setTimeout(() => { - store.state.api.backendInteractor - .fetchUserRelationship({ id: userId }) - .then((relationship) => { + fetchUserRelationship({ + id: userId, + credentials: useOAuthStore().token, + }) + .then(({ data: relationship }) => { store.commit('updateUserRelationship', [relationship]) return relationship }) @@ -25,40 +35,33 @@ const fetchRelationship = (attempt, userId, store) => } }) -export const requestFollow = (userId, store) => - new Promise((resolve) => { - store.state.api.backendInteractor - .followUser({ id: userId }) - .then((updated) => { - store.commit('updateUserRelationship', [updated]) - - if (updated.following || (updated.locked && updated.requested)) { - // If we get result immediately or the account is locked, just stop. - resolve() - return - } - - // But usually we don't get result immediately, so we ask server - // for updated user profile to confirm if we are following them - // Sometimes it takes several tries. Sometimes we end up not following - // user anyway, probably because they locked themselves and we - // don't know that yet. - // Recursive Promise, it will call itself up to 3 times. - - return fetchRelationship(1, updated, store).then(() => { - resolve() - }) - }) +export const requestFollow = async (userId, store) => { + const { data: updated } = await followUser({ + id: userId, + credentials: useOAuthStore().token, }) -export const requestUnfollow = (userId, store) => - new Promise((resolve) => { - store.state.api.backendInteractor - .unfollowUser({ id: userId }) - .then((updated) => { - store.commit('updateUserRelationship', [updated]) - resolve({ - updated, - }) - }) + store.commit('updateUserRelationship', [updated]) + + if (updated.following || (updated.locked && updated.requested)) { + // If we get result immediately or the account is locked, just stop. + return + } + + // But usually we don't get result immediately, so we ask server + // for updated user profile to confirm if we are following them + // Sometimes it takes several tries. Sometimes we end up not following + // user anyway, probably because they locked themselves and we + // don't know that yet. + // Recursive Promise, it will call itself up to 3 times. + return await fetchRelationship(1, updated, store) +} + +export const requestUnfollow = async (userId, store) => { + const { data: updated } = await unfollowUser({ + id: userId, + credentials: useOAuthStore().token, }) + + return await store.commit('updateUserRelationship', [updated]) +} diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js index 530c98aa7..e49206fcd 100644 --- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js +++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js @@ -1,11 +1,10 @@ -import apiService from '../api/api.service.js' -import { promiseInterval } from '../promise_interval/promise_interval.js' +import { fetchFollowRequests } from 'src/api/user.js' +import { promiseInterval } from 'src/services/promise_interval/promise_interval.js' const fetchAndUpdate = ({ store, credentials }) => { - return apiService - .fetchFollowRequests({ credentials }) + return fetchFollowRequests({ credentials }) .then( - (requests) => { + ({ data: requests }) => { store.commit('setFollowRequests', requests) store.commit('addNewUsers', requests) }, diff --git a/src/services/lists_fetcher/lists_fetcher.service.js b/src/services/lists_fetcher/lists_fetcher.service.js deleted file mode 100644 index c395ef93b..000000000 --- a/src/services/lists_fetcher/lists_fetcher.service.js +++ /dev/null @@ -1,32 +0,0 @@ -import apiService from '../api/api.service.js' -import { promiseInterval } from '../promise_interval/promise_interval.js' - -import { useListsStore } from 'src/stores/lists.js' - -const fetchAndUpdate = ({ credentials }) => { - return apiService - .fetchLists({ credentials }) - .then( - (lists) => { - useListsStore().setLists(lists) - }, - (rej) => { - console.error(rej) - }, - ) - .catch((e) => { - console.error(e) - }) -} - -const startFetching = ({ credentials, store }) => { - const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) - boundFetchAndUpdate() - return promiseInterval(boundFetchAndUpdate, 240000) -} - -const listsFetcher = { - startFetching, -} - -export default listsFetcher diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js index 9fe62318c..bdc07c1ec 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 _ from 'lodash' +import { map } from 'lodash' import languagesObject from '../../i18n/messages' @@ -35,7 +35,7 @@ const getLanguageName = (code) => { ) } -const languages = _.map(languagesObject.languages, (code) => ({ +const languages = map(languagesObject.languages, (code) => ({ code, name: getLanguageName(code), })).sort((a, b) => a.name.localeCompare(b.name)) diff --git a/src/services/new_api/mfa.js b/src/services/new_api/mfa.js deleted file mode 100644 index 1d35b65a8..000000000 --- a/src/services/new_api/mfa.js +++ /dev/null @@ -1,54 +0,0 @@ -const verifyOTPCode = ({ - clientId, - clientSecret, - instance, - mfaToken, - code, -}) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'totp') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const verifyRecoveryCode = ({ - clientId, - clientSecret, - instance, - mfaToken, - code, -}) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'recovery') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const mfa = { - verifyOTPCode, - verifyRecoveryCode, -} - -export default mfa diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js deleted file mode 100644 index b803e2146..000000000 --- a/src/services/new_api/oauth.js +++ /dev/null @@ -1,198 +0,0 @@ -import { reduce } from 'lodash' - -import { StatusCodeError } from 'src/services/errors/errors.js' - -const REDIRECT_URI = `${window.location.origin}/oauth-callback` - -export const getJsonOrError = async (response) => { - if (response.ok) { - return response.json().catch((error) => { - throw new StatusCodeError(response.status, error, {}, response) - }) - } else { - throw new StatusCodeError( - response.status, - await response.text(), - {}, - response, - ) - } -} - -export const createApp = (instance) => { - const url = `${instance}/api/v1/apps` - const form = new window.FormData() - - form.append('client_name', 'PleromaFE') - form.append('website', 'https://pleroma.social') - form.append('redirect_uris', REDIRECT_URI) - form.append('scopes', 'read write follow push admin') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then(getJsonOrError) - .then((app) => ({ - clientId: app.client_id, - clientSecret: app.client_secret, - })) -} - -export const verifyAppToken = ({ instance, appToken }) => { - return window - .fetch(`${instance}/api/v1/apps/verify_credentials`, { - method: 'GET', - headers: { Authorization: `Bearer ${appToken}` }, - }) - .then(getJsonOrError) -} - -const login = ({ instance, clientId }) => { - const data = { - response_type: 'code', - client_id: clientId, - redirect_uri: REDIRECT_URI, - scope: 'read write follow push admin', - } - - const dataString = reduce( - data, - (acc, v, k) => { - const encoded = `${k}=${encodeURIComponent(v)}` - if (!acc) { - return encoded - } else { - return `${acc}&${encoded}` - } - }, - false, - ) - - // Do the redirect... - const url = `${instance}/oauth/authorize?${dataString}` - - window.location.href = url -} - -const getTokenWithCredentials = ({ - clientId, - clientSecret, - instance, - username, - password, -}) => { - const url = `${instance}/oauth/token` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('grant_type', 'password') - form.append('username', username) - form.append('password', password) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const getToken = ({ clientId, clientSecret, instance, code }) => { - const url = `${instance}/oauth/token` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('grant_type', 'authorization_code') - form.append('code', code) - form.append('redirect_uri', `${window.location.origin}/oauth-callback`) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -export const getClientToken = ({ clientId, clientSecret, instance }) => { - const url = `${instance}/oauth/token` - const form = new window.FormData() - - form.append('client_id', clientId) - form.append('client_secret', clientSecret) - form.append('grant_type', 'client_credentials') - form.append('redirect_uri', `${window.location.origin}/oauth-callback`) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then(getJsonOrError) -} -const verifyOTPCode = ({ app, instance, mfaToken, code }) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'totp') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const verifyRecoveryCode = ({ app, instance, mfaToken, code }) => { - const url = `${instance}/oauth/mfa/challenge` - const form = new window.FormData() - - form.append('client_id', app.client_id) - form.append('client_secret', app.client_secret) - form.append('mfa_token', mfaToken) - form.append('code', code) - form.append('challenge_type', 'recovery') - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const revokeToken = ({ app, instance, token }) => { - const url = `${instance}/oauth/revoke` - const form = new window.FormData() - - form.append('client_id', app.clientId) - form.append('client_secret', app.clientSecret) - form.append('token', token) - - return window - .fetch(url, { - method: 'POST', - body: form, - }) - .then((data) => data.json()) -} - -const oauth = { - login, - getToken, - getTokenWithCredentials, - verifyOTPCode, - verifyRecoveryCode, - revokeToken, -} - -export default oauth diff --git a/src/services/new_api/password_reset.js b/src/services/new_api/password_reset.js deleted file mode 100644 index 65342c04b..000000000 --- a/src/services/new_api/password_reset.js +++ /dev/null @@ -1,22 +0,0 @@ -import { reduce } from 'lodash' - -const MASTODON_PASSWORD_RESET_URL = '/auth/password' - -const resetPassword = ({ instance, email }) => { - const params = { email } - const query = reduce( - params, - (acc, v, k) => { - const encoded = `${k}=${encodeURIComponent(v)}` - return `${acc}&${encoded}` - }, - '', - ) - const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}` - - return window.fetch(url, { - method: 'POST', - }) -} - -export default resetPassword diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 1fbaf2a2c..37e5e95ad 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -1,9 +1,6 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' import { muteFilterHits } from '../status_parser/status_parser.js' -import { useAnnouncementsStore } from 'src/stores/announcements.js' -import { useI18nStore } from 'src/stores/i18n.js' - import FaviconService from 'src/services/favicon_service/favicon_service.js' export const ACTIONABLE_NOTIFICATION_TYPES = new Set([ @@ -76,18 +73,19 @@ export const maybeShowNotification = ( notificationVisibility, muteFilters, notification, + i18n, ) => { const rootState = store.rootState || store.state if (notification.seen) return if (!visibleTypes(notificationVisibility).includes(notification.type)) return - if (notification.type === 'mention' && isMutedNotification(muteFilters, notification)) + if ( + notification.type === 'mention' && + isMutedNotification(muteFilters, notification) + ) return - const notificationObject = prepareNotificationObject( - notification, - useI18nStore().i18n, - ) + const notificationObject = prepareNotificationObject(notification, i18n) showDesktopNotification(rootState, notificationObject) } @@ -190,7 +188,11 @@ export const prepareNotificationObject = (notification, i18n) => { return notifObj } -export const countExtraNotifications = (store, mergedConfig) => { +export const countExtraNotifications = ( + store, + mergedConfig, + unreadAnnouncementCount, +) => { const rootGetters = store.rootGetters || store.getters if (!mergedConfig.showExtraNotifications) { @@ -202,7 +204,7 @@ export const countExtraNotifications = (store, mergedConfig) => { ? rootGetters.unreadChatCount : 0, mergedConfig.showAnnouncementsInExtraNotifications - ? useAnnouncementsStore().unreadAnnouncementCount + ? unreadAnnouncementCount : 0, mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index c1a9e1a2f..8530c468c 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -1,11 +1,11 @@ -import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { fetchTimeline } from 'src/api/timelines.js' + const update = ({ store, notifications, older }) => { store.dispatch('addNewNotifications', { notifications, older }) } @@ -25,7 +25,7 @@ const mastoApiNotificationTypes = new Set([ 'pleroma:report', ]) -const fetchAndUpdate = ({ store, credentials, older = false, since }) => { +const fetchAndUpdate = ({ store, credentials, older = false, sinceId }) => { const args = { credentials } const rootState = store.rootState || store.state const timelineData = rootState.notifications @@ -35,24 +35,24 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { mastoApiNotificationTypes.add('pleroma:chat_mention') } - args.includeTypes = mastoApiNotificationTypes + args.includeTypes = [...mastoApiNotificationTypes] args.withMuted = !hideMutedPosts args.timeline = 'notifications' if (older) { if (timelineData.minId !== Number.POSITIVE_INFINITY) { - args.until = timelineData.minId + args.maxId = timelineData.minId } return fetchNotifications({ store, args, older }) } else { // fetch new notifications if ( - since === undefined && + sinceId === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY ) { - args.since = timelineData.maxId - } else if (since !== null) { - args.since = since + args.sinceId = timelineData.maxId + } else if (sinceId !== null) { + args.sinceId = sinceId } const result = fetchNotifications({ store, args, older }) @@ -69,7 +69,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { if (readNotifsIds.length > 0 && readNotifsIds.length > 0) { const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification if (minId !== Infinity) { - args.since = false // Don't use since_id since it sorta conflicts with min_id + args.sinceId = null // Don't use since_id since it sorta conflicts with min_id args.minId = minId - 1 // go beyond fetchNotifications({ store, args, older }) } @@ -80,29 +80,25 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { } const fetchNotifications = ({ store, args, older }) => { - return apiService - .fetchTimeline(args) + return fetchTimeline(args) .then((response) => { - if (response.errors) { - if ( - response.status === 400 && - response.statusText.includes('Invalid value for enum') - ) { - response.statusText - .matchAll(/(\w+) - Invalid value for enum./g) - .toArray() - .map((x) => x[1]) - .forEach((x) => mastoApiNotificationTypes.delete(x)) - return fetchNotifications({ store, args, older }) - } else { - throw new Error(`${response.status} ${response.statusText}`) - } - } const notifications = response.data update({ store, notifications, older }) return notifications }) .catch((error) => { + if ( + error.statusCode === 400 && + error.statusText.includes('Invalid value for enum') + ) { + error.statusText + .matchAll(/(\w+) - Invalid value for enum./g) + .toArray() + .map((x) => x[1]) + .forEach((x) => mastoApiNotificationTypes.delete(x)) + return fetchNotifications({ store, args, older }) + } + useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'notifications.error', diff --git a/src/services/promise_interval/promise_interval.js b/src/services/promise_interval/promise_interval.js index 46ac68996..d9396c643 100644 --- a/src/services/promise_interval/promise_interval.js +++ b/src/services/promise_interval/promise_interval.js @@ -4,32 +4,34 @@ // time after the first interval. // - interval is the interval delay in ms. +const wait = (timeout) => { + let timeoutId + const promise = () => + new Promise((resolve) => { + timeoutId = window.setTimeout(() => resolve(), timeout) + }) + return { timeoutId, promise } +} + export const promiseInterval = (promiseCall, interval) => { let stopped = false let timeout = null - const func = () => { - const promise = promiseCall() - // something unexpected happened and promiseCall did not - // return a promise, abort the loop. - if (!(promise && promise.finally)) { - console.warn( - 'promiseInterval: promise call did not return a promise, stopping interval.', - ) - return - } - promise.finally(() => { - if (stopped) return - timeout = window.setTimeout(func, interval) - }) - } - const stopFetcher = () => { stopped = true window.clearTimeout(timeout) } - timeout = window.setTimeout(func, interval) + const loop = async () => { + while (!stopped) { + await promiseCall() + const { timeoutId, promise } = wait(interval) + timeout = timeoutId + await promise() + } + } + + loop().then() return { stop: stopFetcher } } diff --git a/src/services/status_parser/status_parser.js b/src/services/status_parser/status_parser.js index c6ceb1f0a..a011fe265 100644 --- a/src/services/status_parser/status_parser.js +++ b/src/services/status_parser/status_parser.js @@ -10,7 +10,15 @@ export const muteFilterHits = (muteFilters, status) => { return muteFilters .toSorted((a, b) => b.order - a.order) .map((filter) => { - const { hide, expires, name, value, type, enabled, caseSensitive = false } = filter + const { + hide, + expires, + name, + value, + type, + enabled, + caseSensitive = false, + } = filter if (!enabled) return false if (value === '') return false if (expires !== null && expires < Date.now()) return false @@ -18,9 +26,7 @@ export const muteFilterHits = (muteFilters, status) => { case 'word': { let match = false if (caseSensitive) { - match = - statusText.includes(value) || - statusSummary.includes(value) + match = statusText.includes(value) || statusSummary.includes(value) } else { const lowercaseValue = value.toLowerCase() match = @@ -56,7 +62,9 @@ export const muteFilterHits = (muteFilters, status) => { match = poster.toLowerCase().includes(lowercaseValue) || replyToUser.toLowerCase().includes(lowercaseValue) || - mentions.some((mention) => mention.toLowerCase().includes(lowercaseValue)) + mentions.some((mention) => + mention.toLowerCase().includes(lowercaseValue), + ) } if (match) { return { hide, name } diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index 021c31ef8..9a26bd12f 100644 --- a/src/services/status_poster/status_poster.service.js +++ b/src/services/status_poster/status_poster.service.js @@ -1,6 +1,11 @@ import { map } from 'lodash' -import apiService from '../api/api.service.js' +import { + editStatus as apiEditStatus, + postStatus as apiPostStatus, + setMediaDescription as apiSetMediaDescription, + uploadMedia as apiUploadMedia, +} from 'src/api/user.js' const postStatus = ({ store, @@ -18,37 +23,30 @@ const postStatus = ({ }) => { const mediaIds = map(media, 'id') - return apiService - .postStatus({ - credentials: store.state.users.currentUser.credentials, - status, - spoilerText, - visibility, - sensitive, - mediaIds, - inReplyToStatusId, - quoteId, - contentType, - poll, - preview, - idempotencyKey, - }) - .then((data) => { - if (!data.error && !preview) { - store.dispatch('addNewStatuses', { - statuses: [data], - timeline: 'friends', - showImmediately: true, - noIdUpdate: true, // To prevent missing notices on next pull. - }) - } - return data - }) - .catch((err) => { - return { - error: err.message, - } - }) + return apiPostStatus({ + credentials: store.state.users.currentUser.credentials, + status, + spoilerText, + visibility, + sensitive, + mediaIds, + inReplyToStatusId, + quoteId, + contentType, + poll, + preview, + idempotencyKey, + }).then(({ data }) => { + if (!preview) + store.dispatch('addNewStatuses', { + statuses: [data], + timeline: 'friends', + showImmediately: true, + noIdUpdate: true, // To prevent missing notices on next pull. + }) + + return data + }) } const editStatus = ({ @@ -63,26 +61,24 @@ const editStatus = ({ }) => { const mediaIds = map(media, 'id') - return apiService - .editStatus({ - id: statusId, - credentials: store.state.users.currentUser.credentials, - status, - spoilerText, - sensitive, - poll, - mediaIds, - contentType, - }) - .then((data) => { - if (!data.error) { - store.dispatch('addNewStatuses', { - statuses: [data], - timeline: 'friends', - showImmediately: true, - noIdUpdate: true, // To prevent missing notices on next pull. - }) - } + return apiEditStatus({ + id: statusId, + credentials: store.state.users.currentUser.credentials, + status, + spoilerText, + sensitive, + poll, + mediaIds, + contentType, + }) + .then(({ data }) => { + store.dispatch('addNewStatuses', { + statuses: [data], + timeline: 'friends', + showImmediately: true, + noIdUpdate: true, // To prevent missing notices on next pull. + }) + return data }) .catch((err) => { @@ -95,12 +91,14 @@ const editStatus = ({ const uploadMedia = ({ store, formData }) => { const credentials = store.state.users.currentUser.credentials - return apiService.uploadMedia({ credentials, formData }) + return apiUploadMedia({ credentials, formData }).then(({ data }) => data) } const setMediaDescription = ({ store, id, description }) => { const credentials = store.state.users.currentUser.credentials - return apiService.setMediaDescription({ credentials, id, description }) + return apiSetMediaDescription({ credentials, id, description }).then( + ({ data }) => data, + ) } const statusPosterService = { diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 2a695f4db..0ea0341e2 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -8,6 +8,7 @@ import { getEngineChecksum, init } from '../theme_data/theme_data_3.service.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' +import { promisedRequest } from 'src/api/helpers.js' import { ROOT_CONFIG } from 'src/modules/default_config_state.js' // On platforms where this is not supported, it will return undefined @@ -300,7 +301,7 @@ export const applyStyleConfig = (input) => { adoptStyleSheets() } -export const getResourcesIndex = async (url, parser = JSON.parse) => { +export const getResourcesIndex = async (url, parser = (x) => x) => { const cache = 'no-store' const customUrl = url.replace(/\.(\w+)$/, '.custom.$1') let builtin @@ -314,10 +315,11 @@ export const getResourcesIndex = async (url, parser = JSON.parse) => { return [ k, () => - window - .fetch(v, { cache }) - .then((data) => data.text()) - .then((text) => parser(text)) + promisedRequest({ + url: v, + cache, + }) + .then(({ data: text }) => parser(text)) .catch((e) => { console.error(e) return null @@ -331,18 +333,19 @@ export const getResourcesIndex = async (url, parser = JSON.parse) => { } try { - const builtinData = await window.fetch(url, { cache }) - const builtinResources = await builtinData.json() - builtin = resourceTransform(builtinResources) + const { data: builtinData } = await promisedRequest({ url, cache }) + builtin = resourceTransform(builtinData) } catch { builtin = [] console.warn(`Builtin resources at ${url} unavailable`) } try { - const customData = await window.fetch(customUrl, { cache }) - const customResources = await customData.json() - custom = resourceTransform(customResources) + const { data: customData } = await promisedRequest({ + url: customUrl, + cache, + }) + custom = resourceTransform(customData) } catch { custom = [] console.warn(`Custom resources at ${customUrl} unavailable`) diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index 7babbaf32..0c5c82991 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -99,12 +99,16 @@ export const findColor = (color, { dynamicVars, staticVars }) => { const staticVar = staticVars[variableSlot] const dynamicVar = dynamicVars[variableSlot] if (!staticVar && !dynamicVar) { - console.warn(dynamicVars, variableSlot, dynamicVars[variableSlot]) - console.warn(`Couldn't find variable "${variableSlot}", falling back to magenta. Variables are: + console.warn( + `Couldn't find variable "${variableSlot}", falling back to magenta. Variables are: Static: ${JSON.stringify(staticVars, null, 2)} Dynamic: -${JSON.stringify(dynamicVars, null, 2)}`) +${JSON.stringify(dynamicVars, null, 2)}`, + dynamicVars, + variableSlot, + dynamicVars[variableSlot], + ) } targetColor = convert(staticVar ?? dynamicVar ?? '#FF00FF').rgb } @@ -499,10 +503,7 @@ export const init = ({ }), ) const lastVariantRule = variantRules[variantRules.length - 1] - const lastVariantSelector = ruleToSelector( - lastVariantRule, - true, - ) + const lastVariantSelector = ruleToSelector(lastVariantRule, true) if (lastVariantRule && lastVariantSelector !== selector) { inheritRule = lastVariantRule diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 68991addf..80dbc75d0 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -1,13 +1,13 @@ import { camelCase } from 'lodash' -import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { fetchTimeline } from 'src/api/timelines.js' + const update = ({ store, statuses, @@ -35,13 +35,13 @@ const fetchAndUpdate = ({ timeline = 'friends', older = false, showImmediately = false, - userId = false, - listId = false, - statusId = false, - bookmarkFolderId = false, - tag = false, - until, - since, + userId, + listId, + statusId, + bookmarkFolderId, + tag, + maxId, + sinceId, }) => { const args = { timeline, credentials } const rootState = store.rootState || store.state @@ -51,12 +51,13 @@ const fetchAndUpdate = ({ const loggedIn = !!rootState.users.currentUser if (older) { - args.until = until || timelineData.minId + // When minId = 0 we need to fetch without maxId param + args.maxId = maxId || timelineData.minId || null } else { - if (since === undefined) { - args.since = timelineData.maxId - } else if (since !== null) { - args.since = since + if (sinceId === undefined) { + args.sinceId = timelineData.maxId + } else if (sinceId !== null) { + args.sinceId = sinceId } } @@ -75,17 +76,8 @@ const fetchAndUpdate = ({ const numStatusesBeforeFetch = timelineData.statuses.length - return apiService - .fetchTimeline(args) + return fetchTimeline(args) .then((response) => { - if (response.errors) { - if (timeline === 'favorites') { - useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false - return - } - throw new Error(`${response.status} ${response.statusText}`) - } - const { data: statuses, pagination } = response if ( !older && @@ -107,6 +99,10 @@ const fetchAndUpdate = ({ return { statuses, pagination } }) .catch((error) => { + if (error.statusCode === 403 && timeline === 'favorites') { + useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false + return + } useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'timeline.error', @@ -120,11 +116,11 @@ const startFetching = ({ timeline = 'friends', credentials, store, - userId = false, - listId = false, - statusId = false, - bookmarkFolderId = false, - tag = false, + userId, + listId, + statusId, + bookmarkFolderId, + tag, }) => { const rootState = store.rootState || store.state const timelineData = rootState.statuses.timelines[camelCase(timeline)] diff --git a/src/stores/admin_settings.js b/src/stores/admin_settings.js new file mode 100644 index 000000000..5aa91c819 --- /dev/null +++ b/src/stores/admin_settings.js @@ -0,0 +1,615 @@ +import { cloneDeep, differenceWith, flatten, get, isEqual, set } from 'lodash' +import { defineStore } from 'pinia' + +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + addNewEmojiFile, + changeStatusScope, + createEmojiPack, + deleteAccounts, + deleteEmojiPack, + disableMFA, + downloadRemoteEmojiPack, + downloadRemoteEmojiPackZIP, + getAvailableFrontends, + getInstanceConfigDescriptions, + getInstanceDBConfig, + getUserData, + importEmojiFromFS, + installFrontend, + listRemoteEmojiPacks, + listStatuses, + listUsers, + pushInstanceDBConfig, + reloadEmoji, + requirePasswordChange, + resendConfirmationEmail, + setUsersActivationStatus, + setUsersApprovalStatus, + setUsersConfirmationStatus, + setUsersRight, + setUsersSuggestionStatus, + setUsersTags, +} from 'src/api/admin.js' +import { listEmojiPacks } from 'src/api/public.js' +import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.service.js' + +export const defaultState = { + frontends: [], + loaded: false, + needsReboot: null, + config: null, + modifiedPaths: null, + descriptions: null, + draft: null, + dbConfigEnabled: null, +} + +export const newUserFlags = { + ...defaultState.flagStorage, +} + +export const useAdminSettingsStore = defineStore('adminSettings', { + state: () => ({ + ...cloneDeep(defaultState), + }), + actions: { + // Configuration Stuff + setInstanceAdminNoDbConfig() { + this.loaded = false + this.dbConfigEnabled = false + }, + updateAdminSettings({ config, modifiedPaths }) { + this.loaded = true + this.dbConfigEnabled = true + this.config = config + this.modifiedPaths = modifiedPaths + }, + updateAdminDescriptions({ descriptions }) { + this.descriptions = descriptions + }, + updateAdminDraft({ path, value }) { + const [group, key, subkey] = path + const parent = [group, key, subkey] + + set(this.draft, path, value) + + // force-updating grouped draft to trigger refresh of group settings + if (path.length > parent.length) { + set(this.draft, parent, cloneDeep(get(this.draft, parent))) + } + }, + resetAdminDraft() { + this.draft = cloneDeep(this.config) + }, + + loadAdminStuff() { + getInstanceDBConfig({ + credentials: useOAuthStore().token, + }) + .then(({ data: backendDbConfig }) => + this.setInstanceAdminSettings({ + credentials: useOAuthStore().token, + backendDbConfig, + }), + ) + .catch(({ statusCode, statusText }) => { + if (statusCode === 400) { + if (/configurable_from_database/.test(statusText)) { + this.setInstanceAdminNoDbConfig() + } + } + }) + if (this.descriptions === null) { + getInstanceConfigDescriptions({ + credentials: useOAuthStore().token, + }).then(({ data: backendDescriptions }) => + this.setInstanceAdminDescriptions({ + credentials: useOAuthStore().token, + backendDescriptions, + }), + ) + } + }, + setInstanceAdminSettings({ backendDbConfig }) { + const config = this.config || {} + const modifiedPaths = new Set() + + backendDbConfig.configs.forEach((c) => { + const path = [c.group, c.key] + if (c.db) { + // Path elements can contain dot, therefore we use ' -> ' as a separator instead + // Using strings for modified paths for easier searching + c.db.forEach((x) => modifiedPaths.add([...path, x].join(' -> '))) + } + + // we need to preserve tuples on second level only, possibly third + // but it's not a case right now. + const convert = (value, preserveTuples, preserveTuplesLv2) => { + if (Array.isArray(value) && value.length > 0 && value[0].tuple) { + if (!preserveTuples) { + return value.reduce((acc, c) => { + if (c.tuple == null) { + return { + ...acc, + [c]: c, + } + } + return { + ...acc, + [c.tuple[0]]: convert(c.tuple[1], preserveTuplesLv2), + } + }, {}) + } else { + return value.map((x) => x.tuple) + } + } else { + if (!preserveTuples) { + return value + } else { + return value.tuple + } + } + } + // for most stuff we want maps since those are more convenient + // however this doesn't allow for multiple values per same key + // so for those cases we want to preserve tuples as-is + // right now it's made exclusively for :pleroma.:rate_limit + // so it might not work properly elsewhere + const preserveTuples = path.find((x) => x === ':rate_limit') + set(config, path, convert(c.value, false, preserveTuples)) + }) + // patching http adapter config to be easier to handle + const adapter = config[':pleroma'][':http'][':adapter'] + if (Array.isArray(adapter)) { + config[':pleroma'][':http'][':adapter'] = { + [':ssl_options']: { + [':versions']: [], + }, + } + } + this.updateAdminSettings({ config, modifiedPaths }) + this.resetAdminDraft() + }, + setInstanceAdminDescriptions({ backendDescriptions }) { + const convert = ( + { children, description, label, key = '', group, suggestions }, + path, + acc, + ) => { + const newPath = group ? [group, key] : [key] + const obj = { description, label, suggestions } + if (Array.isArray(children)) { + children.forEach((c) => { + convert(c, newPath, obj) + }) + } + set(acc, newPath, obj) + } + + const descriptions = {} + + backendDescriptions.forEach((d) => convert(d, '', descriptions)) + this.updateAdminDescriptions({ descriptions }) + }, + + // This action takes draft state, diffs it with live config state and then pushes + // only differences between the two. Difference detection only work up to subkey (third) level. + pushAdminDraft() { + // TODO cleanup paths in modifiedPaths + const convert = (value) => { + if (typeof value !== 'object') { + return value + } else if (Array.isArray(value)) { + return value.map(convert) + } else { + return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] })) + } + } + + // Getting all group-keys used in config + const allGroupKeys = flatten( + Object.entries(this.config).map(([group, lv1data]) => + Object.keys(lv1data).map((key) => ({ group, key })), + ), + ) + + // Only using group-keys where there are changes detected + const changedGroupKeys = allGroupKeys.filter(({ group, key }) => { + return !isEqual(this.config[group][key], this.draft[group][key]) + }) + + // Here we take all changed group-keys and get all changed subkeys + const changed = changedGroupKeys.map(({ group, key }) => { + const config = this.config[group][key] + const draft = this.draft[group][key] + + // We convert group-key value into entries arrays + const eConfig = Object.entries(config) + const eDraft = Object.entries(draft) + + // Then those entries array we diff so only changed subkey entries remain + // We use the diffed array to reconstruct the object and then shove it into convert() + return { + group, + key, + value: convert( + Object.fromEntries(differenceWith(eDraft, eConfig, isEqual)), + ), + } + }) + + pushInstanceDBConfig({ + credentials: useOAuthStore().token, + payload: { + configs: changed, + }, + }) + .then(() => + getInstanceDBConfig({ + credentials: useOAuthStore().token, + }).then(({ data }) => data), + ) + .then((backendDbConfig) => + this.setInstanceAdminSettings({ + credentials: useOAuthStore().token, + + backendDbConfig, + }), + ) + }, + pushAdminSetting({ path, value }) { + const [group, key, ...rest] = Array.isArray(path) + ? path + : path.split(/\./g) + const clone = {} // not actually cloning the entire thing to avoid excessive writes + set(clone, rest, value) + + // TODO cleanup paths in modifiedPaths + const convert = (value) => { + if (typeof value !== 'object') { + return value + } else if (Array.isArray(value)) { + return value.map(convert) + } else { + return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] })) + } + } + + pushInstanceDBConfig({ + credentials: useOAuthStore().token, + payload: { + configs: [ + { + group, + key, + value: convert(clone), + }, + ], + }, + }) + .then(() => + getInstanceDBConfig({ + credentials: useOAuthStore().token, + }).then(({ data }) => data), + ) + .then((backendDbConfig) => + this.setInstanceAdminSettings({ + credentials: useOAuthStore().token, + backendDbConfig, + }), + ) + }, + resetAdminSetting({ path }) { + const [group, key, subkey] = Array.isArray(path) + ? path + : path.split(/\./g) + + this.modifiedPaths.delete(path) + + return pushInstanceDBConfig({ + credentials: useOAuthStore().token, + payload: { + configs: [ + { + group, + key, + delete: true, + subkeys: [subkey], + }, + ], + }, + }) + .then(() => + getInstanceDBConfig({ + credentials: useOAuthStore().token, + }).then(({ data }) => data), + ) + .then((backendDbConfig) => + this.setInstanceAdminSettings({ backendDbConfig }), + ) + }, + + // Frontends Stuff + loadFrontendsStuff() { + getAvailableFrontends({ + credentials: useOAuthStore().token, + }).then(({ data: frontends }) => + this.setAvailableFrontends({ frontends }), + ) + }, + + setAvailableFrontends({ frontends }) { + this.frontends = frontends.map((f) => { + f.installedRefs = f.installed_refs + if (f.name === 'pleroma-fe') { + f.refs = ['master', 'develop'] + } else { + f.refs = [f.ref] + } + return f + }) + }, + + installFrontend() { + return installFrontend({ + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + + // Statuses stuff + async fetchStatuses(opts) { + const { + data: { total, activities }, + } = await listStatuses({ + credentials: useOAuthStore().token, + opts, + }) + + const statuses = activities.map(parseStatus) + + await window.vuex.dispatch('addNewStatuses', { statuses }) + + return { + items: statuses, + count: total, + } + }, + async changeStatusScope(opts) { + const { data } = await changeStatusScope({ + credentials: useOAuthStore().token, + opts, + }) + const status = parseStatus(data) + + await window.vuex.dispatch('addNewStatuses', { statuses: [status] }) + }, + + // Users stuff + async fetchUsers(opts) { + const { + data: { users, count }, + } = await listUsers({ + credentials: useOAuthStore().token, + opts, + }) + + return { + items: await Promise.all( + users.map( + async (userAdminData) => + await window.vuex.dispatch('updateUserAdminData', { + userAdminData, + }), + ), + ), + count, + } + }, + async getUserData({ user }) { + const api = getUserData + const { screen_name } = user + + const result = await api({ + credentials: useOAuthStore().token, + screen_name, + }) + + window.vuex.commit('updateUserAdminData', { user: result.data }) + }, + async deleteUsers({ users }) { + const screen_names = users.map((u) => u.screen_name) + const api = deleteAccounts + + const resultUserIds = await api({ + credentials: useOAuthStore().token, + screen_names, + }) + + resultUserIds.data.forEach((userId) => { + window.vuex.dispatch( + 'markStatusesAsDeleted', + (status) => userId === status.user.id, + ) + // TODO when migrated to pinia, also remove user + }) + + return resultUserIds + }, + resendConfirmationEmail({ users }) { + const screen_names = users.map((u) => u.screen_name) + + return resendConfirmationEmail({ + credentials: useOAuthStore().token, + screen_names, + }).then(({ data }) => data) + }, + requirePasswordChange({ users }) { + const screen_names = users.map((u) => u.screen_name) + + return requirePasswordChange({ + credentials: useOAuthStore().token, + screen_names, + }).then(({ data }) => data) + }, + // Singular only! + disableMFA({ user }) { + const { screen_name } = user + + return disableMFA({ + credentials: useOAuthStore().token, + screen_name, + }).then(({ data }) => data) + }, + async setUsersTags({ users, tags, value }) { + const screen_names = users.map((u) => u.screen_name) + const api = setUsersTags + + await api({ + credentials: useOAuthStore().token, + screen_names, + tags, + value, + }) + + users.forEach((user) => { + this.getUserData({ user }) + }) + }, + async setUsersRight({ users, right, value }) { + const screen_names = users.map((u) => u.screen_name) + const api = setUsersRight + + await api({ + credentials: useOAuthStore().token, + screen_names, + right, + value, + }) + + users.forEach((user) => { + window.vuex.commit('updateRight', { user, right, value }) + }) + }, + async setUsersActivationStatus({ users, value }) { + const screen_names = users.map((u) => u.screen_name) + const api = setUsersActivationStatus + + const resultUsers = await api({ + credentials: useOAuthStore().token, + screen_names, + value, + }) + + resultUsers.data.forEach((user) => { + window.vuex.commit('updateUserAdminData', { user }) + }) + }, + async setUsersSuggestionStatus({ users, value }) { + const screen_names = users.map((u) => u.screen_name) + const api = setUsersSuggestionStatus + + const resultUsers = await api({ + credentials: useOAuthStore().token, + screen_names, + value, + }) + + resultUsers.data.forEach((user) => { + window.vuex.commit('updateUserAdminData', { user }) + }) + }, + async setUsersConfirmationStatus({ users }) { + const screen_names = users.map((u) => u.screen_name) + const api = setUsersConfirmationStatus + + await api({ + credentials: useOAuthStore().token, + screen_names, + }) + + users.forEach((user) => { + this.getUserData({ user }) + }) + }, + async setUsersApprovalStatus({ users }) { + const screen_names = users.map((u) => u.screen_name) + const api = setUsersApprovalStatus + + const resultUsers = await api({ + credentials: useOAuthStore().token, + screen_names, + }) + + resultUsers.data.forEach((user) => { + window.vuex.commit('updateUserAdminData', { user }) + }) + }, + reloadEmoji() { + return reloadEmoji({ credentials: useOAuthStore().token }).then( + ({ data }) => data, + ) + }, + importEmojiFromFS() { + return importEmojiFromFS({ credentials: useOAuthStore().token }).then( + ({ data }) => data, + ) + }, + listEmojiPacks(params) { + return listEmojiPacks({ + ...params, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + listRemoteEmojiPacks(params) { + return listRemoteEmojiPacks({ + ...params, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + addNewEmojiFile({ packName, file, shortcode, filename }) { + return addNewEmojiFile({ + packName, + file, + shortcode, + filename, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + downloadRemoteEmojiPack({ instance, packName, as }) { + return downloadRemoteEmojiPack({ + instance, + packName, + as, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + downloadRemoteEmojiPackZIP({ url, packName }) { + return downloadRemoteEmojiPackZIP({ + url, + packName, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + createEmojiPack({ name }) { + return createEmojiPack({ + name, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + deleteEmojiPack({ name }) { + return deleteEmojiPack({ + name, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + saveEmojiPackMetadata({ name, newData }) { + return createEmojiPack({ + name, + newData, + credentials: useOAuthStore().token, + }).then(({ data }) => data) + }, + }, +}) diff --git a/src/stores/announcements.js b/src/stores/announcements.js index bf88666c6..a5f3e4d8e 100644 --- a/src/stores/announcements.js +++ b/src/stores/announcements.js @@ -1,5 +1,9 @@ import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { dismissAnnouncement, getAnnouncements } from 'src/api/user.js' + const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5 export const useAnnouncementsStore = defineStore('announcements', { @@ -7,6 +11,8 @@ export const useAnnouncementsStore = defineStore('announcements', { announcements: [], supportsAnnouncements: true, fetchAnnouncementsTimer: undefined, + adminActions: {}, + userActions: {}, }), getters: { unreadAnnouncementCount() { @@ -21,29 +27,37 @@ export const useAnnouncementsStore = defineStore('announcements', { }, }, actions: { - fetchAnnouncements() { - if (!this.supportsAnnouncements) { - return Promise.resolve() - } + async fetchAnnouncements() { + if (!this.supportsAnnouncements) return const currentUser = window.vuex.state.users.currentUser const isAdmin = currentUser && - currentUser.privileges.includes('announcements_manage_announcements') + currentUser.privileges.has('announcements_manage_announcements') - const getAnnouncements = async () => { - if (!isAdmin) { - return window.vuex.state.api.backendInteractor.fetchAnnouncements() + try { + if (isAdmin) { + this.adminActions = await import('src/api/admin.js') + } else { + const all = await getAnnouncements({ + credentials: useOAuthStore().token, + }) + return all.data } - const all = - await window.vuex.state.api.backendInteractor.adminFetchAnnouncements() - const visible = - await window.vuex.state.api.backendInteractor.fetchAnnouncements() + const { data: all } = await this.adminActions.getAnnouncements({ + credentials: useOAuthStore().token, + }) + + const { data: visible } = await getAnnouncements({ + credentials: useOAuthStore().token, + }) + const visibleObject = visible.reduce((a, c) => { a[c.id] = c return a }, {}) + const getWithinVisible = (announcement) => visibleObject[announcement.id] @@ -56,35 +70,30 @@ export const useAnnouncementsStore = defineStore('announcements', { } }) - return all + this.announcements = all + } catch (error) { + // If and only if backend does not support announcements, it would return 404. + // In this case, silently ignores it. + if (error && error.statusCode === 404) { + this.supportsAnnouncements = false + } else { + throw error + } } - - return getAnnouncements() - .then((announcements) => { - this.announcements = announcements - }) - .catch((error) => { - // If and only if backend does not support announcements, it would return 404. - // In this case, silently ignores it. - if (error && error.statusCode === 404) { - this.supportsAnnouncements = false - } else { - throw error - } - }) }, markAnnouncementAsRead(id) { - return window.vuex.state.api.backendInteractor - .dismissAnnouncement({ id }) - .then(() => { - const index = this.announcements.findIndex((a) => a.id === id) + return dismissAnnouncement({ + id, + credentials: useOAuthStore().token, + }).then(() => { + const index = this.announcements.findIndex((a) => a.id === id) - if (index < 0) { - return - } + if (index < 0) { + return + } - this.announcements[index].read = true - }) + this.announcements[index].read = true + }) }, startFetchingAnnouncements() { if (this.fetchAnnouncementsTimer) { @@ -105,22 +114,38 @@ export const useAnnouncementsStore = defineStore('announcements', { clearInterval(interval) }, postAnnouncement({ content, startsAt, endsAt, allDay }) { - return window.vuex.state.api.backendInteractor - .postAnnouncement({ content, startsAt, endsAt, allDay }) + return this.adminActions + .postAnnouncement({ + credentials: useOAuthStore().token, + content, + startsAt, + endsAt, + allDay, + }) .then(() => { return this.fetchAnnouncements() }) }, editAnnouncement({ id, content, startsAt, endsAt, allDay }) { - return window.vuex.state.api.backendInteractor - .editAnnouncement({ id, content, startsAt, endsAt, allDay }) + return this.adminActions + .editAnnouncement({ + id, + content, + startsAt, + endsAt, + allDay, + credentials: useOAuthStore().token, + }) .then(() => { return this.fetchAnnouncements() }) }, deleteAnnouncement(id) { - return window.vuex.state.api.backendInteractor - .deleteAnnouncement({ id }) + return this.adminActions + .deleteAnnouncement({ + id, + credentials: useOAuthStore().token, + }) .then(() => { return this.fetchAnnouncements() }) diff --git a/src/stores/bookmark_folders.js b/src/stores/bookmark_folders.js index 028322f9d..713a21d00 100644 --- a/src/stores/bookmark_folders.js +++ b/src/stores/bookmark_folders.js @@ -1,6 +1,16 @@ import { find, remove } from 'lodash' import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + createBookmarkFolder, + deleteBookmarkFolder, + fetchBookmarkFolders, + updateBookmarkFolder, +} from 'src/api/user.js' +import { promiseInterval } from 'src/services/promise_interval/promise_interval.js' + export const useBookmarkFoldersStore = defineStore('bookmarkFolders', { state: () => ({ allFolders: [], @@ -16,6 +26,20 @@ export const useBookmarkFoldersStore = defineStore('bookmarkFolders', { }, }, actions: { + startFetching() { + this.fetcher = promiseInterval(() => { + fetchBookmarkFolders({ + credentials: useOAuthStore().token, + }) + .then(({ data: folders }) => this.setBookmarkFolders(folders)) + .catch((e) => { + console.error(e) + }) + }, 240000) + }, + stopFetching() { + this.fetcher?.stop() + }, setBookmarkFolders(value) { this.allFolders = value }, @@ -30,23 +54,31 @@ export const useBookmarkFoldersStore = defineStore('bookmarkFolders', { } }, createBookmarkFolder({ name, emoji }) { - return window.vuex.state.api.backendInteractor - .createBookmarkFolder({ name, emoji }) - .then((folder) => { - this.setBookmarkFolder(folder) - return folder - }) + return createBookmarkFolder({ + name, + emoji, + credentials: useOAuthStore().token, + }).then(({ data: folder }) => { + this.setBookmarkFolder(folder) + return folder + }) }, updateBookmarkFolder({ folderId, name, emoji }) { - return window.vuex.state.api.backendInteractor - .updateBookmarkFolder({ folderId, name, emoji }) - .then((folder) => { - this.setBookmarkFolder(folder) - return folder - }) + return updateBookmarkFolder({ + credentials: useOAuthStore().token, + folderId, + name, + emoji, + }).then(({ data: folder }) => { + this.setBookmarkFolder(folder) + return folder + }) }, deleteBookmarkFolder({ folderId }) { - window.vuex.state.api.backendInteractor.deleteBookmarkFolder({ folderId }) + deleteBookmarkFolder({ + folderId, + credentials: useOAuthStore().token, + }) remove(this.allFolders, (folder) => folder.id === folderId) }, }, diff --git a/src/stores/emoji.js b/src/stores/emoji.js index 0673862e9..3316f8328 100644 --- a/src/stores/emoji.js +++ b/src/stores/emoji.js @@ -1,7 +1,10 @@ +import { merge } from 'lodash' import { defineStore } from 'pinia' import { useInstanceStore } from 'src/stores/instance.js' +import { useOAuthStore } from 'src/stores/oauth.js' +import { listEmojiPacks } from 'src/api/public.js' import { ensureFinalFallback } from 'src/i18n/languages.js' import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations' @@ -10,6 +13,8 @@ const defaultState = { // Custom emoji from server customEmoji: [], customEmojiFetched: false, + adminPacksLocal: null, + adminPacksLocalLoading: true, // Unicode emoji from bundle emoji: {}, @@ -178,6 +183,66 @@ export const useEmojiStore = defineStore('emoji', { ) }, + async getAdminPacksLocal(refresh) { + if (!refresh && this.adminPacksLocal) return this.adminPacksLocal + this.adminPacksLocalLoading = true + this.adminPacksLocal = await this.getAdminPacks( + useInstanceStore().server, + (params) => + listEmojiPacks({ + ...params, + credentials: useOAuthStore().token, + }).then(({ data }) => data), + ) + this.adminPacksLocalLoading = false + }, + + async getAdminPacks(instance, listFunction) { + const currentUser = window.vuex.state.users.currentUser + + if (!currentUser.rights.admin) return + + const pageSize = 25 + + return await listFunction({ + instance, + page: 1, + pageSize: 0, + }) + .then((data) => { + const promises = [] + + for (let i = 0; i < Math.ceil(data.count / pageSize); i++) { + promises.push( + listFunction({ + instance, + page: i, + pageSize, + }).then((pageData) => { + return pageData.packs + }), + ) + } + + return Promise.all(promises).then((results) => { + return merge({}, ...results) + }) + }) + .then((allPacks) => { + // Sort by key + return Object.keys(allPacks) + .sort() + .reduce((acc, key) => { + if (key.length === 0) return acc + acc[key] = allPacks[key] + return acc + }, {}) + }) + .catch((data) => { + console.error(data) + }) + }, + async getCustomEmoji() { try { let res = await window.fetch('/api/v1/pleroma/emoji') diff --git a/src/stores/instance.js b/src/stores/instance.js index 54b3cf43c..02edd1235 100644 --- a/src/stores/instance.js +++ b/src/stores/instance.js @@ -1,4 +1,4 @@ -import { get, set } from 'lodash' +import { set } from 'lodash' import { defineStore } from 'pinia' import { @@ -11,10 +11,11 @@ import { LOCAL_DEFAULT_CONFIG_DEFINITIONS, validateSetting, } from '../modules/default_config_state.js' -import apiService from '../services/api/api.service.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { fetchKnownDomains } from 'src/api/public.js' + const REMOTE_INTERACTION_URL = '/main/ostatus' const ROOT_STATE_DEFINITIONS = { @@ -210,9 +211,10 @@ export const useInstanceStore = defineStore('instance', { }, async getKnownDomains() { try { - this.knownDomains = await apiService.fetchKnownDomains({ + const { data } = await fetchKnownDomains({ credentials: window.vuex.state.users.currentUser.credentials, }) + this.knownDomains = data } catch (e) { console.warn("Can't load known domains\n", e) } diff --git a/src/stores/interface.js b/src/stores/interface.js index 111700fbb..951811c62 100644 --- a/src/stores/interface.js +++ b/src/stores/interface.js @@ -58,8 +58,10 @@ export const useInterfaceStore = defineStore('interface', { }, layoutType: 'normal', globalNotices: [], + globalError: null, layoutHeight: 0, lastTimeline: null, + foreignProfileBackground: null, }), actions: { setTemporaryChanges({ confirm, revert }) { @@ -96,6 +98,9 @@ export const useInterfaceStore = defineStore('interface', { console.error(`${error}`) } }, + setForeignProfileBackground(url) { + this.foreignProfileBackground = url + }, settingsSaved({ success, error }) { if (success) { if (this.noticeClearTimeout) { @@ -129,7 +134,24 @@ export const useInterfaceStore = defineStore('interface', { } } }, - togglePeekSettingsModal() { + setSettingsModalState(newState) { + const oldState = this.settingsModalState + const legal = (() => { + switch (oldState) { + case 'minimized': + return true + case 'visible': + return true + case 'hidden': + return newState === 'visible' + } + })() + + if (legal) { + this.settingsModalState = newState + } + }, + toggleMinimizeSettingsModal() { switch (this.settingsModalState) { case 'minimized': this.settingsModalState = 'visible' @@ -137,8 +159,12 @@ export const useInterfaceStore = defineStore('interface', { case 'visible': this.settingsModalState = 'minimized' return + case 'hidden': + return default: - throw new Error('Illegal minimization state of settings modal') + throw new Error( + `Illegal minimization state of settings modal: ${this.settingsModalState}`, + ) } }, clearSettingsModalTargetTab() { @@ -151,11 +177,35 @@ export const useInterfaceStore = defineStore('interface', { removeGlobalNotice(notice) { this.globalNotices = this.globalNotices.filter((n) => n !== notice) }, + setGlobalError({ error, instance, info }) { + console.log(info) + switch (info) { + case 'https://vuejs.org/error-reference/#runtime-13': { + this.globalError = { + title: 'general.refresh_required', + content: 'general.refresh_required_content', + // `true` disables cache on Firefox (non-standard) + recover: () => window.location.reload(true), + recoverText: 'general.refresh_required_refresh', + error, + } + break + } + default: { + this.globalError = { error } + break + } + } + console.log(this.globalError) + }, + clearGlobalError() { + this.globalError = null + }, pushGlobalNotice({ messageKey, messageArgs = {}, level = 'error', - timeout = 0, + timeout = 5000, }) { const notice = { messageKey, @@ -168,7 +218,7 @@ export const useInterfaceStore = defineStore('interface', { // Adding a new element to array wraps it in a Proxy, which breaks the comparison // TODO: Generate UUID or something instead or relying on !== operator? const newNotice = this.globalNotices[this.globalNotices.length - 1] - if (timeout) { + if (timeout > 0) { setTimeout(() => this.removeGlobalNotice(newNotice), timeout) } diff --git a/src/stores/lists.js b/src/stores/lists.js index b33a119cc..37471d46e 100644 --- a/src/stores/lists.js +++ b/src/stores/lists.js @@ -1,8 +1,23 @@ import { find, remove } from 'lodash' import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { + addAccountsToList, + createList, + deleteList, + fetchLists, + getList, + getListAccounts, + removeAccountsFromList, + updateList, +} from 'src/api/user.js' +import { promiseInterval } from 'src/services/promise_interval/promise_interval.js' + export const useListsStore = defineStore('lists', { state: () => ({ + fetcher: null, allLists: [], allListsObject: {}, }), @@ -18,34 +33,57 @@ export const useListsStore = defineStore('lists', { }, }, actions: { + startFetching() { + this.fetcher = promiseInterval(() => { + fetchLists({ + credentials: useOAuthStore().token, + }) + .then(({ data: lists }) => this.setLists(lists)) + .catch((e) => { + console.error(e) + }) + }, 240000) + }, + stopFetching() { + this.fetcher?.stop() + }, setLists(value) { this.allLists = value }, - createList({ title }) { - return window.vuex.state.api.backendInteractor - .createList({ title }) - .then((list) => { - this.setList({ listId: list.id, title }) - return list - }) + async createList({ title }) { + return await createList({ + title, + credentials: useOAuthStore().token, + }).then(({ data: list }) => { + this.setList({ listId: list.id, title }) + return list + }) }, - fetchList({ listId }) { - return window.vuex.state.api.backendInteractor - .getList({ listId }) - .then((list) => this.setList({ listId: list.id, title: list.title })) + async fetchList({ listId }) { + return await getList({ + listId, + credentials: useOAuthStore().token, + }).then(({ data: list }) => + this.setList({ listId: list.id, title: list.title }), + ) }, - fetchListAccounts({ listId }) { - return window.vuex.state.api.backendInteractor - .getListAccounts({ listId }) - .then((accountIds) => { - if (!this.allListsObject[listId]) { - this.allListsObject[listId] = { accountIds: [] } - } - this.allListsObject[listId].accountIds = accountIds - }) + async fetchListAccounts({ listId }) { + return await getListAccounts({ + listId, + credentials: useOAuthStore().token, + }).then(({ data: accountIds }) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds = accountIds + }) }, - setList({ listId, title }) { - window.vuex.state.api.backendInteractor.updateList({ listId, title }) + async setList({ listId, title }) { + await updateList({ + listId, + title, + credentials: useOAuthStore().token, + }) if (!this.allListsObject[listId]) { this.allListsObject[listId] = { accountIds: [] } @@ -59,7 +97,7 @@ export const useListsStore = defineStore('lists', { entry.title = title } }, - setListAccounts({ listId, accountIds }) { + async setListAccounts({ listId, accountIds }) { const saved = this.allListsObject[listId]?.accountIds || [] const added = accountIds.filter((id) => !saved.includes(id)) const removed = saved.filter((id) => !accountIds.includes(id)) @@ -67,47 +105,62 @@ export const useListsStore = defineStore('lists', { this.allListsObject[listId] = { accountIds: [] } } this.allListsObject[listId].accountIds = accountIds + const promises = [] if (added.length > 0) { - window.vuex.state.api.backendInteractor.addAccountsToList({ - listId, - accountIds: added, - }) + promises.push( + addAccountsToList({ + listId, + accountIds: added, + credentials: useOAuthStore().token, + }), + ) } if (removed.length > 0) { - window.vuex.state.api.backendInteractor.removeAccountsFromList({ - listId, - accountIds: removed, - }) + promises.push( + removeAccountsFromList({ + listId, + accountIds: removed, + credentials: useOAuthStore().token, + }), + ) } + await Promise.all(promises) }, - addListAccount({ listId, accountId }) { - return window.vuex.state.api.backendInteractor - .addAccountsToList({ listId, accountIds: [accountId] }) - .then((result) => { - if (!this.allListsObject[listId]) { - this.allListsObject[listId] = { accountIds: [] } - } - this.allListsObject[listId].accountIds.push(accountId) - return result - }) + async addListAccount({ listId, accountId }) { + return await addAccountsToList({ + listId, + accountIds: [accountId], + credentials: useOAuthStore().token, + }).then((result) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds.push(accountId) + return result + }) }, - removeListAccount({ listId, accountId }) { - return window.vuex.state.api.backendInteractor - .removeAccountsFromList({ listId, accountIds: [accountId] }) - .then((result) => { - if (!this.allListsObject[listId]) { - this.allListsObject[listId] = { accountIds: [] } - } - const { accountIds } = this.allListsObject[listId] - const set = new Set(accountIds) - set.delete(accountId) - this.allListsObject[listId].accountIds = [...set] + async removeListAccount({ listId, accountId }) { + return await removeAccountsFromList({ + listId, + accountIds: [accountId], + credentials: useOAuthStore().token, + }).then((result) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + const { accountIds } = this.allListsObject[listId] + const set = new Set(accountIds) + set.delete(accountId) + this.allListsObject[listId].accountIds = [...set] - return result - }) + return result + }) }, - deleteList({ listId }) { - window.vuex.state.api.backendInteractor.deleteList({ listId }) + async deleteList({ listId }) { + await deleteList({ + listId, + credentials: useOAuthStore().token, + }) delete this.allListsObject[listId] remove(this.allLists, (list) => list.id === listId) diff --git a/src/stores/local_config.js b/src/stores/local_config.js index 5b15c6ff1..786da657b 100644 --- a/src/stores/local_config.js +++ b/src/stores/local_config.js @@ -1,8 +1,5 @@ import { cloneDeep, set } from 'lodash' import { defineStore } from 'pinia' -import { toRaw } from 'vue' - -import { useInstanceStore } from 'src/stores/instance' import { LOCAL_DEFAULT_CONFIG, diff --git a/src/stores/oauth.js b/src/stores/oauth.js index 2a79c2fa9..35cd67afb 100644 --- a/src/stores/oauth.js +++ b/src/stores/oauth.js @@ -2,11 +2,7 @@ import { defineStore } from 'pinia' import { useInstanceStore } from 'src/stores/instance.js' -import { - createApp, - getClientToken, - verifyAppToken, -} from 'src/services/new_api/oauth.js' +import { createApp, getClientToken, verifyAppToken } from 'src/api/oauth.js' // status codes about verifyAppToken (GET /api/v1/apps/verify_credentials) const isAppTokenRejected = (error) => @@ -41,10 +37,7 @@ export const useOAuthStore = defineStore('oauth', { userToken: false, }), getters: { - getToken() { - return this.userToken || this.appToken - }, - getUserToken() { + token() { return this.userToken }, }, @@ -64,9 +57,9 @@ export const useOAuthStore = defineStore('oauth', { }, async createApp() { const instance = useInstanceStore().server - const app = await createApp(instance) - this.setClientData(app) - return app + const app = await createApp({ instance }) + this.setClientData(app.data) + return app.data }, /// Use this if you want to get the client id and secret but are not interested /// in whether they are valid. @@ -88,8 +81,8 @@ export const useOAuthStore = defineStore('oauth', { clientSecret: this.clientSecret, instance, }) - this.setAppToken(res.access_token) - return res.access_token + this.setAppToken(res.data.access_token) + return res.data.access_token }, /// Use this if you want to ensure the app is still valid to use. /// @return {string} The access token to the app (not attached to any user) @@ -97,8 +90,7 @@ export const useOAuthStore = defineStore('oauth', { if (this.appToken) { try { await verifyAppToken({ - instance: useInstanceStore().server, - appToken: this.appToken, + credentials: this.appToken, }) return this.appToken } catch (e) { diff --git a/src/stores/oauth_tokens.js b/src/stores/oauth_tokens.js index ae9b396ac..4d18ffae7 100644 --- a/src/stores/oauth_tokens.js +++ b/src/stores/oauth_tokens.js @@ -1,25 +1,35 @@ import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchOAuthTokens, revokeOAuthToken } from 'src/api/user.js' + +/* Just to clear the confusion: + * OAuth Store is responsible for user authentication + * OAuth Tokens Store is responsible for *managing* all of the user's tokens, + * i.e. for current and other clients + */ export const useOAuthTokensStore = defineStore('oauthTokens', { state: () => ({ tokens: [], }), actions: { fetchTokens() { - window.vuex.state.api.backendInteractor - .fetchOAuthTokens() - .then((tokens) => { - this.swapTokens(tokens) - }) + fetchOAuthTokens({ + credentials: useOAuthStore().token, + }).then(({ data: tokens }) => { + this.swapTokens(tokens) + }) }, revokeToken(id) { - window.vuex.state.api.backendInteractor - .revokeOAuthToken({ id }) - .then((response) => { - if (response.status === 201) { - this.swapTokens(this.tokens.filter((token) => token.id !== id)) - } - }) + revokeOAuthToken({ + id, + credentials: useOAuthStore().token, + }).then(({ status }) => { + if (status === 201) { + this.swapTokens(this.tokens.filter((token) => token.id !== id)) + } + }) }, swapTokens(tokens) { this.tokens = tokens diff --git a/src/stores/polls.js b/src/stores/polls.js index aac8a2421..e2f4b0ab2 100644 --- a/src/stores/polls.js +++ b/src/stores/polls.js @@ -1,6 +1,11 @@ import { merge } from 'lodash' import { defineStore } from 'pinia' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { fetchPoll } from 'src/api/public.js' +import { vote } from 'src/api/user.js' + export const usePollsStore = defineStore('polls', { state: () => ({ // Contains key = id, value = number of trackers for this poll @@ -19,16 +24,17 @@ export const usePollsStore = defineStore('polls', { } }, updateTrackedPoll(pollId) { - window.vuex.state.api.backendInteractor - .fetchPoll({ pollId }) - .then((poll) => { - setTimeout(() => { - if (this.trackedPolls[pollId]) { - this.updateTrackedPoll(pollId) - } - }, 30 * 1000) - this.mergeOrAddPoll(poll) - }) + fetchPoll({ + pollId, + credentials: useOAuthStore().token, + }).then(({ data: poll }) => { + setTimeout(() => { + if (this.trackedPolls[pollId]) { + this.updateTrackedPoll(pollId) + } + }, 30 * 1000) + this.mergeOrAddPoll(poll) + }) }, trackPoll(pollId) { if (!this.trackedPolls[pollId]) { @@ -50,12 +56,14 @@ export const usePollsStore = defineStore('polls', { } }, votePoll({ pollId, choices }) { - return window.vuex.state.api.backendInteractor - .vote({ pollId, choices }) - .then((poll) => { - this.mergeOrAddPoll(poll) - return poll - }) + return vote({ + pollId, + choices, + credentials: useOAuthStore().token, + }).then(({ data: poll }) => { + this.mergeOrAddPoll(poll) + return poll + }) }, }, }) diff --git a/src/stores/reports.js b/src/stores/reports.js index d2cd4fd7c..7d319d8e3 100644 --- a/src/stores/reports.js +++ b/src/stores/reports.js @@ -1,7 +1,10 @@ -import filter from 'lodash/filter' +import { filter } from 'lodash' import { defineStore } from 'pinia' import { useInterfaceStore } from 'src/stores/interface.js' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { setReportState } from 'src/api/admin.js' export const useReportsStore = defineStore('reports', { state: () => ({ @@ -38,18 +41,21 @@ export const useReportsStore = defineStore('reports', { setReportState({ id, state }) { const oldState = this.reports[id].state this.reports[id].state = state - window.vuex.state.api.backendInteractor - .setReportState({ id, state }) - .catch((e) => { - console.error('Failed to set report state', e) - useInterfaceStore().pushGlobalNotice({ - level: 'error', - messageKey: 'general.generic_error_message', - messageArgs: [e.message], - timeout: 5000, - }) - this.reports[id].state = oldState + + setReportState({ + id, + state, + credentials: useOAuthStore().token, + }).catch((e) => { + console.error('Failed to set report state', e) + useInterfaceStore().pushGlobalNotice({ + level: 'error', + messageKey: 'general.generic_error_message', + messageArgs: [e.message], + timeout: 5000, }) + this.reports[id].state = oldState + }) }, addReport(report) { this.reports[report.id] = report diff --git a/src/stores/sync_config.js b/src/stores/sync_config.js index 722dd7f16..47c6978d5 100644 --- a/src/stores/sync_config.js +++ b/src/stores/sync_config.js @@ -14,15 +14,16 @@ import { uniqWith, unset, } from 'lodash' -import { v4 as uuidv4 } from 'uuid' import { defineStore } from 'pinia' +import { v4 as uuidv4 } from 'uuid' import { toRaw } from 'vue' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useLocalConfigStore } from 'src/stores/local_config.js' +import { useOAuthStore } from 'src/stores/oauth.js' +import { updateProfileJSON } from 'src/api/user.js' import { storage } from 'src/lib/storage.js' import { makeUndefined, @@ -231,9 +232,17 @@ export const _mergeJournal = (...journals) => { Object.hasOwn(entry, 'timestamp'), ) const grouped = groupBy(allJournals, 'path') - const trimmedGrouped = Object.entries(grouped).map(([path, journal]) => { - // side effect - journal.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1)) + const trimmedGrouped = Object.entries(grouped).map(([path, rawJournal]) => { + const journal = rawJournal + .map((data, index) => ({ data, index })) + .toSorted(({ data: a, index: ai }, { data: b, index: bi }) => { + if (a.timestamp === b.timestamp) { + return ai - bi + } else { + return a.timestamp > b.timestamp ? 1 : -1 + } + }) + .map((x) => x.data) if (path.startsWith('collections')) { const lastRemoveIndex = findLastIndex( @@ -268,9 +277,16 @@ export const _mergeJournal = (...journals) => { } }) - const flat = flatten(trimmedGrouped).sort((a, b) => - a.timestamp > b.timestamp ? 1 : -1, - ) + const flat = flatten(trimmedGrouped) + .map((data, index) => ({ data, index })) + .toSorted(({ data: a, index: ai }, { data: b, index: bi }) => { + if (a.timestamp === b.timestamp) { + return ai - bi + } else { + return a.timestamp > b.timestamp ? 1 : -1 + } + }) + .map((x) => x.data) return take(flat, 500) } @@ -684,8 +700,6 @@ export const useSyncConfigStore = defineStore('sync_config', { `Already migrated Values: ${[...migratedEntries].join() || '[none]'}`, ) - const { configMigration } = useSyncConfigStore().flagStorage - Object.entries(oldDefaultConfigSync).forEach(([key, value]) => { const oldValue = config[key] const defaultValue = value @@ -791,18 +805,22 @@ export const useSyncConfigStore = defineStore('sync_config', { if (!needPush) return this.updateCache({ username: window.vuex.state.users.currentUser.fqn }) const params = { pleroma_settings_store: { 'pleroma-fe': this.cache } } - window.vuex.state.api.backendInteractor.updateProfileJSON({ params }) + updateProfileJSON({ + params, + credentials: useOAuthStore().token, + }) }, }, persist: { afterLoad(state) { console.debug('Validating persisted state of SyncConfig') const newState = { ...state } + newState.prefsStorage = newState.prefsStorage || {} const newEntries = Object.entries(ROOT_CONFIG).map(([path, value]) => { const definition = ROOT_CONFIG_DEFINITIONS[path] const finalValue = validateSetting({ path, - value: newState.prefsStorage.simple[path], + value: newState.prefsStorage.simple?.[path], definition, throwError: false, validateObjects: false, diff --git a/src/stores/user_highlight.js b/src/stores/user_highlight.js index 5ca13b6a9..759ebd509 100644 --- a/src/stores/user_highlight.js +++ b/src/stores/user_highlight.js @@ -1,19 +1,18 @@ import { merge as _merge, - clamp, clone, cloneDeep, - findLastIndex, flatten, - get, groupBy, isEqual, takeRight, - uniqWith, } from 'lodash' import { defineStore } from 'pinia' import { toRaw } from 'vue' +import { useOAuthStore } from 'src/stores/oauth.js' + +import { updateProfileJSON } from 'src/api/user.js' import { storage } from 'src/lib/storage.js' export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically @@ -30,17 +29,6 @@ export const defaultState = { cache: null, } -export const _moveItemInArray = (array, value, movement) => { - const oldIndex = array.indexOf(value) - const newIndex = oldIndex + movement - const newArray = [...array] - // remove old - newArray.splice(oldIndex, 1) - // add new - newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value) - return newArray -} - const _wrapData = (data, userName) => { return { ...data, @@ -292,7 +280,10 @@ export const useUserHighlightStore = defineStore('user_highlight', { ) } }) - storage.setItem('vuex-lz', { ...vuexState, config: { ...config, highlight } }) + storage.setItem('vuex-lz', { + ...vuexState, + config: { ...config, highlight }, + }) if (recent === null) { console.debug( @@ -341,12 +332,13 @@ export const useUserHighlightStore = defineStore('user_highlight', { const params = { pleroma_settings_store: { user_highlight: this.cache }, } - window.vuex.state.api.backendInteractor - .updateProfileJSON({ params }) - .then((user) => { - this.initUserHighlight(user) - this.dirty = false - }) + updateProfileJSON({ + params, + credentials: useOAuthStore().token, + }).then(({ data: user }) => { + this.initUserHighlight(user) + this.dirty = false + }) }, }, persist: { diff --git a/src/sw.js b/src/sw.js index 1e7abd3de..b7629b5f2 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,5 +1,6 @@ /* eslint-env serviceworker */ +// biome-ignore: side effect import of assets list import 'virtual:pleroma-fe/service_worker_env' import { createI18n } from 'vue-i18n' diff --git a/static/empty.css b/static/empty.css new file mode 100644 index 000000000..e69de29bb diff --git a/test/e2e-playwright/playwright.config.mjs b/test/e2e-playwright/playwright.config.mjs index 04747ee77..51a4de21e 100644 --- a/test/e2e-playwright/playwright.config.mjs +++ b/test/e2e-playwright/playwright.config.mjs @@ -1,7 +1,7 @@ /* global process */ import { defineConfig, devices } from 'playwright/test' -const baseURL = process.env.E2E_BASE_URL || 'http://localhost:8080' +const baseURL = process.env.E2E_BASE_URL || 'http://localhost:8099' export default defineConfig({ testDir: './specs', @@ -25,12 +25,13 @@ export default defineConfig({ video: 'retain-on-failure', }, webServer: { - command: 'yarn dev -- --host 0.0.0.0 --port 8080 --strictPort', + command: 'yarn dev -- --host 0.0.0.0 --port $PORT --strictPort', url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120_000, env: { ...process.env, + PORT: process.env.PORT || '8099', VITE_PROXY_TARGET: process.env.VITE_PROXY_TARGET || 'http://localhost:4000', VITE_PROXY_ORIGIN: diff --git a/test/e2e-playwright/specs/user_smoke.spec.js b/test/e2e-playwright/specs/user_smoke.spec.js index a71378c06..205cd3e69 100644 --- a/test/e2e-playwright/specs/user_smoke.spec.js +++ b/test/e2e-playwright/specs/user_smoke.spec.js @@ -34,14 +34,9 @@ const logout = async (page) => { name: 'Logout', exact: true, }) - if (await confirmLogout.isVisible()) { - await Promise.all([ - page.waitForURL(/\/main\/(public|all)/), - confirmLogout.click(), - ]) - } else { - await page.waitForURL(/\/main\/(public|all)/) - } + await expect(confirmLogout).toBeVisible() + await confirmLogout.click() + await page.waitForURL(/\/main\/(public|all)/) await expect(page.locator('#sidebar form.login-form')).toBeVisible() } diff --git a/test/e2e/specs/test.js b/test/e2e/specs/test.js index f8993989b..ba3e757fd 100644 --- a/test/e2e/specs/test.js +++ b/test/e2e/specs/test.js @@ -4,7 +4,7 @@ module.exports = { 'default e2e tests': function (browser) { // automatically uses dev Server port from /config.index.js - // default: http://localhost:8080 + // default: http://localhost:8099 // see nightwatch.conf.js const devServer = browser.globals.devServerURL diff --git a/test/fixtures/mock_api.js b/test/fixtures/mock_api.js index 03fb01a64..6fabe6356 100644 --- a/test/fixtures/mock_api.js +++ b/test/fixtures/mock_api.js @@ -1,73 +1,19 @@ -import { HttpResponse, http } from 'msw' -import { setupWorker } from 'msw/browser' import { test as testBase } from 'vitest' -// https://mswjs.io/docs/recipes/vitest-browser-mode -export const injectMswToTest = (defaultHandlers) => { - const worker = setupWorker(...defaultHandlers) +import { worker } from './worker.js' - return testBase.extend({ - worker: [ - // biome-ignore lint: required by vitest - async ({}, use) => { - await worker.start() +export const test = testBase.extend({ + worker: [ + // biome-ignore lint: required by vitest + async ({}, use) => { + await worker.start() - await use(worker) + await use(worker) - worker.resetHandlers() - worker.stop() - }, - { - auto: true, - }, - ], - }) -} - -export const testServer = 'https://test.server.example' - -export const authApis = [ - http.post(`${testServer}/api/v1/apps`, () => { - return HttpResponse.json({ - client_id: 'test-id', - client_secret: 'test-secret', - }) - }), - http.get(`${testServer}/api/v1/apps/verify_credentials`, ({ request }) => { - const authHeader = request.headers.get('Authorization') - if ( - authHeader === 'Bearer test-app-token' || - authHeader === 'Bearer also-good-app-token' - ) { - return HttpResponse.json({}) - } else { - // Pleroma 2.9.0 gives the following respoonse upon error - return HttpResponse.json( - { error: { detail: 'Internal server error' } }, - { - status: 400, - }, - ) - } - }), - http.post(`${testServer}/oauth/token`, async ({ request }) => { - const data = await request.formData() - - if ( - data.get('client_id') === 'test-id' && - data.get('client_secret') === 'test-secret' && - data.get('grant_type') === 'client_credentials' && - data.has('redirect_uri') - ) { - return HttpResponse.json({ access_token: 'test-app-token' }) - } else { - // Pleroma 2.9.0 gives the following respoonse upon error - return HttpResponse.json( - { error: 'Invalid credentials' }, - { - status: 400, - }, - ) - } - }), -] + worker.resetHandlers() + }, + { + auto: true, + }, + ], +}) diff --git a/test/fixtures/setup_test.js b/test/fixtures/setup_test.js index 02c49eb60..85a062cc7 100644 --- a/test/fixtures/setup_test.js +++ b/test/fixtures/setup_test.js @@ -1,7 +1,11 @@ +import { createTestingPinia } from '@pinia/testing' import { config } from '@vue/test-utils' import { createMemoryHistory, createRouter } from 'vue-router' 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' @@ -37,8 +41,13 @@ const getDefaultOpts = ({ (Vue) => { Vue.directive('body-scroll-lock', {}) }, + createTestingPinia(), ], - components: {}, + components: { + RichContent, + Status, + StillImage, + }, stubs: { I18nT: true, teleport: true, diff --git a/test/fixtures/statuses.json b/test/fixtures/statuses.json deleted file mode 100644 index 76faf9746..000000000 --- a/test/fixtures/statuses.json +++ /dev/null @@ -1,1640 +0,0 @@ -[ - { - "text": "@gameragodzilla @verius This is why you let the military run the theater of operations, not the cameras, PR handlers, and politicians.", - "truncated": false, - "created_at": "Sun Aug 21 16:56:30 +0000 2016", - "in_reply_to_status_id": "247026", - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255500:objectType=comment", - "source": "ostatus", - "id": 247029, - "in_reply_to_user_id": 23253, - "in_reply_to_screen_name": "gameragodzilla", - "geo": null, - "user": { - "id": 23258, - "name": "Maiyannah Bishop", - "screen_name": "maiyannah", - "location": "Canada", - "description": "Admin of the HA community \u2022 Editor-in-chief of Highland Arrow \u2022 Lead Developer of !postActiv \u2022 FSF Associate Member \u2022 Email: maiyannah@highlandarrow.com", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23258-48-20160819164757.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23258-48-20160819164757.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23258-original-20160819160607.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23258-original-20160819160607.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": "http:\/\/www.highlandarrow.com", - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 21:36:53 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 937, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/maiyannah", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1", - "favourites_count": 37 - }, - "statusnet_html": "@gameragodzilla<\/a> @verius<\/a> This is why you let the military run the theater of operations, not the cameras, PR handlers, and politicians.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255500", - "in_reply_to_profileurl": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "in_reply_to_ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73", - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23253, - "screen_name": "gameragodzilla", - "fullname": "Gamera Godzilla", - "profileurl": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "fl0wn favorited something by moonman: important: https:\/\/shitposter.club\/attachment\/182769", - "truncated": false, - "created_at": "Sun Aug 21 16:55:32 +0000 2016", - "in_reply_to_status_id": "246906", - "uri": "tag:sealion.club,2016-08-21:fave:238:note:957678:2016-08-21T12:55:32-04:00", - "source": "ostatus", - "id": 247028, - "in_reply_to_user_id": 23220, - "in_reply_to_screen_name": "moonman", - "geo": null, - "user": { - "id": 23205, - "name": "fl0wn", - "screen_name": "fl0wn", - "location": "Jack's House", - "description": "the all seeing all dancing crap of the world", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23205-48-20160730095757.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23205-48-20160730095757.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23205-original-20160730095636.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23205-original-20160730095636.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Tue Jul 26 23:04:47 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 1160, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/sealion.club\/fl0wn", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": true, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/sealion.club\/user\/238", - "favourites_count": 672 - }, - "statusnet_html": "fl0wn favorited something by moonman: important: https:\/\/shitposter.club\/attachment\/182769<\/a>", - "statusnet_conversation_id": 200703, - "statusnet_in_groups": false, - "external_url": "https:\/\/sealion.club\/notice\/957950", - "in_reply_to_profileurl": "https:\/\/shitposter.club\/moonman", - "in_reply_to_ostatus_uri": "https:\/\/shitposter.club\/user\/1", - "attentions": [], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": false, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "#furry #nsfw https:\/\/soykaf.com\/attachment\/64411", - "truncated": false, - "created_at": "Sun Aug 21 16:55:41 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:soykaf.com,2016-08-21:noticeId=339506:objectType=note", - "source": "ostatus", - "id": 247027, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "attachments": [ - { - "url": "https:\/\/soykaf.com\/file\/21105283b95c5db2c3ac38d06f0d34093a35dd6a965cec89c7950a786351d6a4.jpg", - "mimetype": "image\/jpeg", - "size": "614464", - "oembed": false, - "id": "103984", - "version": "1.2", - "thumb_url": "https:\/\/social.heldscal.la\/attachment\/103984\/thumbnail?w=438&h=600", - "large_thumb_url": "https:\/\/social.heldscal.la\/attachment\/103984\/thumbnail?w=438&h=600", - "width": "982", - "height": "1348" - } - ], - "user": { - "id": 24329, - "name": "Mouse", - "screen_name": "mouse", - "location": "Somewhere in the US", - "description": "Furry. Lover of animals and good food. Coffee keeps me sane.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/24329-48-20160821162822.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/24329-48-20160821162822.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/24329-original-20160821162818.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/24329-original-20160821162818.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Mon Aug 15 23:12:20 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 124, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/soykaf.com\/mouse", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": true, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/soykaf.com\/user\/2558", - "favourites_count": 11 - }, - "statusnet_html": "#furry<\/a><\/span> #nsfw<\/a><\/span> https:\/\/soykaf.com\/attachment\/64411<\/a>", - "statusnet_conversation_id": 200729, - "statusnet_in_groups": false, - "external_url": "https:\/\/soykaf.com\/notice\/339506", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@maiyannah @verius I mean I do sympathize on a moral level because Hussein is indeed a monster. We just didn't do the level of nation building we did post-WW2 with Germany and Japan to ensure they don't turn into chaotic hellholes as soon as the old regime was toppled. We half assed our way in and half assed our way out. And now ISIS has become a far bigger threat than Hussein ever was.", - "truncated": false, - "created_at": "Sun Aug 21 16:55:24 +0000 2016", - "in_reply_to_status_id": "247021", - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255499:objectType=comment", - "source": "ostatus", - "id": 247026, - "in_reply_to_user_id": 23258, - "in_reply_to_screen_name": "maiyannah", - "geo": null, - "user": { - "id": 23253, - "name": "Gamera Godzilla", - "screen_name": "gameragodzilla", - "location": null, - "description": "Hopefully I wont be banned by Staffan here.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 20:05:23 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 691, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73", - "favourites_count": 30 - }, - "statusnet_html": "@maiyannah<\/a> @verius<\/a> I mean I do sympathize on a moral level because Hussein is indeed a monster. We just didn't do the level of nation building we did post-WW2 with Germany and Japan to ensure they don't turn into chaotic hellholes as soon as the old regime was toppled. We half assed our way in and half assed our way out. And now ISIS has become a far bigger threat than Hussein ever was.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255499", - "in_reply_to_profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "in_reply_to_ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1", - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23258, - "screen_name": "maiyannah", - "fullname": "Maiyannah Bishop", - "profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "whew!", - "truncated": false, - "created_at": "Sun Aug 21 16:54:59 +0000 2016", - "in_reply_to_status_id": "247016", - "uri": "tag:soykaf.com,2016-08-21:noticeId=339504:objectType=note", - "source": "ostatus", - "id": 247025, - "in_reply_to_user_id": 23236, - "in_reply_to_screen_name": "jimrusell01", - "geo": null, - "user": { - "id": 24329, - "name": "Mouse", - "screen_name": "mouse", - "location": "Somewhere in the US", - "description": "Furry. Lover of animals and good food. Coffee keeps me sane.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/24329-48-20160821162822.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/24329-48-20160821162822.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/24329-original-20160821162818.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/24329-original-20160821162818.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Mon Aug 15 23:12:20 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 124, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/soykaf.com\/mouse", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": true, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/soykaf.com\/user\/2558", - "favourites_count": 11 - }, - "statusnet_html": "whew!", - "statusnet_conversation_id": 200726, - "statusnet_in_groups": false, - "external_url": "https:\/\/soykaf.com\/notice\/339504", - "in_reply_to_profileurl": "https:\/\/gs.smuglo.li\/jimrusell01", - "in_reply_to_ostatus_uri": "https:\/\/gs.smuglo.li\/user\/1987", - "attentions": [ - { - "id": 23236, - "screen_name": "jimrusell01", - "fullname": "Jim Rusell", - "profileurl": "https:\/\/gs.smuglo.li\/jimrusell01", - "ostatus_uri": "https:\/\/gs.smuglo.li\/user\/1987" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@verius @gameragodzilla Sure, we'll just let them eat up other countries. \u00a0How'd that work out for Britain prior to WW2, remind me?", - "truncated": false, - "created_at": "Sun Aug 21 16:54:12 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255498:objectType=comment", - "source": "ostatus", - "id": 247024, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "user": { - "id": 23258, - "name": "Maiyannah Bishop", - "screen_name": "maiyannah", - "location": "Canada", - "description": "Admin of the HA community \u2022 Editor-in-chief of Highland Arrow \u2022 Lead Developer of !postActiv \u2022 FSF Associate Member \u2022 Email: maiyannah@highlandarrow.com", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23258-48-20160819164757.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23258-48-20160819164757.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23258-original-20160819160607.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23258-original-20160819160607.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": "http:\/\/www.highlandarrow.com", - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 21:36:53 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 937, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/maiyannah", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1", - "favourites_count": 37 - }, - "statusnet_html": "@verius<\/a> @gameragodzilla<\/a> Sure, we'll just let them eat up other countries. \u00a0How'd that work out for Britain prior to WW2, remind me?", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255498", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23253, - "screen_name": "gameragodzilla", - "fullname": "Gamera Godzilla", - "profileurl": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@verius @maiyannah The Middle East supplies more of Europe's oil than the US's, actually. Most of our oil comes from our hat, Canada. So if anything, you guys should be more concerned about the Middle East than us.", - "truncated": false, - "created_at": "Sun Aug 21 16:54:01 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255497:objectType=comment", - "source": "ostatus", - "id": 247023, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "user": { - "id": 23253, - "name": "Gamera Godzilla", - "screen_name": "gameragodzilla", - "location": null, - "description": "Hopefully I wont be banned by Staffan here.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 20:05:23 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 691, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73", - "favourites_count": 30 - }, - "statusnet_html": "@verius<\/a> @maiyannah<\/a> The Middle East supplies more of Europe's oil than the US's, actually. Most of our oil comes from our hat, Canada. So if anything, you guys should be more concerned about the Middle East than us.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255497", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23258, - "screen_name": "maiyannah", - "fullname": "Maiyannah Bishop", - "profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "mouse favorited something by jimrusell01: #nsfw https:\/\/gs.smuglo.li\/attachment\/111717", - "truncated": false, - "created_at": "Sun Aug 21 16:54:04 +0000 2016", - "in_reply_to_status_id": "247016", - "uri": "tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00", - "source": "ostatus", - "id": 247022, - "in_reply_to_user_id": 23236, - "in_reply_to_screen_name": "jimrusell01", - "geo": null, - "user": { - "id": 24329, - "name": "Mouse", - "screen_name": "mouse", - "location": "Somewhere in the US", - "description": "Furry. Lover of animals and good food. Coffee keeps me sane.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/24329-48-20160821162822.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/24329-48-20160821162822.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/24329-original-20160821162818.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/24329-original-20160821162818.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Mon Aug 15 23:12:20 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 124, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/soykaf.com\/mouse", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": true, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/soykaf.com\/user\/2558", - "favourites_count": 11 - }, - "statusnet_html": "mouse favorited something by jimrusell01: #nsfw<\/a><\/span> https:\/\/gs.smuglo.li\/attachment\/111717<\/a>", - "statusnet_conversation_id": 200726, - "statusnet_in_groups": false, - "external_url": "https:\/\/soykaf.com\/notice\/339502", - "in_reply_to_profileurl": "https:\/\/gs.smuglo.li\/jimrusell01", - "in_reply_to_ostatus_uri": "https:\/\/gs.smuglo.li\/user\/1987", - "attentions": [], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": false, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@gameragodzilla @verius I think I tend to be more charitible to George Bush than most are, because the problems he had were inherited from elsewhere. \u00a0Was he really up to them? \u00a0Not really, but it was Clinton that authorized the CIA missions that messed up that region.", - "truncated": false, - "created_at": "Sun Aug 21 16:53:29 +0000 2016", - "in_reply_to_status_id": "247019", - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255495:objectType=comment", - "source": "ostatus", - "id": 247021, - "in_reply_to_user_id": 23253, - "in_reply_to_screen_name": "gameragodzilla", - "geo": null, - "user": { - "id": 23258, - "name": "Maiyannah Bishop", - "screen_name": "maiyannah", - "location": "Canada", - "description": "Admin of the HA community \u2022 Editor-in-chief of Highland Arrow \u2022 Lead Developer of !postActiv \u2022 FSF Associate Member \u2022 Email: maiyannah@highlandarrow.com", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23258-48-20160819164757.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23258-48-20160819164757.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23258-original-20160819160607.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23258-original-20160819160607.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": "http:\/\/www.highlandarrow.com", - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 21:36:53 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 937, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/maiyannah", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1", - "favourites_count": 37 - }, - "statusnet_html": "@gameragodzilla<\/a> @verius<\/a> I think I tend to be more charitible to George Bush than most are, because the problems he had were inherited from elsewhere. \u00a0Was he really up to them? \u00a0Not really, but it was Clinton that authorized the CIA missions that messed up that region.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255495", - "in_reply_to_profileurl": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "in_reply_to_ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73", - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23253, - "screen_name": "gameragodzilla", - "fullname": "Gamera Godzilla", - "profileurl": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "\u50cd\u304d\u65b9\u3068\u516c\u5171\u6027\u304c\u4eca\u5f8c\u5909\u308f\u308b\u793e\u4f1a\u306b\u306a\u308b\u3060\u308d\u3046\u304b\u3002 http:\/\/togetter.com\/li\/1013965", - "truncated": false, - "created_at": "Sun Aug 21 16:51:38 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:status.akionux.net,2016-08-21:noticeId=359563:objectType=note", - "source": "ostatus", - "id": 247020, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "attachments": [ - { - "url": "http:\/\/togetter.com\/li\/1013965", - "mimetype": "text\/html; charset=UTF-8", - "size": null, - "oembed": { - "type": "article", - "provider": "Togetter", - "provider_url": null, - "oembedHTML": "\u65b0\u3057\u3044\u50cd\u304d\u65b9\u3002", - "title": "\u300c\u597d\u304d\u306a\u65e5\u306b\u9023\u7d61\u306a\u3057\u3067\u51fa\u52e4\u30fb\u6b20\u52e4\u3067\u304d\u308b\u300d\u30b7\u30b9\u30c6\u30e0\u3092\u78ba\u7acb\u3057\u305f\u5de5\u5834\u9577\u306e\u8a71\u306b\u3055\u307e\u3056\u307e\u306a\u58f0\u304c\u96c6\u307e\u308b", - "author_name": "", - "author_url": null - }, - "id": "103983", - "version": "1.2", - "thumb_url": "https:\/\/social.heldscal.la\/file\/thumb-103983-450x237-f4bfa5cd6a67ab53d1bf602234f666003acea4b0615d436b61e9e36dff0eece0.jpg.jpg", - "large_thumb_url": "https:\/\/social.heldscal.la\/file\/thumb-103983-1000x525-f4bfa5cd6a67ab53d1bf602234f666003acea4b0615d436b61e9e36dff0eece0.jpg.jpg", - "width": null, - "height": null - } - ], - "user": { - "id": 24161, - "name": "\u041c\u0430\u0440\u0443\u043e\u043a\u0430 \u0425\u0438\u0440\u043e\u043a\u0430\u0434\u0437\u0443", - "screen_name": "kamiyajing", - "location": "Tokyo", - "description": "Reserach : Complex Chemical Physics. Interest : mathematics, linguistics, Phenomenology, Vedas. Impressed by free software movement and would like to learn about PC, especially GNU\/LINUX while I'm super-beginner!", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/24161-48-20160812083104.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/24161-48-20160812083104.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/24161-original-20160812082954.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/24161-original-20160812082954.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Fri Aug 12 08:29:52 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 96, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/status.akionux.net\/kamiyajing", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/status.akionux.net\/user\/2695", - "favourites_count": 3 - }, - "statusnet_html": "\u50cd\u304d\u65b9\u3068\u516c\u5171\u6027\u304c\u4eca\u5f8c\u5909\u308f\u308b\u793e\u4f1a\u306b\u306a\u308b\u3060\u308d\u3046\u304b\u3002
\n
\n
http:\/\/togetter.com\/li\/1013965<\/a>", - "statusnet_conversation_id": 200728, - "statusnet_in_groups": false, - "external_url": "https:\/\/status.akionux.net\/notice\/359563", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@maiyannah @verius Bush's issue was more mission creep and failing to think long term. He just thought about toppeling Hussein, which was indeed easily accomplished within the span of a few months. It was the guerilla fighting afterwards that caused major problems. Clinton was just being an idiot and failed to apprehend Bin Laden prior to 9\/11.", - "truncated": false, - "created_at": "Sun Aug 21 16:52:31 +0000 2016", - "in_reply_to_status_id": "247013", - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255493:objectType=comment", - "source": "ostatus", - "id": 247019, - "in_reply_to_user_id": 23258, - "in_reply_to_screen_name": "maiyannah", - "geo": null, - "user": { - "id": 23253, - "name": "Gamera Godzilla", - "screen_name": "gameragodzilla", - "location": null, - "description": "Hopefully I wont be banned by Staffan here.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 20:05:23 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 691, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73", - "favourites_count": 30 - }, - "statusnet_html": "@maiyannah<\/a> @verius<\/a> Bush's issue was more mission creep and failing to think long term. He just thought about toppeling Hussein, which was indeed easily accomplished within the span of a few months. It was the guerilla fighting afterwards that caused major problems.
\n
\nClinton was just being an idiot and failed to apprehend Bin Laden prior to 9\/11.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255493", - "in_reply_to_profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "in_reply_to_ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1", - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23258, - "screen_name": "maiyannah", - "fullname": "Maiyannah Bishop", - "profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "How flat is flat enough? https:\/\/gs.smuglo.li\/attachment\/111716", - "truncated": false, - "created_at": "Sun Aug 21 16:52:11 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:gs.smuglo.li,2016-08-21:noticeId=589590:objectType=note", - "source": "ostatus", - "id": 247018, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "attachments": [ - { - "url": "https:\/\/gs.smuglo.li\/file\/c1835c05ee3a2f7f2dcf9bdbb0100330842b46819dc78f2a390c790ad8a203fc.jpg", - "mimetype": "image\/jpeg", - "size": "3808766", - "oembed": false, - "id": "103982", - "version": "1.2", - "thumb_url": "https:\/\/social.heldscal.la\/file\/thumb-103982-450x321-c1835c05ee3a2f7f2dcf9bdbb0100330842b46819dc78f2a390c790ad8a203fc.jpg", - "large_thumb_url": "https:\/\/social.heldscal.la\/file\/thumb-103982-1000x712-c1835c05ee3a2f7f2dcf9bdbb0100330842b46819dc78f2a390c790ad8a203fc.jpg", - "width": "5031", - "height": "3579" - } - ], - "user": { - "id": 23266, - "name": "\u3059\u307f", - "screen_name": "sumi", - "location": "The Bunker", - "description": null, - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23266-48-20160814155800.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23266-48-20160814155800.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23266-original-20160814155757.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23266-original-20160814155757.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 23:10:58 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 74, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/gs.smuglo.li\/sumi", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/gs.smuglo.li\/user\/580", - "favourites_count": 1 - }, - "statusnet_html": "How flat is flat enough?
https:\/\/gs.smuglo.li\/attachment\/111716<\/a>", - "statusnet_conversation_id": 200727, - "statusnet_in_groups": false, - "external_url": "https:\/\/gs.smuglo.li\/notice\/589590", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "1iceloops123 favorited something by jimrusell01: #nsfw https:\/\/gs.smuglo.li\/attachment\/111717", - "truncated": false, - "created_at": "Sun Aug 21 16:52:15 +0000 2016", - "in_reply_to_status_id": "247016", - "uri": "tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00", - "source": "ostatus", - "id": 247017, - "in_reply_to_user_id": 23236, - "in_reply_to_screen_name": "jimrusell01", - "geo": null, - "user": { - "id": 23212, - "name": "iceloops123", - "screen_name": "1iceloops123", - "location": "backup account", - "description": "loves videogames,art,music,comics,cartoons nothing new. sealion.club\/iceloops123 freezepeach.xyz\/iceloops123 gs.smuglo.li\/iceloops123", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23212-48-20160727103811.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23212-48-20160727103811.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23212-original-20160727103801.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23212-original-20160727103801.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": "http:\/\/iceloops123.tumblr.com\/", - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 10:38:01 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 2092, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/shitposter.club\/1iceloops123", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": true, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/shitposter.club\/user\/3895", - "favourites_count": 643 - }, - "statusnet_html": "1iceloops123 favorited something by jimrusell01: #nsfw<\/a><\/span> https:\/\/gs.smuglo.li\/attachment\/111717<\/a>", - "statusnet_conversation_id": 200726, - "statusnet_in_groups": false, - "external_url": "https:\/\/shitposter.club\/notice\/773502", - "in_reply_to_profileurl": "https:\/\/gs.smuglo.li\/jimrusell01", - "in_reply_to_ostatus_uri": "https:\/\/gs.smuglo.li\/user\/1987", - "attentions": [], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": false, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "#nsfw https:\/\/gs.smuglo.li\/attachment\/111717", - "truncated": false, - "created_at": "Sun Aug 21 16:51:44 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:gs.smuglo.li,2016-08-21:noticeId=589587:objectType=note", - "source": "ostatus", - "id": 247016, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "attachments": [ - { - "url": "https:\/\/gs.smuglo.li\/file\/13688381b409df69aa11f0469fe19fcc932734d70ddbf8f39249364e205a08f2.jpg", - "mimetype": "image\/jpeg", - "size": "155985", - "oembed": false, - "id": "103981", - "version": "1.2", - "thumb_url": "https:\/\/social.heldscal.la\/file\/thumb-103981-428x600-13688381b409df69aa11f0469fe19fcc932734d70ddbf8f39249364e205a08f2.jpg", - "large_thumb_url": "https:\/\/social.heldscal.la\/file\/thumb-103981-712x1000-13688381b409df69aa11f0469fe19fcc932734d70ddbf8f39249364e205a08f2.jpg", - "width": "712", - "height": "1000" - } - ], - "user": { - "id": 23236, - "name": "Jim Rusell", - "screen_name": "jimrusell01", - "location": "The Internet", - "description": "I once was a normal human, but i'm now just a simple bot.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23236-48-20160727155842.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23236-48-20160727155842.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23236-original-20160727155834.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23236-original-20160727155834.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 15:58:34 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 919, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/gs.smuglo.li\/jimrusell01", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": true, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/gs.smuglo.li\/user\/1987", - "favourites_count": 33 - }, - "statusnet_html": "#nsfw<\/a><\/span> https:\/\/gs.smuglo.li\/attachment\/111717<\/a>", - "statusnet_conversation_id": 200726, - "statusnet_in_groups": false, - "external_url": "https:\/\/gs.smuglo.li\/notice\/589587", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [], - "fave_num": 2, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "https:\/\/www.youtube.com\/watch?v=cV9BtuPpW9w&feature=share", - "truncated": false, - "created_at": "Sun Aug 21 16:51:49 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:shitposter.club,2016-08-21:noticeId=773500:objectType=note", - "source": "ostatus", - "id": 247015, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "attachments": [ - { - "url": "https:\/\/www.youtube.com\/watch?v=cV9BtuPpW9w&feature=share", - "mimetype": "text\/html; charset=utf-8", - "size": null, - "oembed": { - "type": "video", - "provider": "YouTube", - "provider_url": "https:\/\/www.youtube.com\/", - "oembedHTML": "", - "title": "PASSPORT.MID played on a Roland Sound Canvas SC-55", - "author_name": "rerolledDK", - "author_url": "https:\/\/www.youtube.com\/user\/rerolledDK" - }, - "id": "103980", - "version": "1.2", - "thumb_url": "https:\/\/social.heldscal.la\/file\/thumb-103980-450x338-95067907509a4f756e4317c47e8dc38738148e9b915f673c34bbef8de47d283e.jpg.jpg", - "large_thumb_url": "https:\/\/social.heldscal.la\/file\/95067907509a4f756e4317c47e8dc38738148e9b915f673c34bbef8de47d283e.jpg", - "width": null, - "height": null - } - ], - "user": { - "id": 23212, - "name": "iceloops123", - "screen_name": "1iceloops123", - "location": "backup account", - "description": "loves videogames,art,music,comics,cartoons nothing new. sealion.club\/iceloops123 freezepeach.xyz\/iceloops123 gs.smuglo.li\/iceloops123", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23212-48-20160727103811.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23212-48-20160727103811.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23212-original-20160727103801.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23212-original-20160727103801.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": "http:\/\/iceloops123.tumblr.com\/", - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 10:38:01 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 2092, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/shitposter.club\/1iceloops123", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": true, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/shitposter.club\/user\/3895", - "favourites_count": 643 - }, - "statusnet_html": "https:\/\/www.youtube.com\/watch?v=cV9BtuPpW9w&feature=share<\/a>", - "statusnet_conversation_id": 200725, - "statusnet_in_groups": false, - "external_url": "https:\/\/shitposter.club\/notice\/773500", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@verius @maiyannah Good thing you don't vote. :P Of course, it's also incredibly naive to think that corruption and shady things in one country won't impact another country. The bullshit surrounding Brussels is why the UK decided to leave the EU after all.", - "truncated": false, - "created_at": "Sun Aug 21 16:51:16 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255491:objectType=comment", - "source": "ostatus", - "id": 247014, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "user": { - "id": 23253, - "name": "Gamera Godzilla", - "screen_name": "gameragodzilla", - "location": null, - "description": "Hopefully I wont be banned by Staffan here.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 20:05:23 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 691, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73", - "favourites_count": 30 - }, - "statusnet_html": "@verius<\/a> @maiyannah<\/a> Good thing you don't vote. :P
\n
\nOf course, it's also incredibly naive to think that corruption and shady things in one country won't impact another country. The bullshit surrounding Brussels is why the UK decided to leave the EU after all.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255491", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23258, - "screen_name": "maiyannah", - "fullname": "Maiyannah Bishop", - "profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@verius @gameragodzilla Yeah it's not like her husband was the one that fucked up the middle east situation or anything. \u00a0George Bush just wishes he had as much a hand in that as Bill Clinton did.", - "truncated": false, - "created_at": "Sun Aug 21 16:50:53 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255490:objectType=comment", - "source": "ostatus", - "id": 247013, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "user": { - "id": 23258, - "name": "Maiyannah Bishop", - "screen_name": "maiyannah", - "location": "Canada", - "description": "Admin of the HA community \u2022 Editor-in-chief of Highland Arrow \u2022 Lead Developer of !postActiv \u2022 FSF Associate Member \u2022 Email: maiyannah@highlandarrow.com", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23258-48-20160819164757.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23258-48-20160819164757.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23258-original-20160819160607.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23258-original-20160819160607.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": "http:\/\/www.highlandarrow.com", - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 21:36:53 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 937, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/maiyannah", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1", - "favourites_count": 37 - }, - "statusnet_html": "@
verius<\/a> @gameragodzilla<\/a> Yeah it's not like her husband was the one that fucked up the middle east situation or anything. \u00a0George Bush just wishes he had as much a hand in that as Bill Clinton did.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255490", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23253, - "screen_name": "gameragodzilla", - "fullname": "Gamera Godzilla", - "profileurl": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@maiyannah @verius Trump's issue is his tendency to blurt out whatever the fuck he has in mind at any given time. Sure, that may be a quality that people admire about him, and even I have a sneaky sense of humor at the fact that he just doesn't give a shit, but that does mean the media tend to try and focus on the stupid shit he says while desperately burying the myriad of shady things Clinton has done.", - "truncated": false, - "created_at": "Sun Aug 21 16:48:50 +0000 2016", - "in_reply_to_status_id": "247009", - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255488:objectType=comment", - "source": "ostatus", - "id": 247012, - "in_reply_to_user_id": 23258, - "in_reply_to_screen_name": "maiyannah", - "geo": null, - "user": { - "id": 23253, - "name": "Gamera Godzilla", - "screen_name": "gameragodzilla", - "location": null, - "description": "Hopefully I wont be banned by Staffan here.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 20:05:23 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 691, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73", - "favourites_count": 30 - }, - "statusnet_html": "@maiyannah<\/a> @verius<\/a> Trump's issue is his tendency to blurt out whatever the fuck he has in mind at any given time. Sure, that may be a quality that people admire about him, and even I have a sneaky sense of humor at the fact that he just doesn't give a shit, but that does mean the media tend to try and focus on the stupid shit he says while desperately burying the myriad of shady things Clinton has done.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255488", - "in_reply_to_profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "in_reply_to_ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1", - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23258, - "screen_name": "maiyannah", - "fullname": "Maiyannah Bishop", - "profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "#furry #nsfw https:\/\/soykaf.com\/attachment\/64406", - "truncated": false, - "created_at": "Sun Aug 21 16:47:50 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:soykaf.com,2016-08-21:noticeId=339493:objectType=note", - "source": "ostatus", - "id": 247011, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "attachments": [ - { - "url": "https:\/\/soykaf.com\/file\/135f7f53bfcfe1730b68d1608db4e303355fd7c25acd2402197bc33da552d227.jpg", - "mimetype": "image\/jpeg", - "size": "191078", - "oembed": false, - "id": "103979", - "version": "1.2", - "thumb_url": "https:\/\/social.heldscal.la\/attachment\/103979\/thumbnail?w=417&h=600", - "large_thumb_url": "https:\/\/social.heldscal.la\/attachment\/103979\/thumbnail?w=417&h=600", - "width": "710", - "height": "1024" - } - ], - "user": { - "id": 24329, - "name": "Mouse", - "screen_name": "mouse", - "location": "Somewhere in the US", - "description": "Furry. Lover of animals and good food. Coffee keeps me sane.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/24329-48-20160821162822.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/24329-48-20160821162822.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/24329-original-20160821162818.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/24329-original-20160821162818.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Mon Aug 15 23:12:20 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 124, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/soykaf.com\/mouse", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": true, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/soykaf.com\/user\/2558", - "favourites_count": 11 - }, - "statusnet_html": "#furry<\/a><\/span> #nsfw<\/a><\/span> https:\/\/soykaf.com\/attachment\/64406<\/a>", - "statusnet_conversation_id": 200724, - "statusnet_in_groups": false, - "external_url": "https:\/\/soykaf.com\/notice\/339493", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - }, - { - "text": "@verius @maiyannah Considering the almost cult-like devotion certain people have for him, I doubt it. Oh well, my main concern right now is still keeping Hillary out of office. I'll still take a crazy, unreliable narcissist over Hillary's corruption.", - "truncated": false, - "created_at": "Sun Aug 21 16:47:26 +0000 2016", - "in_reply_to_status_id": null, - "uri": "tag:community.highlandarrow.com,2016-08-21:noticeId=255486:objectType=comment", - "source": "ostatus", - "id": 247010, - "in_reply_to_user_id": null, - "in_reply_to_screen_name": null, - "geo": null, - "user": { - "id": 23253, - "name": "Gamera Godzilla", - "screen_name": "gameragodzilla", - "location": null, - "description": "Hopefully I wont be banned by Staffan here.", - "profile_image_url": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_https": "https:\/\/social.heldscal.la\/avatar\/23253-48-20160802061558.jpeg", - "profile_image_url_profile_size": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "profile_image_url_original": "https:\/\/social.heldscal.la\/avatar\/23253-original-20160727200523.jpeg", - "groups_count": 0, - "linkcolor": false, - "backgroundcolor": false, - "url": null, - "protected": false, - "followers_count": 1, - "friends_count": 0, - "created_at": "Wed Jul 27 20:05:23 +0000 2016", - "utc_offset": "0", - "time_zone": "UTC", - "statuses_count": 691, - "following": true, - "statusnet_blocking": false, - "notifications": true, - "statusnet_profile_url": "https:\/\/community.highlandarrow.com\/gameragodzilla", - "cover_photo": false, - "background_image": false, - "profile_link_color": false, - "profile_background_color": false, - "profile_banner_url": false, - "follows_you": false, - "blocks_you": false, - "is_local": false, - "is_silenced": false, - "rights": { - "delete_user": false, - "delete_others_notice": false, - "silence": false, - "sandbox": false - }, - "is_sandboxed": false, - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/73", - "favourites_count": 30 - }, - "statusnet_html": "@verius<\/a> @maiyannah<\/a> Considering the almost cult-like devotion certain people have for him, I doubt it.
\n
\nOh well, my main concern right now is still keeping Hillary out of office. I'll still take a crazy, unreliable narcissist over Hillary's corruption.", - "statusnet_conversation_id": 200682, - "statusnet_in_groups": false, - "external_url": "https:\/\/community.highlandarrow.com\/notice\/255486", - "in_reply_to_profileurl": null, - "in_reply_to_ostatus_uri": null, - "attentions": [ - { - "id": 23250, - "screen_name": "verius", - "fullname": "Verius", - "profileurl": "https:\/\/community.highlandarrow.com\/verius", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/500" - }, - { - "id": 23258, - "screen_name": "maiyannah", - "fullname": "Maiyannah Bishop", - "profileurl": "https:\/\/community.highlandarrow.com\/maiyannah", - "ostatus_uri": "https:\/\/community.highlandarrow.com\/user\/1" - } - ], - "fave_num": 0, - "repeat_num": 0, - "is_post_verb": true, - "is_local": false, - "favorited": false, - "repeated": false - } -] diff --git a/test/fixtures/worker.js b/test/fixtures/worker.js new file mode 100644 index 000000000..e6ed89dc9 --- /dev/null +++ b/test/fixtures/worker.js @@ -0,0 +1,5 @@ +import { setupWorker } from 'msw/browser' + +export const worker = setupWorker() + +window.__test__ = window.__test__ || 'TEST' diff --git a/test/unit/specs/boot/routes.spec.js b/test/unit/specs/boot/routes.spec.js index f4be28a65..6df0ab80d 100644 --- a/test/unit/specs/boot/routes.spec.js +++ b/test/unit/specs/boot/routes.spec.js @@ -37,12 +37,9 @@ describe('routes', () => { const matchedComponents = router.currentRoute.value.matched - expect( - Object.hasOwn( - matchedComponents[0].components.default.components, - 'UserCard', - ), - ).to.eql(true) + expect(matchedComponents[0].components.default.name).to.eql( + 'AsyncComponentWrapper', + ) }) it("user's profile at /users", async () => { @@ -50,12 +47,9 @@ describe('routes', () => { const matchedComponents = router.currentRoute.value.matched - expect( - Object.hasOwn( - matchedComponents[0].components.default.components, - 'UserCard', - ), - ).to.eql(true) + expect(matchedComponents[0].components.default.name).to.eql( + 'AsyncComponentWrapper', + ) }) it('list view', async () => { @@ -63,12 +57,9 @@ describe('routes', () => { const matchedComponents = router.currentRoute.value.matched - expect( - Object.hasOwn( - matchedComponents[0].components.default.components, - 'ListsCard', - ), - ).to.eql(true) + expect(matchedComponents[0].components.default.name).to.eql( + 'AsyncComponentWrapper', + ) }) it('list timeline', async () => { @@ -76,12 +67,9 @@ describe('routes', () => { const matchedComponents = router.currentRoute.value.matched - expect( - Object.hasOwn( - matchedComponents[0].components.default.components, - 'Timeline', - ), - ).to.eql(true) + expect(matchedComponents[0].components.default.name).to.eql( + 'AsyncComponentWrapper', + ) }) it('list edit', async () => { @@ -89,11 +77,8 @@ describe('routes', () => { const matchedComponents = router.currentRoute.value.matched - expect( - Object.hasOwn( - matchedComponents[0].components.default.components, - 'BasicUserCard', - ), - ).to.eql(true) + expect(matchedComponents[0].components.default.name).to.eql( + 'AsyncComponentWrapper', + ) }) }) diff --git a/test/unit/specs/components/draft.spec.js b/test/unit/specs/components/draft.spec.js index 6abf2d75b..b69d2323e 100644 --- a/test/unit/specs/components/draft.spec.js +++ b/test/unit/specs/components/draft.spec.js @@ -100,7 +100,7 @@ describe('Draft saving', () => { await textarea.setValue('mew mew') wrapper.vm.requestClose() expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - await waitForEvent(wrapper, 'can-close') + await waitForEvent(wrapper, 'close-accepted') }) it('should save when close if auto-save is off, and unsavedPostAction is save', async () => { @@ -122,7 +122,7 @@ describe('Draft saving', () => { await textarea.setValue('mew mew') wrapper.vm.requestClose() expect(wrapper.vm.$store.getters.draftCount).to.equal(1) - await waitForEvent(wrapper, 'can-close') + await waitForEvent(wrapper, 'close-accepted') }) it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => { @@ -143,7 +143,7 @@ describe('Draft saving', () => { const textarea = wrapper.get('textarea') await textarea.setValue('mew mew') wrapper.vm.requestClose() - await waitForEvent(wrapper, 'can-close') + await waitForEvent(wrapper, 'close-accepted') expect(wrapper.vm.$store.getters.draftCount).to.equal(0) }) @@ -166,15 +166,20 @@ describe('Draft saving', () => { await textarea.setValue('mew mew') wrapper.vm.requestClose() await nextTick() - const saveButton = wrapper.findByText( - 'button', - $t('post_status.close_confirm_save_button'), - ) + 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, 'can-close') + await waitForEvent(wrapper, 'close-accepted') }) }) diff --git a/test/unit/specs/components/gallery.spec.js b/test/unit/specs/components/gallery.spec.js index 367bab0b4..c80d902f0 100644 --- a/test/unit/specs/components/gallery.spec.js +++ b/test/unit/specs/components/gallery.spec.js @@ -129,7 +129,7 @@ describe('Gallery', () => { ]) }) - it('mixed attachments', () => { + it('mixed attachments 1', () => { local = { attachments: [ { type: 'audio' }, @@ -138,7 +138,6 @@ describe('Gallery', () => { { type: 'image' }, { type: 'image' }, { type: 'image' }, - { type: 'image' }, ], } @@ -147,21 +146,17 @@ describe('Gallery', () => { { items: [{ type: 'image' }] }, { audio: true, items: [{ type: 'audio' }] }, { - items: [ - { type: 'image' }, - { type: 'image' }, - { type: 'image' }, - { type: 'image' }, - ], + items: [{ type: 'image' }, { type: 'image' }, { type: 'image' }], }, ]) + }) + it('mixed attachments 2', () => { local = { attachments: [ { type: 'image' }, { type: 'image' }, { type: 'image' }, - { type: 'image' }, { type: 'audio' }, { type: 'image' }, { type: 'audio' }, @@ -172,12 +167,13 @@ describe('Gallery', () => { { items: [{ type: 'image' }, { type: 'image' }, { type: 'image' }], }, - { items: [{ type: 'image' }] }, { audio: true, items: [{ type: 'audio' }] }, { items: [{ type: 'image' }] }, { audio: true, items: [{ type: 'audio' }] }, ]) + }) + it('7 images', () => { local = { attachments: [ { type: 'image' }, @@ -205,7 +201,9 @@ describe('Gallery', () => { ], }, ]) + }) + it('8 images', () => { local = { attachments: [ { type: 'image' }, @@ -230,6 +228,42 @@ describe('Gallery', () => { ]) }) + it('4 images + audio + image + 4 images', () => { + local = { + attachments: [ + { type: 'image' }, + { type: 'image' }, + { type: 'image' }, + { type: 'image' }, + { type: 'audio' }, + { type: 'image' }, + { type: 'audio' }, + { type: 'image' }, + { type: 'image' }, + { type: 'image' }, + { type: 'image' }, + ], + } + + expect(Gallery.computed.rows.call(local)).to.eql([ + { + items: [{ type: 'image' }, { type: 'image' }], + }, + { + items: [{ type: 'image' }, { type: 'image' }], + }, + { audio: true, items: [{ type: 'audio' }] }, + { items: [{ type: 'image' }] }, + { audio: true, items: [{ type: 'audio' }] }, + { + items: [{ type: 'image' }, { type: 'image' }], + }, + { + items: [{ type: 'image' }, { type: 'image' }], + }, + ]) + }) + it('does not do grouping when grid is set', () => { const attachments = [ { type: 'audio' }, diff --git a/test/unit/specs/components/post_status_form.spec.js b/test/unit/specs/components/post_status_form.spec.js new file mode 100644 index 000000000..8f3d6d80f --- /dev/null +++ b/test/unit/specs/components/post_status_form.spec.js @@ -0,0 +1,61 @@ +import { createTestingPinia } from '@pinia/testing' +import { mount } from '@vue/test-utils' +import { setActivePinia } from 'pinia' + +import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' +import { mountOpts } from '../../../fixtures/setup_test' + +import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' + +const currentUser = { + id: 'current-user', + default_scope: 'public', + locked: false, +} + +const repliedUser = { + id: 'replied-user', + screen_name: 'replied', +} + +const repliedStatus = { + id: 'status-1', + visibility: 'public', + user: repliedUser, +} + +const replyMountOpts = () => + mountOpts({ + props: { + replyTo: repliedStatus.id, + repliedUser, + attentions: [], + copyMessageScope: repliedStatus.visibility, + disableDraft: true, + }, + afterStore(store) { + store.state.users.currentUser = currentUser + store.state.statuses.allStatusesObject = { + [repliedStatus.id]: repliedStatus, + } + }, + }) + +describe('PostStatusForm', () => { + beforeEach(() => { + setActivePinia(createTestingPinia()) + }) + + it('initializes a reply form when quoteReply is unset', () => { + useInstanceCapabilitiesStore().quotingAvailable = true + + const wrapper = mount(PostStatusForm, replyMountOpts()) + + expect(wrapper.vm.newStatus.type).to.equal('reply') + expect(wrapper.vm.newStatus.quote).to.eql({ + id: '', + url: '', + thread: false, + }) + }) +}) diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js index 8b198420b..36d323575 100644 --- a/test/unit/specs/components/user_profile.spec.js +++ b/test/unit/specs/components/user_profile.spec.js @@ -4,7 +4,6 @@ import { createStore } from 'vuex' import UserProfile from 'src/components/user_profile/user_profile.vue' import { getters } from 'src/modules/users.js' -import backendInteractorService from 'src/services/backend_interactor_service/backend_interactor_service.js' const mutations = { clearTimeline: () => { @@ -53,10 +52,6 @@ const externalProfileStore = createStore({ actions, getters: testGetters, state: { - api: { - fetchers: {}, - backendInteractor: backendInteractorService(''), - }, interface: { browserSupport: '', }, @@ -116,10 +111,6 @@ const localProfileStore = createStore({ actions, getters: testGetters, state: { - api: { - fetchers: {}, - backendInteractor: backendInteractorService(''), - }, interface: { browserSupport: '', }, diff --git a/test/unit/specs/services/api/helpers.spec.js b/test/unit/specs/services/api/helpers.spec.js new file mode 100644 index 000000000..11874790a --- /dev/null +++ b/test/unit/specs/services/api/helpers.spec.js @@ -0,0 +1,90 @@ +import { paramsString } from 'src/api/helpers.js' + +describe('API Helpers', () => { + describe('paramsString', () => { + it('should return empty string when given empty object', () => { + const string = paramsString({}) + + expect(string).to.eq('') + }) + + it('should return empty string when given null', () => { + const string = paramsString(null) + + expect(string).to.eq('') + }) + + it('should return empty string when given undefined', () => { + const string = paramsString(undefined) + + expect(string).to.eq('') + }) + + it('should return URI param string for normal object', () => { + const string = paramsString({ a: 1, b: '3' }) + + expect(string).to.eq('?a=1&b=3') + }) + + it('should encode objects correctly', () => { + const string = paramsString({ foo: true, bar: [1, 2, 3] }) + + expect(string).to.eq('?foo=true&bar[]=1&bar[]=2&bar[]=3') + }) + + it('should drop nullish params', () => { + const string = paramsString({ present: 'yes', missing: null }) + + expect(string).to.eq('?present=yes') + }) + + it('should convert camelCase keys to snake_keys objects correctly', () => { + const string = paramsString({ isActive: true, MaybeNot: false }) + + expect(string).to.eq('?is_active=true&maybe_not=false') + }) + + it('should work with maps', () => { + const string = paramsString( + new Map([ + ['key', 'yes'], + ['key2', 'also yes'], + ]), + ) + + expect(string).to.eq('?key=yes&key_2=also%20yes') + }) + + it('should escape components correctly', () => { + const string = paramsString({ gachi: '♂', muchi: 'Билли Геррингтон' }) + + expect(string).to.eq( + '?gachi=%E2%99%82&muchi=%D0%91%D0%B8%D0%BB%D0%BB%D0%B8%20%D0%93%D0%B5%D1%80%D1%80%D0%B8%D0%BD%D0%B3%D1%82%D0%BE%D0%BD', + ) + }) + + it('should throw when passed a non-object', () => { + expect(() => { + paramsString('Totally an object') + }).to.throw() + }) + + it('should throw when passed an array', () => { + expect(() => { + paramsString(['Totally an object']) + }).to.throw() + }) + + it('should throw when array param is non-primitive', () => { + expect(() => { + paramsString({ a: [() => ''] }) + }).to.throw() + }) + + it('should throw when array param is nullish', () => { + expect(() => { + paramsString({ a: [1, null, 3] }) + }).to.throw() + }) + }) +}) diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index cd877b05e..afd17e56b 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,80 +1,10 @@ +import mastoapidata from '../../../../fixtures/mastoapi.json' + import { parseLinkHeaderPagination, - parseNotification, parseStatus, parseUser, -} from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' -import mastoapidata from '../../../../fixtures/mastoapi.json' -import qvitterapidata from '../../../../fixtures/statuses.json' - -const makeMockStatusQvitter = (overrides = {}) => { - return Object.assign( - { - activity_type: 'post', - attachments: [], - attentions: [], - created_at: 'Tue Jan 15 13:57:56 +0000 2019', - external_url: 'https://ap.example/whatever', - fave_num: 1, - favorited: false, - id: '10335970', - in_reply_to_ostatus_uri: null, - in_reply_to_profileurl: null, - in_reply_to_screen_name: null, - in_reply_to_status_id: null, - in_reply_to_user_id: null, - is_local: false, - is_post_verb: true, - possibly_sensitive: false, - repeat_num: 0, - repeated: false, - statusnet_conversation_id: '16300488', - summary: null, - tags: [], - text: 'haha benis', - uri: 'https://ap.example/whatever', - user: makeMockUserQvitter(), - visibility: 'public', - }, - overrides, - ) -} - -const makeMockUserQvitter = (overrides = {}) => { - return Object.assign( - { - background_image: null, - cover_photo: '', - created_at: 'Mon Jan 14 13:56:51 +0000 2019', - default_scope: 'public', - description: 'ebin', - description_html: '

ebin

', - favourites_count: 0, - fields: [], - followers_count: 1, - following: true, - follows_you: true, - friends_count: 1, - id: '60717', - is_local: false, - locked: false, - name: 'Spurdo :ebin:', - name_html: 'Spurdo ', - no_rich_text: false, - pleroma: { confirmation_pending: false, tags: [] }, - profile_image_url: 'https://ap.example/whatever', - profile_image_url_https: 'https://ap.example/whatever', - profile_image_url_original: 'https://ap.example/whatever', - profile_image_url_profile_size: 'https://ap.example/whatever', - rights: { delete_others_notice: false }, - screen_name: 'spurdo@ap.example', - statuses_count: 46, - statusnet_blocking: false, - statusnet_profile_url: '', - }, - overrides, - ) -} +} from 'src/services/entity_normalizer/entity_normalizer.service.js' const makeMockUserMasto = (overrides = {}) => { return Object.assign( @@ -151,19 +81,6 @@ const makeMockStatusMasto = (overrides = {}) => { ) } -const makeMockNotificationQvitter = (overrides = {}) => { - return Object.assign( - { - notice: makeMockStatusQvitter(), - ntype: 'follow', - from_profile: makeMockUserQvitter(), - is_seen: 0, - id: 123, - }, - overrides, - ) -} - const makeMockEmojiMasto = (overrides = [{}]) => { return [ Object.assign( @@ -189,78 +106,6 @@ const makeMockEmojiMasto = (overrides = [{}]) => { describe('API Entities normalizer', () => { describe('parseStatus', () => { - describe('QVitter preprocessing', () => { - it("doesn't blow up", () => { - const parsed = qvitterapidata.map(parseStatus) - expect(parsed.length).to.eq(qvitterapidata.length) - }) - - it('identifies favorites', () => { - const fav = { - uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00', - is_post_verb: false, - } - - const mastoFav = { - uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite', - is_post_verb: false, - } - - expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property( - 'type', - 'favorite', - ) - expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property( - 'type', - 'favorite', - ) - }) - - it('processes repeats correctly', () => { - const post = makeMockStatusQvitter({ - retweeted_status: null, - id: 'deadbeef', - }) - const repeat = makeMockStatusQvitter({ - retweeted_status: post, - is_post_verb: false, - id: 'foobar', - }) - - const parsedPost = parseStatus(post) - const parsedRepeat = parseStatus(repeat) - - expect(parsedPost).to.have.property('type', 'status') - expect(parsedRepeat).to.have.property('type', 'retweet') - expect(parsedRepeat).to.have.property('retweeted_status') - expect(parsedRepeat).to.have.nested.property( - 'retweeted_status.id', - 'deadbeef', - ) - }) - - it('sets nsfw for statuses with the #nsfw tag', () => { - const safe = makeMockStatusQvitter({ id: '1', text: 'Hello oniichan' }) - const nsfw = makeMockStatusQvitter({ - id: '1', - text: 'Hello oniichan #nsfw', - }) - - expect(parseStatus(safe).nsfw).to.eq(false) - expect(parseStatus(nsfw).nsfw).to.eq(true) - }) - - it('leaves existing nsfw settings alone', () => { - const nsfw = makeMockStatusQvitter({ - id: '1', - text: 'Hello oniichan #nsfw', - nsfw: false, - }) - - expect(parseStatus(nsfw).nsfw).to.eq(false) - }) - }) - describe('Mastoapi preprocessing and converting', () => { it("doesn't blow up", () => { const parsed = mastoapidata.map(parseStatus) @@ -344,60 +189,6 @@ describe('API Entities normalizer', () => { }) }) - // We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI - // is more of an afterthought - describe('parseNotifications (QvitterAPI)', () => { - it("correctly normalizes data to FE's format", () => { - const notif = makeMockNotificationQvitter({ - id: 123, - notice: makeMockStatusQvitter({ id: 444 }), - from_profile: makeMockUserQvitter({ id: 'spurdo' }), - }) - expect(parseNotification(notif)).to.have.property('id', 123) - expect(parseNotification(notif)).to.have.property('seen', false) - expect(parseNotification(notif)).to.have.nested.property( - 'status.id', - '444', - ) - expect(parseNotification(notif)).to.have.nested.property( - 'action.id', - '444', - ) - expect(parseNotification(notif)).to.have.nested.property( - 'from_profile.id', - 'spurdo', - ) - }) - - it('correctly normalizes favorite notifications', () => { - const notif = makeMockNotificationQvitter({ - id: 123, - ntype: 'like', - notice: makeMockStatusQvitter({ - id: 444, - favorited_status: makeMockStatusQvitter({ id: 4412 }), - }), - is_seen: 1, - from_profile: makeMockUserQvitter({ id: 'spurdo' }), - }) - expect(parseNotification(notif)).to.have.property('id', 123) - expect(parseNotification(notif)).to.have.property('type', 'like') - expect(parseNotification(notif)).to.have.property('seen', true) - expect(parseNotification(notif)).to.have.nested.property( - 'status.id', - '4412', - ) - expect(parseNotification(notif)).to.have.nested.property( - 'action.id', - '444', - ) - expect(parseNotification(notif)).to.have.nested.property( - 'from_profile.id', - 'spurdo', - ) - }) - }) - describe('Link header pagination', () => { it('Parses min and max ids as integers', () => { const linkHeader = diff --git a/test/unit/specs/services/theme_data/iss_deserializer.spec.js b/test/unit/specs/services/theme_data/iss_deserializer.spec.js index 0b93b6149..75cd6efab 100644 --- a/test/unit/specs/services/theme_data/iss_deserializer.spec.js +++ b/test/unit/specs/services/theme_data/iss_deserializer.spec.js @@ -27,7 +27,7 @@ describe('ISS (de)serialization', () => { /* // Debug snippet - const onlyComponent = componentsContext('./components/panel_header.style.js').default + const onlyComponent = componentsContext('src/components/panel_header.style.js').default it.only(`(De)serialization of component ${onlyComponent.name} works`, () => { const normalized = onlyComponent.defaultRules.map(x => ({ component: onlyComponent.name, ...x })) console.debug('BEGIN INPUT ================') diff --git a/test/unit/specs/stores/lists.spec.js b/test/unit/specs/stores/lists.spec.js index 083730856..bb1ef12b4 100644 --- a/test/unit/specs/stores/lists.spec.js +++ b/test/unit/specs/stores/lists.spec.js @@ -1,22 +1,23 @@ -import { createPinia, setActivePinia } from 'pinia' -import { createStore } from 'vuex' +import { createTestingPinia } from '@pinia/testing' +import { HttpResponse, http } from 'msw' +import { setActivePinia } from 'pinia' + +import { test as it } from '/test/fixtures/mock_api.js' import { useListsStore } from 'src/stores/lists.js' -import apiModule from 'src/modules/api.js' - -setActivePinia(createPinia()) -const store = useListsStore() -window.vuex = createStore({ - modules: { - api: apiModule, - }, -}) +import { MASTODON_LIST_ACCOUNTS_URL, MASTODON_LIST_URL } from 'src/api/user.js' describe('The lists store', () => { + let store + + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + store = useListsStore() + }) + describe('actions', () => { it('updates array of all lists', () => { - store.$reset() const list = { id: '1', title: 'testList' } store.setLists([list]) @@ -24,12 +25,19 @@ describe('The lists store', () => { expect(store.allLists).to.eql([list]) }) - it('adds a new list with a title, updating the title for existing lists', () => { - store.$reset() + it('adds a new list with a title, updating the title for existing lists', async ({ + worker, + }) => { const list = { id: '1', title: 'testList' } const modList = { id: '1', title: 'anotherTestTitle' } - store.setList({ listId: list.id, title: list.title }) + worker.use( + http.put(MASTODON_LIST_URL(':id'), () => + HttpResponse.json({ ok: true }), + ), + ) + + await store.setList({ listId: list.id, title: list.title }) expect(store.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [], @@ -37,7 +45,7 @@ describe('The lists store', () => { expect(store.allLists).to.have.length(1) expect(store.allLists[0]).to.eql(list) - store.setList({ listId: modList.id, title: modList.title }) + await store.setList({ listId: modList.id, title: modList.title }) expect(store.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [], @@ -46,24 +54,38 @@ describe('The lists store', () => { expect(store.allLists[0]).to.eql(modList) }) - it('adds a new list with an array of IDs, updating the IDs for existing lists', () => { - store.$reset() + it('adds a new list with an array of IDs, updating the IDs for existing lists', async ({ + worker, + }) => { const list = { id: '1', accountIds: ['1', '2', '3'] } const modList = { id: '1', accountIds: ['3', '4', '5'] } - store.setListAccounts({ listId: list.id, accountIds: list.accountIds }) + worker.use( + http.post(MASTODON_LIST_ACCOUNTS_URL(':id'), () => + HttpResponse.json({ ok: true }), + ), + http.delete(MASTODON_LIST_ACCOUNTS_URL(':id'), () => + HttpResponse.json({ ok: true }), + ), + ) + + await store.setListAccounts({ + listId: list.id, + accountIds: list.accountIds, + }) expect(store.allListsObject[list.id].accountIds).to.eql(list.accountIds) - store.setListAccounts({ + await store.setListAccounts({ listId: modList.id, accountIds: modList.accountIds, }) + expect(store.allListsObject[modList.id].accountIds).to.eql( modList.accountIds, ) }) - it('deletes a list', () => { + it('deletes a list', async ({ worker }) => { store.$patch({ allLists: [{ id: '1', title: 'testList' }], allListsObject: { @@ -72,7 +94,13 @@ describe('The lists store', () => { }) const listId = '1' - store.deleteList({ listId }) + worker.use( + http.delete(MASTODON_LIST_URL(':id'), () => + HttpResponse.json({ ok: true }), + ), + ) + + await store.deleteList({ listId }) expect(store.allLists).to.have.length(0) expect(store.allListsObject).to.eql({}) }) diff --git a/test/unit/specs/stores/oauth.spec.js b/test/unit/specs/stores/oauth.spec.js index 4664bba02..a06cbb2fd 100644 --- a/test/unit/specs/stores/oauth.spec.js +++ b/test/unit/specs/stores/oauth.spec.js @@ -1,27 +1,80 @@ import { createTestingPinia } from '@pinia/testing' import { HttpResponse, http } from 'msw' -import { createPinia, setActivePinia } from 'pinia' +import { setActivePinia } from 'pinia' -import { - authApis, - injectMswToTest, - testServer, -} from '/test/fixtures/mock_api.js' +import { test as it } from '/test/fixtures/mock_api.js' -import { useInstanceStore } from 'src/stores/instance.js' import { useOAuthStore } from 'src/stores/oauth.js' -const test = injectMswToTest(authApis) +import { + MASTODON_APP_URL, + MASTODON_APP_VERIFY_URL, + OAUTH_TOKEN_URL, +} from 'src/api/oauth.js' + +const authApis = () => [ + http.post(MASTODON_APP_URL, () => { + return HttpResponse.json({ + client_id: 'test-id', + client_secret: 'test-secret', + }) + }), + http.get(MASTODON_APP_VERIFY_URL, ({ request }) => { + const authHeader = request.headers.get('Authorization') + if ( + authHeader === 'Bearer test-app-token' || + authHeader === 'Bearer also-good-app-token' + ) { + return HttpResponse.json({}) + } else { + // Pleroma 2.9.0 gives the following respoonse upon error + return HttpResponse.json( + { error: { detail: 'Internal server error' } }, + { + status: 400, + }, + ) + } + }), + http.post(OAUTH_TOKEN_URL, async ({ request }) => { + const data = await request.formData() + + if ( + data.get('client_id') === 'test-id' && + data.get('client_secret') === 'test-secret' && + data.get('grant_type') === 'client_credentials' && + data.has('redirect_uri') + ) { + return HttpResponse.json({ access_token: 'test-app-token' }) + } else { + // Pleroma 2.9.0 gives the following respoonse upon error + return HttpResponse.json( + { error: 'Invalid credentials' }, + { + status: 400, + }, + ) + } + }), +] describe('oauth store', () => { beforeEach(() => { setActivePinia(createTestingPinia({ stubActions: false })) - useInstanceStore().server = testServer }) describe('createApp', () => { - test('it should use create an app and record client id and secret', async () => { + it('should use create an app and record client id and secret', async ({ + worker, + }) => { + worker.use( + http.post(MASTODON_APP_URL, () => { + return HttpResponse.text('Throttled', { status: 429 }) + }), + ) + const store = useOAuthStore() + worker.use(...authApis()) const app = await store.createApp() expect(store.clientId).to.eql('test-id') expect(store.clientSecret).to.eql('test-secret') @@ -29,9 +82,9 @@ describe('oauth store', () => { expect(app.clientSecret).to.eql('test-secret') }) - test('it should throw and not update if failed', async ({ worker }) => { + it('should throw and not update if failed', async ({ worker }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Throttled', { status: 429 }) }), ) @@ -45,7 +98,8 @@ describe('oauth store', () => { }) describe('ensureApp', () => { - test('it should create an app if it does not exist', async () => { + it('should create an app if it does not exist', async ({ worker }) => { + worker.use(...authApis()) const store = useOAuthStore() const app = await store.ensureApp() expect(store.clientId).to.eql('test-id') @@ -54,9 +108,9 @@ describe('oauth store', () => { expect(app.clientSecret).to.eql('test-secret') }) - test('it should not create an app if it exists', async ({ worker }) => { + it('should not create an app if it exists', async ({ worker }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Should not call this API', { status: 400 }) }), ) @@ -74,7 +128,8 @@ describe('oauth store', () => { }) describe('getAppToken', () => { - test('it should get app token and set it in state', async () => { + it('should get app token and set it in state', async ({ worker }) => { + worker.use(...authApis()) const store = useOAuthStore() store.clientId = 'test-id' store.clientSecret = 'test-secret' @@ -84,7 +139,10 @@ describe('oauth store', () => { expect(store.appToken).to.eql('test-app-token') }) - test('it should throw and not set state if it cannot get app token', async () => { + it('should throw and not set state if it cannot get app token', async ({ + worker, + }) => { + worker.use(...authApis()) const store = useOAuthStore() store.clientId = 'bad-id' store.clientSecret = 'bad-secret' @@ -95,14 +153,16 @@ describe('oauth store', () => { }) describe('ensureAppToken', () => { - test('it should work if the state is empty', async () => { + it('should work if the state is empty', async ({ worker }) => { + worker.use(...authApis()) const store = useOAuthStore() const token = await store.ensureAppToken() expect(token).to.eql('test-app-token') expect(store.appToken).to.eql('test-app-token') }) - test('it should work if we already have a working token', async () => { + it('should work if we already have a working token', async ({ worker }) => { + worker.use(...authApis()) const store = useOAuthStore() store.appToken = 'also-good-app-token' @@ -111,11 +171,12 @@ describe('oauth store', () => { expect(store.appToken).to.eql('also-good-app-token') }) - test('it should work if we have a bad token but good app credentials', async ({ + it('should work if we have a bad token but good app credentials', async ({ worker, }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + ...authApis(), + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Should not call this API', { status: 400 }) }), ) @@ -129,11 +190,12 @@ describe('oauth store', () => { expect(store.appToken).to.eql('test-app-token') }) - test('it should work if we have no token but good app credentials', async ({ + it('should work if we have no token but good app credentials', async ({ worker, }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + ...authApis(), + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Should not call this API', { status: 400 }) }), ) @@ -146,7 +208,10 @@ describe('oauth store', () => { expect(store.appToken).to.eql('test-app-token') }) - test('it should work if we have no token and bad app credentials', async () => { + it('should work if we have no token and bad app credentials', async ({ + worker, + }) => { + worker.use(...authApis()) const store = useOAuthStore() store.clientId = 'bad-id' store.clientSecret = 'bad-secret' @@ -158,7 +223,10 @@ describe('oauth store', () => { expect(store.clientSecret).to.eql('test-secret') }) - test('it should work if we have bad token and bad app credentials', async () => { + it('should work if we have bad token and bad app credentials', async ({ + worker, + }) => { + worker.use(...authApis()) const store = useOAuthStore() store.appToken = 'bad-app-token' store.clientId = 'bad-id' @@ -171,9 +239,9 @@ describe('oauth store', () => { expect(store.clientSecret).to.eql('test-secret') }) - test('it should throw if we cannot create an app', async ({ worker }) => { + it('should throw if we cannot create an app', async ({ worker }) => { worker.use( - http.post(`${testServer}/api/v1/apps`, () => { + http.post(MASTODON_APP_URL, () => { return HttpResponse.text('Throttled', { status: 429 }) }), ) @@ -182,17 +250,15 @@ describe('oauth store', () => { await expect(store.ensureAppToken()).rejects.toThrowError('Throttled') }) - test('it should throw if we cannot obtain app token', async ({ - worker, - }) => { + it('should throw if we cannot obtain app token', async ({ worker }) => { worker.use( - http.post(`${testServer}/oauth/token`, () => { + http.post(OAUTH_TOKEN_URL, () => { return HttpResponse.text('Throttled', { status: 429 }) }), ) const store = useOAuthStore() - await expect(store.ensureAppToken()).rejects.toThrowError('Throttled') + await expect(store.getAppToken()).rejects.toThrowError('Throttled') }) }) }) diff --git a/test/unit/specs/stores/sync_config.spec.js b/test/unit/specs/stores/sync_config.spec.js index 4a502f626..a045aa18a 100644 --- a/test/unit/specs/stores/sync_config.spec.js +++ b/test/unit/specs/stores/sync_config.spec.js @@ -232,7 +232,8 @@ describe('The SyncConfig store', () => { expect(store.prefsStorage._journal.length).to.eql(2) }) - it('should remove depth = 3 set/unset entries from journal', () => { + // 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 = () => { diff --git a/test/unit/specs/stores/user_highlight.spec.js b/test/unit/specs/stores/user_highlight.spec.js index c46852256..f80143b6e 100644 --- a/test/unit/specs/stores/user_highlight.spec.js +++ b/test/unit/specs/stores/user_highlight.spec.js @@ -1,10 +1,8 @@ -import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { _getRecentData, _mergeHighlights, - _moveItemInArray, useUserHighlightStore, } from 'src/stores/user_highlight.js' diff --git a/tools/e2e/run.sh b/tools/e2e/run.sh index 3c0ba8a36..452302cbd 100644 --- a/tools/e2e/run.sh +++ b/tools/e2e/run.sh @@ -5,7 +5,7 @@ set -u COMPOSE_FILE="docker-compose.e2e.yml" : "${COMPOSE_MENU:=false}" -: "${PLEROMA_IMAGE:=git.pleroma.social:5050/pleroma/pleroma:stable}" +: "${PLEROMA_IMAGE:=git.pleroma.social/pleroma/pleroma:stable}" : "${E2E_ADMIN_USERNAME:=admin}" : "${E2E_ADMIN_PASSWORD:=adminadmin}" : "${E2E_ADMIN_EMAIL:=admin@example.com}" diff --git a/vite.config.js b/vite.config.js index 401cb4b35..beff5fe76 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,6 +2,7 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' +import { playwright } from '@vitest/browser-playwright' import { defineConfig } from 'vite' import eslint from 'vite-plugin-eslint2' import stylelint from 'vite-plugin-stylelint' @@ -11,11 +12,7 @@ import { getCommitHash } from './build/commit_hash.js' import copyPlugin from './build/copy_plugin.js' import emojisPlugin from './build/emojis_plugin.js' import mswPlugin from './build/msw_plugin.js' -import { - buildSwPlugin, - devSwPlugin, - swMessagesPlugin, -} from './build/sw_plugin.js' +import { buildSwPlugin, swMessagesPlugin } from './build/sw_plugin.js' const localConfigPath = '/config/local.json' const normalizeTarget = (target) => { @@ -75,6 +72,7 @@ export default defineConfig(async ({ mode, command }) => { const settings = await getLocalDevSettings() const target = settings.target || 'http://localhost:4000/' const origin = settings.origin || target + const targetSW = target.replace(/^http/, 'ws') const transformSW = getTransformSWSettings(settings) const proxy = { '/api': { @@ -82,6 +80,14 @@ export default defineConfig(async ({ mode, command }) => { changeOrigin: true, cookieDomainRewrite: 'localhost', ws: true, + rewriteWsOrigin: true, + }, + '/auth': { + // Mastodon password reset lives here + target, + changeOrigin: true, + cookieDomainRewrite: 'localhost', + ws: true, }, '/nodeinfo': { target, @@ -93,20 +99,21 @@ export default defineConfig(async ({ mode, command }) => { changeOrigin: true, cookieDomainRewrite: 'localhost', }, - '/socket': { - target, - changeOrigin: true, - cookieDomainRewrite: 'localhost', - ws: true, - headers: { - Origin: origin, - }, - }, '/oauth': { target, changeOrigin: true, cookieDomainRewrite: 'localhost', }, + '/socket': { + target: targetSW, + changeOrigin: true, + cookieDomainRewrite: 'localhost', + rewriteWsOrigin: true, + ws: true, + headers: { + Origin: origin, + }, + }, } const swSrc = 'src/sw.js' @@ -135,7 +142,6 @@ export default defineConfig(async ({ mode, command }) => { }, }), vueJsx(), - devSwPlugin({ swSrc, swDest, transformSW, alias }), buildSwPlugin({ swSrc, swDest }), swMessagesPlugin(), emojisPlugin(), @@ -158,19 +164,6 @@ export default defineConfig(async ({ mode, command }) => { }), ...(mode === 'test' ? [mswPlugin()] : []), ], - optimizeDeps: { - // For unknown reasons, during vitest, vite will re-optimize the following - // deps, causing the test to reload, so add them here so that it will not - // reload during tests - include: [ - 'custom-event-polyfill', - 'vue-i18n', - '@ungap/event-target', - 'lodash.merge', - 'body-scroll-lock', - '@kazvmoe-infra/pinch-zoom-element', - ], - }, css: { devSourcemap: true, }, @@ -195,14 +188,14 @@ export default defineConfig(async ({ mode, command }) => { __VUE_PROD_DEVTOOLS__: false, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, }, + // devtools: { enabled: true }, build: { sourcemap: true, - rollupOptions: { + rolldownOptions: { input: { main: 'index.html', }, output: { - inlineDynamicImports: false, entryFileNames(chunkInfo) { const id = chunkInfo.facadeModuleId if (id.endsWith(swSrc)) { @@ -250,8 +243,10 @@ export default defineConfig(async ({ mode, command }) => { exclude: [...configDefaults.exclude, 'test/e2e-playwright/**'], browser: { enabled: true, - provider: 'playwright', - instances: [{ browser: 'firefox' }], + headless: true, + provider: playwright(), + // https://github.com/mswjs/msw/issues/2757 + instances: [{ browser: 'chromium' }], }, }, } diff --git a/yarn.lock b/yarn.lock index 54d81f139..b60f88b7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,14 +2,6 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - "@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" @@ -37,7 +29,7 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.27.1": +"@babel/code-frame@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== @@ -46,6 +38,15 @@ js-tokens "^4.0.0" picocolors "^1.1.1" +"@babel/code-frame@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== + dependencies: + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790" @@ -56,6 +57,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f" integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== +"@babel/compat-data@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" + integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== + "@babel/core@7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" @@ -77,21 +83,21 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/core@^7.27.1": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.3.tgz#aceddde69c5d1def69b839d09efa3e3ff59c97cb" - integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ== +"@babel/core@^7.29.0": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" + integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.3" - "@babel/helper-compilation-targets" "^7.27.2" - "@babel/helper-module-transforms" "^7.28.3" - "@babel/helpers" "^7.28.3" - "@babel/parser" "^7.28.3" - "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.3" - "@babel/types" "^7.28.2" + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helpers" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -140,6 +146,17 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" +"@babel/generator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3" + integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== + dependencies: + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" @@ -154,6 +171,13 @@ dependencies: "@babel/types" "^7.27.3" +"@babel/helper-annotate-as-pure@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz#c70fe3c6ecbdc3fd2dd1b0f498428b88b82ce47f" + integrity sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw== + dependencies: + "@babel/types" "^7.29.7" + "@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": version "7.27.2" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" @@ -165,6 +189,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" + integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz#3e747434ea007910c320c4d39a6b46f20f371d46" @@ -178,6 +213,19 @@ "@babel/traverse" "^7.28.3" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz#6eddf286f2ec418f740c91d60a83347c55838ddd" + integrity sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-member-expression-to-functions" "^7.29.7" + "@babel/helper-optimise-call-expression" "^7.29.7" + "@babel/helper-replace-supers" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/traverse" "^7.29.7" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.0.tgz#0e41f7d38c2ebe06ebd9cf0e02fb26019c77cd95" @@ -212,6 +260,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== +"@babel/helper-globals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" + integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== + "@babel/helper-member-expression-to-functions@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" @@ -220,6 +273,14 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helper-member-expression-to-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz#8dbdb3ce0b5c487e1aec10e13c9a43a500814df8" + integrity sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg== + dependencies: + "@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" @@ -236,6 +297,14 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helper-module-imports@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" + integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/helper-module-transforms@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz#e1663b8b71d2de948da5c4fb2a20ca4f3ec27a6f" @@ -254,6 +323,15 @@ "@babel/helper-validator-identifier" "^7.27.1" "@babel/traverse" "^7.28.3" +"@babel/helper-module-transforms@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" + integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/helper-optimise-call-expression@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" @@ -261,6 +339,13 @@ dependencies: "@babel/types" "^7.27.1" +"@babel/helper-optimise-call-expression@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz#77b0b5b94f1997fa9d6e3125f445227b1faf9d85" + integrity sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong== + dependencies: + "@babel/types" "^7.29.7" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6": version "7.26.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" @@ -271,6 +356,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== +"@babel/helper-plugin-utils@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz#c0a0766f1a13617d8a17407d7ab8f9d486225ea4" + integrity sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw== + "@babel/helper-remap-async-to-generator@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" @@ -289,6 +379,15 @@ "@babel/helper-optimise-call-expression" "^7.27.1" "@babel/traverse" "^7.27.1" +"@babel/helper-replace-supers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz#bc3c3964329043c79112e513c1b198f16589ac21" + integrity sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.29.7" + "@babel/helper-optimise-call-expression" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" @@ -297,6 +396,14 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helper-skip-transparent-expression-wrappers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz#50c95c7e4c4f54936cfa0116428edc559862d551" + integrity sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/helper-string-parser@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" @@ -307,6 +414,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== + "@babel/helper-validator-identifier@^7.25.9", "@babel/helper-validator-identifier@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" @@ -317,11 +429,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== +"@babel/helper-validator-option@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" + integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== + "@babel/helper-wrap-function@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz#b88285009c31427af318d4fe37651cd62a142409" @@ -331,14 +453,6 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" -"@babel/helpers@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.3.tgz#b83156c0a2232c133d1b535dd5d3452119c7e441" - integrity sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw== - dependencies: - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.2" - "@babel/helpers@^7.28.4": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" @@ -347,6 +461,14 @@ "@babel/template" "^7.27.2" "@babel/types" "^7.28.4" +"@babel/helpers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" + integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== + dependencies: + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/highlight@^7.0.0": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" @@ -385,6 +507,13 @@ dependencies: "@babel/types" "^7.28.5" +"@babel/parser@^7.29.3", "@babel/parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334" + integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== + dependencies: + "@babel/types" "^7.29.7" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz#fbde57974707bbfa0376d34d425ff4fa6c732421" @@ -450,12 +579,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-typescript@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" - integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== +"@babel/plugin-syntax-typescript@^7.28.6", "@babel/plugin-syntax-typescript@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz#7c29388932313ed58413a0343048d75d92fb5b24" + integrity sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA== dependencies: - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-plugin-utils" "^7.29.7" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -859,16 +988,16 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-typescript@^7.27.1": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz#796cbd249ab56c18168b49e3e1d341b72af04a6b" - integrity sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg== +"@babel/plugin-transform-typescript@^7.28.6": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz#f0449c3df7037bbe232043476851c38f5e4a7615" + integrity sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw== dependencies: - "@babel/helper-annotate-as-pure" "^7.27.3" - "@babel/helper-create-class-features-plugin" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" - "@babel/plugin-syntax-typescript" "^7.27.1" + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-syntax-typescript" "^7.29.7" "@babel/plugin-transform-unicode-escapes@^7.27.1": version "7.27.1" @@ -1002,11 +1131,6 @@ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== -"@babel/runtime@^7.12.5": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.1.tgz#9fce313d12c9a77507f264de74626e87fd0dc541" - integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== - "@babel/template@^7.27.0": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" @@ -1025,6 +1149,15 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" +"@babel/template@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" + integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@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" @@ -1077,6 +1210,19 @@ "@babel/types" "^7.28.5" debug "^4.3.1" +"@babel/traverse@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" + integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-globals" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.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": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" @@ -1109,6 +1255,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@babel/types@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" + integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== + dependencies: + "@babel/helper-string-parser" "^7.29.7" + "@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" @@ -1168,27 +1322,15 @@ resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz#71ba2fb5505b3b01dd3cf551ef329e0094636125" integrity sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg== -"@bundled-es-modules/cookie@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz#b41376af6a06b3e32a15241d927b840a9b4de507" - integrity sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw== - dependencies: - cookie "^0.7.2" +"@blazediff/core@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@blazediff/core/-/core-1.9.1.tgz#ad61c4ec48dc11a2913b9753c8c74902e05e8f14" + integrity sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA== -"@bundled-es-modules/statuses@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz#761d10f44e51a94902c4da48675b71a76cc98872" - integrity sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg== - dependencies: - statuses "^2.0.1" - -"@bundled-es-modules/tough-cookie@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz#fa9cd3cedfeecd6783e8b0d378b4a99e52bde5d3" - integrity sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw== - dependencies: - "@types/tough-cookie" "^4.0.5" - tough-cookie "^4.1.4" +"@bufbuild/protobuf@^2.5.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.12.0.tgz#53225636a8fcebb2bd94998ad9d42f99f96add4d" + integrity sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA== "@cacheable/memoize@^2.0.3": version "2.0.3" @@ -1370,140 +1512,43 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b" integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw== +"@devframes/hub@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@devframes/hub/-/hub-0.5.2.tgz#ac35e96525def8cf9f713fb305f6b1a8a8338ab2" + integrity sha512-qMkBFw1OqhPuNs1tQWkRq0z0Tg49kXNu53bs59tdF4lytKupatWVnL3cpsVPqn+Q5P7A70r99BKTcm+prMtHqw== + dependencies: + birpc "^4.0.0" + nostics "^0.2.0" + pathe "^2.0.3" + perfect-debounce "^2.1.0" + tinyexec "^1.2.2" + "@dual-bundle/import-meta-resolve@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz#cd0b25b3808cd9e684cd6cd549bbf8e1dcf05ee7" integrity sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg== -"@esbuild/aix-ppc64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49" - integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg== +"@emnapi/core@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" + integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== + dependencies: + "@emnapi/wasi-threads" "1.2.1" + tslib "^2.4.0" -"@esbuild/android-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03" - integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ== +"@emnapi/runtime@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" + integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== + dependencies: + tslib "^2.4.0" -"@esbuild/android-arm@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae" - integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg== - -"@esbuild/android-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6" - integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g== - -"@esbuild/darwin-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz#0a678c4ac4bf8717e67481e1a797e6c152f93c84" - integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w== - -"@esbuild/darwin-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe" - integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ== - -"@esbuild/freebsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a" - integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA== - -"@esbuild/freebsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb" - integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw== - -"@esbuild/linux-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5" - integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA== - -"@esbuild/linux-arm@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f" - integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw== - -"@esbuild/linux-ia32@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b" - integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw== - -"@esbuild/linux-loong64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb" - integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw== - -"@esbuild/linux-mips64el@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5" - integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ== - -"@esbuild/linux-ppc64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74" - integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw== - -"@esbuild/linux-riscv64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273" - integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww== - -"@esbuild/linux-s390x@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263" - integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw== - -"@esbuild/linux-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910" - integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ== - -"@esbuild/netbsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077" - integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg== - -"@esbuild/netbsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034" - integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A== - -"@esbuild/openbsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad" - integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg== - -"@esbuild/openbsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2" - integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw== - -"@esbuild/openharmony-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1" - integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ== - -"@esbuild/sunos-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244" - integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA== - -"@esbuild/win32-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935" - integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q== - -"@esbuild/win32-ia32@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343" - integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA== - -"@esbuild/win32-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f" - integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA== +"@emnapi/wasi-threads@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" + integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== + dependencies: + tslib "^2.4.0" "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.5.0": version "4.5.1" @@ -1580,6 +1625,26 @@ "@eslint/core" "^0.17.0" levn "^0.4.1" +"@floating-ui/core@^1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.5.tgz#d4af157a03330af5a60e69da7a4692507ada0622" + integrity sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ== + dependencies: + "@floating-ui/utils" "^0.2.11" + +"@floating-ui/dom@^1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.6.tgz#f915bba5abbb177e1f227cacee1b4d0634b187bf" + integrity sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ== + dependencies: + "@floating-ui/core" "^1.7.5" + "@floating-ui/utils" "^0.2.11" + +"@floating-ui/utils@^0.2.11": + version "0.2.11" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f" + integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== + "@fortawesome/fontawesome-common-types@7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz#a4e0b7e40073d5fdef41182da1bc216a05875659" @@ -1639,37 +1704,41 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.2.tgz#1860473de7dfa1546767448f333db80cb0ff2161" integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ== -"@inquirer/confirm@^5.0.0": - version "5.1.8" - resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.8.tgz#476af2476cd4867905dcabfca8598da4dd65e923" - integrity sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg== - dependencies: - "@inquirer/core" "^10.1.9" - "@inquirer/type" "^3.0.5" +"@inquirer/ansi@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-2.0.7.tgz#86de22810cac3ed406ec10f8d66016815b8226b4" + integrity sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q== -"@inquirer/core@^10.1.9": - version "10.1.9" - resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.9.tgz#9ab672a2d9ca60c5d45c7fa9b63e4fe9e038a02e" - integrity sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw== +"@inquirer/confirm@^6.0.11": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-6.1.1.tgz#9c6a7d79c6132b2af57fdb75747f056204e55356" + integrity sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ== dependencies: - "@inquirer/figures" "^1.0.11" - "@inquirer/type" "^3.0.5" - ansi-escapes "^4.3.2" + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/core@^11.2.1": + version "11.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-11.2.1.tgz#54ccd8f7d47852140b6066cbd77d63b2c2b168fd" + integrity sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" cli-width "^4.1.0" - mute-stream "^2.0.0" + fast-wrap-ansi "^0.2.0" + mute-stream "^3.0.0" signal-exit "^4.1.0" - wrap-ansi "^6.2.0" - yoctocolors-cjs "^2.1.2" -"@inquirer/figures@^1.0.11": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.11.tgz#4744e6db95288fea1dead779554859710a959a21" - integrity sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw== +"@inquirer/figures@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-2.0.7.tgz#f5cc5843732a81304d06a0db4b53cc7dbda15541" + integrity sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw== -"@inquirer/type@^3.0.5": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.5.tgz#fe00207e57d5f040e5b18e809c8e7abc3a2ade3a" - integrity sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg== +"@inquirer/type@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-4.0.7.tgz#9c6f0d857fe6ad549a3a932343b64e76acb34b10" + integrity sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g== "@intlify/core-base@11.1.12": version "11.1.12" @@ -1762,10 +1831,10 @@ resolved "https://registry.yarnpkg.com/@keyv/serialize/-/serialize-1.1.1.tgz#0c01dd3a3483882af7cf3878d4e71d505c81fc4a" integrity sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA== -"@mswjs/interceptors@^0.39.1": - version "0.39.2" - resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.39.2.tgz#de9de0ab23f99d387c7904df7219a92157d1d666" - integrity sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg== +"@mswjs/interceptors@^0.41.3": + version "0.41.9" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.41.9.tgz#9d90bbd60d1ddc30dbcbb827a9bb2e470493530d" + integrity sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w== dependencies: "@open-draft/deferred-promise" "^2.2.0" "@open-draft/logger" "^0.3.0" @@ -1876,6 +1945,13 @@ "@napi-rs/nice-win32-ia32-msvc" "1.0.1" "@napi-rs/nice-win32-x64-msvc" "1.0.1" +"@napi-rs/wasm-runtime@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1" + integrity sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow== + dependencies: + "@tybys/wasm-util" "^0.10.1" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -1938,6 +2014,11 @@ resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA== +"@open-draft/deferred-promise@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz#9725acc5afe8ecde690e9e198a094859fdbf2e45" + integrity sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA== + "@open-draft/logger@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954" @@ -1946,99 +2027,213 @@ is-node-process "^1.2.0" outvariant "^1.4.0" -"@open-draft/until@^2.0.0", "@open-draft/until@^2.1.0": +"@open-draft/until@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== -"@parcel/watcher-android-arm64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" - integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== +"@oxc-parser/binding-android-arm-eabi@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.132.0.tgz#f88d600252349b5e380e695cadf889cea896f676" + integrity sha512-KrLaPWa5c9Y7LkW+rKkaUE3y7DBDrQtaf7rlsSDfv6KAHUjgzAIRA761Lrrp6//Yd/Rlie/yEOt9YENCoJnOcw== -"@parcel/watcher-darwin-arm64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" - integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== +"@oxc-parser/binding-android-arm64@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.132.0.tgz#ef91deec0305c54fa6c7b519f82da63d36b49788" + integrity sha512-SThDrSeamB/kG2+NxcJ5/wSLcV6dUqDknrPLqFYQ0ST/55mtBP4M7Q/f3QbubH6aAd11wpzZn/nwbVRSdobOpg== -"@parcel/watcher-darwin-x64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" - integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== +"@oxc-parser/binding-darwin-arm64@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.132.0.tgz#033a8f2789c3d09509ddd1a219dcbf2fd516125f" + integrity sha512-Lc0f/TYoKBghE5/2Gsv7bLXk+TJZunx2Tf61X8hG4ARXdc8UYI26dCGccFSd1AyFbK3jfaNXtMnupggDbjPXdQ== -"@parcel/watcher-freebsd-x64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" - integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== +"@oxc-parser/binding-darwin-x64@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.132.0.tgz#56601549bad307fcee2b3e0756769e36598841f4" + integrity sha512-RG2eJIpf7C21z9HSSXFw1bTArdpKe7Y4fwcJTwRq1yCSe1vSavaN9GA1sm9KqzemTLAGVktQ+7qBTGp0vQeUZg== -"@parcel/watcher-linux-arm-glibc@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" - integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== +"@oxc-parser/binding-freebsd-x64@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.132.0.tgz#68140dd5670556fca3aa094f0cb7e706854b5967" + integrity sha512-wQIPntPLtJ8NcBpvKPbEv3NqzV6k8eP8tP/jE9Rg8HTg/j7urZGFSsTCPCW5k77Qfw2DM4vRvc9p3I4yq/Shvw== -"@parcel/watcher-linux-arm-musl@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" - integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== +"@oxc-parser/binding-linux-arm-gnueabihf@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.132.0.tgz#84ef8af25ffb6172b02b1747bbbef668e09235c1" + integrity sha512-PixKEpeSe3yxQWqNyOCBALRYc72+Tj7ILDofUl3iXo25cVOzLA6jHUhmOINRtWIPh7dbUie3QNeabwaQpZTw6w== -"@parcel/watcher-linux-arm64-glibc@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" - integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== +"@oxc-parser/binding-linux-arm-musleabihf@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.132.0.tgz#ca6a2dffed23143c9bcbefd8250832c71fdfb4d7" + integrity sha512-sCR+DzGHlyHKnbA2z9zWjTUhIo8Sy0enJl4RDsBwPmkxYynPatpwOAWe8W5127SlW0boqUWHGtr1NWn5UwIhXQ== -"@parcel/watcher-linux-arm64-musl@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" - integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== +"@oxc-parser/binding-linux-arm64-gnu@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.132.0.tgz#ed1a4718c61d05836015c8eac7395ffe74c3f94a" + integrity sha512-sQBix5P2cW+IpzTcCwYxnh9yALrKSIkKJThspBvMGcygSMnbzkSvhN7SfuX1hvBk8y1XEChsdkU3ET0V5DmzUw== -"@parcel/watcher-linux-x64-glibc@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e" - integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A== +"@oxc-parser/binding-linux-arm64-musl@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.132.0.tgz#ae32a94bb666604728fa48c568ced5bb270d1819" + integrity sha512-WozHg3Kc//8Sk756HXXgMbEAvqtG+Lzb9JOojwQzIGDtN78Az2dLttkb71akWYUF/8IgYfDSlfKh4Uot8is5Vw== -"@parcel/watcher-linux-x64-musl@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee" - integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== +"@oxc-parser/binding-linux-ppc64-gnu@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.132.0.tgz#0de7511156b2b5d7d4fc3574ab3badd93a07c1ae" + integrity sha512-CmX/ulNBOEwWTyVRmcpYKAcAizW6+OjtLJgo7fXoL9OqQvjF4VER8tPomv44vwzfSCy1BHbsB0ZlZYzYJNj4cA== -"@parcel/watcher-win32-arm64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" - integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== +"@oxc-parser/binding-linux-riscv64-gnu@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.132.0.tgz#9a4a3b3261b6ada598b65adc4521581c45aa1003" + integrity sha512-j9oQS+hM90SdhviNGWbPgT4+Rlq+ac++q/zjgwPD1mVHgxHzATvoRGtDx0sXGmFOQ9J9YkwAhYGb5MAHL6TAsA== -"@parcel/watcher-win32-ia32@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" - integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== +"@oxc-parser/binding-linux-riscv64-musl@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.132.0.tgz#e55c1d671e41617f27535216483ccc01f1ff4a5e" + integrity sha512-bLz+Xi+Agnfmd7kWPEsSVwCn2k4EyIalZkNBcQ0OGIv9rqn8VgCPLNd03tM9mKX/5TdlvDXalz0q71BIrOPNqg== -"@parcel/watcher-win32-x64@2.5.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" - integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== +"@oxc-parser/binding-linux-s390x-gnu@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.132.0.tgz#2e4b692103d8ee745990c7ed5fd023387e6c93d9" + integrity sha512-U6t2qbJU0ypTfyj9QV3W1Y6mITDTL8ai/OR6NUn85vyHthOvobKWgXzU4tu0EskSzlpuVFz1g0jFGulDIUKHxQ== + +"@oxc-parser/binding-linux-x64-gnu@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.132.0.tgz#2ba1d08aeaed17247dac4cb5b9a3bc83b7bd7501" + integrity sha512-WcEaSNHFk8yz5YFlQQAlhq6jOFmZBB/RKE7uzhyCIf+pF1Lmv9gUH4221mle2Gd9iHyWT3ySNph8yZgb1xYdWg== + +"@oxc-parser/binding-linux-x64-musl@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.132.0.tgz#677889452adb283e791798faf70af0627bd493ad" + integrity sha512-iQrV4iJzQgRwK3BWRmQl1C3C6g3wYpXN2WLdQdyR+efoUnncdShZAVp9OgcojtlD3MDRbuOMGG3SjxF4fL4nlQ== + +"@oxc-parser/binding-openharmony-arm64@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.132.0.tgz#2928bbd0f815a7bf11a86b1bccfb0f352b92a7b3" + integrity sha512-FWzmUGrZ6GUby4U7WIwcCtab6tdmlTO3xTRRKyb5kjIJVEiaUAT8animUG/nK8ZCA8gkRkPOTId4rl6uTqUmJQ== + +"@oxc-parser/binding-wasm32-wasi@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.132.0.tgz#37df389cce33c8664763a402853a73559b882ce2" + integrity sha512-TlbMppxJI5CjWDes0QaP6G3aneVg1yikBu5QYI+DUShF9WDL66ccgKFNNGmi/Wybtszw6hxwAvv76T4DaPKnHw== + dependencies: + "@emnapi/core" "1.10.0" + "@emnapi/runtime" "1.10.0" + "@napi-rs/wasm-runtime" "^1.1.4" + +"@oxc-parser/binding-win32-arm64-msvc@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.132.0.tgz#b1a0913ad2545c30f498ba181c05de3898240976" + integrity sha512-RH/NbFjGKqdUAUi7Oh3LQPxUk2hsWFEEQ38HSnbRQT8QjBZFKqL1fMbmsB3N4jy/KPh9iX94+9dmkEMBBbambw== + +"@oxc-parser/binding-win32-ia32-msvc@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.132.0.tgz#13964f4b59671f7235f4f85866ab3db6e4afd6c5" + integrity sha512-JUr4jQY9jxoIB/YTLXr6XofSi5xikj6p5/Ns1h0VOBDT0j1jKU+kMsv2xxv51RwnETcXpA1Yw/9oUAfcqfaqEA== + +"@oxc-parser/binding-win32-x64-msvc@0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.132.0.tgz#468339fb08809ddb856f3bc51db718790fb51f05" + integrity sha512-2dapgHpA5X8DSXF4AU36hJWYf6zP0tKjMXFRAZFBD62pkevW/uhFDXoFH9Y/3Fd2EtDrw5ByNnR1wVE9X9y0SQ== + +"@oxc-project/types@=0.133.0": + version "0.133.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8" + integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA== + +"@oxc-project/types@^0.132.0": + version "0.132.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.132.0.tgz#d77243df4fe1a0a1e60e12ac6240fa898d2363ff" + integrity sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ== + +"@parcel/watcher-android-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564" + integrity sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A== + +"@parcel/watcher-darwin-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz#88d3e720b59b1eceffce98dac46d7c40e8be5e8e" + integrity sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA== + +"@parcel/watcher-darwin-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz#bf05d76a78bc15974f15ec3671848698b0838063" + integrity sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg== + +"@parcel/watcher-freebsd-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz#8bc26e9848e7303ac82922a5ae1b1ef1bdb48a53" + integrity sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng== + +"@parcel/watcher-linux-arm-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz#1328fee1deb0c2d7865079ef53a2ba4cc2f8b40a" + integrity sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ== + +"@parcel/watcher-linux-arm-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz#bad0f45cb3e2157746db8b9d22db6a125711f152" + integrity sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg== + +"@parcel/watcher-linux-arm64-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz#b75913fbd501d9523c5f35d420957bf7d0204809" + integrity sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA== + +"@parcel/watcher-linux-arm64-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz#da5621a6a576070c8c0de60dea8b46dc9c3827d4" + integrity sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA== + +"@parcel/watcher-linux-x64-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz#ce437accdc4b30f93a090b4a221fd95cd9b89639" + integrity sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ== + +"@parcel/watcher-linux-x64-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz#02400c54b4a67efcc7e2327b249711920ac969e2" + integrity sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg== + +"@parcel/watcher-win32-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz#caae3d3c7583ca0a7171e6bd142c34d20ea1691e" + integrity sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q== + +"@parcel/watcher-win32-ia32@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz#9ac922550896dfe47bfc5ae3be4f1bcaf8155d6d" + integrity sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g== + +"@parcel/watcher-win32-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz#73fdafba2e21c448f0e456bbe13178d8fe11739d" + integrity sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw== "@parcel/watcher@^2.4.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200" - integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.6.tgz#3f932828c894f06d0ad9cfefade1756ecc6ef1f1" + integrity sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ== dependencies: - detect-libc "^1.0.3" + detect-libc "^2.0.3" is-glob "^4.0.3" - micromatch "^4.0.5" node-addon-api "^7.0.0" + picomatch "^4.0.3" optionalDependencies: - "@parcel/watcher-android-arm64" "2.5.1" - "@parcel/watcher-darwin-arm64" "2.5.1" - "@parcel/watcher-darwin-x64" "2.5.1" - "@parcel/watcher-freebsd-x64" "2.5.1" - "@parcel/watcher-linux-arm-glibc" "2.5.1" - "@parcel/watcher-linux-arm-musl" "2.5.1" - "@parcel/watcher-linux-arm64-glibc" "2.5.1" - "@parcel/watcher-linux-arm64-musl" "2.5.1" - "@parcel/watcher-linux-x64-glibc" "2.5.1" - "@parcel/watcher-linux-x64-musl" "2.5.1" - "@parcel/watcher-win32-arm64" "2.5.1" - "@parcel/watcher-win32-ia32" "2.5.1" - "@parcel/watcher-win32-x64" "2.5.1" + "@parcel/watcher-android-arm64" "2.5.6" + "@parcel/watcher-darwin-arm64" "2.5.6" + "@parcel/watcher-darwin-x64" "2.5.6" + "@parcel/watcher-freebsd-x64" "2.5.6" + "@parcel/watcher-linux-arm-glibc" "2.5.6" + "@parcel/watcher-linux-arm-musl" "2.5.6" + "@parcel/watcher-linux-arm64-glibc" "2.5.6" + "@parcel/watcher-linux-arm64-musl" "2.5.6" + "@parcel/watcher-linux-x64-glibc" "2.5.6" + "@parcel/watcher-linux-x64-musl" "2.5.6" + "@parcel/watcher-win32-arm64" "2.5.6" + "@parcel/watcher-win32-ia32" "2.5.6" + "@parcel/watcher-win32-x64" "2.5.6" "@pinia/testing@1.0.3": version "1.0.3" @@ -2055,130 +2250,116 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1" integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww== -"@rolldown/pluginutils@^1.0.0-beta.9": - version "1.0.0-beta.33" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.33.tgz#ca02474d97182d0444dfd079c4e8f2c4125bf599" - integrity sha512-she25NCG6NoEPC/SEB4pHs5STcnfI4VBFOzjeI63maSPrWME5J2XC8ogrBgp8NaE/xzj28/kbpSaebiMvFRj+w== +"@publint/pack@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@publint/pack/-/pack-0.1.4.tgz#866a82a1a8ab52329ae08baec6f3969ed99a30bf" + integrity sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ== -"@rollup/pluginutils@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.2.0.tgz#eac25ca5b0bdda4ba735ddaca5fbf26bd435f602" - integrity sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw== +"@quansync/fs@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@quansync/fs/-/fs-1.0.0.tgz#17131b1f1c261fcfb63893272c488df89c73f48f" + integrity sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ== + dependencies: + quansync "^1.0.0" + +"@rolldown/binding-android-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592" + integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw== + +"@rolldown/binding-darwin-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc" + integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA== + +"@rolldown/binding-darwin-x64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad" + integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg== + +"@rolldown/binding-freebsd-x64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1" + integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g== + +"@rolldown/binding-linux-arm-gnueabihf@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f" + integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw== + +"@rolldown/binding-linux-arm64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1" + integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw== + +"@rolldown/binding-linux-arm64-musl@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069" + integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q== + +"@rolldown/binding-linux-ppc64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8" + integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg== + +"@rolldown/binding-linux-s390x-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763" + integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg== + +"@rolldown/binding-linux-x64-gnu@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7" + integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg== + +"@rolldown/binding-linux-x64-musl@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3" + integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow== + +"@rolldown/binding-openharmony-arm64@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309" + integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg== + +"@rolldown/binding-wasm32-wasi@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b" + integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg== + dependencies: + "@emnapi/core" "1.10.0" + "@emnapi/runtime" "1.10.0" + "@napi-rs/wasm-runtime" "^1.1.4" + +"@rolldown/binding-win32-arm64-msvc@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad" + integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g== + +"@rolldown/binding-win32-x64-msvc@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb" + integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA== + +"@rolldown/debug@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@rolldown/debug/-/debug-1.0.3.tgz#6e818fc76cf3385068decf18ada5061876658f66" + integrity sha512-mQ4V7ODNxW4o09we34Dw9I29ByK/m7yTHT8Nqt+wwWCcxPsiGoixUsFDiruxGQwrjk6XYgwi/Cf0Prg0x5ABsA== + +"@rolldown/pluginutils@^1.0.0", "@rolldown/pluginutils@^1.0.0-rc.2", "@rolldown/pluginutils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" + integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== + +"@rollup/pluginutils@^5.3.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.4.0.tgz#ac23a29ced0247060a210815fca39c17de4d2f26" + integrity sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg== dependencies: "@types/estree" "^1.0.0" estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-android-arm-eabi@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz#0f44a2f8668ed87b040b6fe659358ac9239da4db" - integrity sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ== - -"@rollup/rollup-android-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz#25b9a01deef6518a948431564c987bcb205274f5" - integrity sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA== - -"@rollup/rollup-darwin-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz#8a102869c88f3780c7d5e6776afd3f19084ecd7f" - integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA== - -"@rollup/rollup-darwin-x64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz#8e526417cd6f54daf1d0c04cf361160216581956" - integrity sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA== - -"@rollup/rollup-freebsd-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz#0e7027054493f3409b1f219a3eac5efd128ef899" - integrity sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA== - -"@rollup/rollup-freebsd-x64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz#72b204a920139e9ec3d331bd9cfd9a0c248ccb10" - integrity sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ== - -"@rollup/rollup-linux-arm-gnueabihf@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz#ab1b522ebe5b7e06c99504cc38f6cd8b808ba41c" - integrity sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ== - -"@rollup/rollup-linux-arm-musleabihf@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz#f8cc30b638f1ee7e3d18eac24af47ea29d9beb00" - integrity sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ== - -"@rollup/rollup-linux-arm64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz#7af37a9e85f25db59dc8214172907b7e146c12cc" - integrity sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg== - -"@rollup/rollup-linux-arm64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz#a623eb0d3617c03b7a73716eb85c6e37b776f7e0" - integrity sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q== - -"@rollup/rollup-linux-loong64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz#76ea038b549c5c6c5f0d062942627c4066642ee2" - integrity sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA== - -"@rollup/rollup-linux-ppc64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz#d9a4c3f0a3492bc78f6fdfe8131ac61c7359ccd5" - integrity sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw== - -"@rollup/rollup-linux-riscv64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz#87ab033eebd1a9a1dd7b60509f6333ec1f82d994" - integrity sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw== - -"@rollup/rollup-linux-riscv64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz#bda3eb67e1c993c1ba12bc9c2f694e7703958d9f" - integrity sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg== - -"@rollup/rollup-linux-s390x-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz#f7bc10fbe096ab44694233dc42a2291ed5453d4b" - integrity sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ== - -"@rollup/rollup-linux-x64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz#a151cb1234cc9b2cf5e8cfc02aa91436b8f9e278" - integrity sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q== - -"@rollup/rollup-linux-x64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz#7859e196501cc3b3062d45d2776cfb4d2f3a9350" - integrity sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg== - -"@rollup/rollup-openharmony-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz#85d0df7233734df31e547c1e647d2a5300b3bf30" - integrity sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw== - -"@rollup/rollup-win32-arm64-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz#e62357d00458db17277b88adbf690bb855cac937" - integrity sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w== - -"@rollup/rollup-win32-ia32-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz#fc7cd40f44834a703c1f1c3fe8bcc27ce476cd50" - integrity sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg== - -"@rollup/rollup-win32-x64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz#1a22acfc93c64a64a48c42672e857ee51774d0d3" - integrity sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ== - -"@rollup/rollup-win32-x64-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz#1657f56326bbe0ac80eedc9f9c18fc1ddd24e107" - integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg== - "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -2212,51 +2393,47 @@ lodash.get "^4.4.2" type-detect "^4.1.0" +"@standard-schema/spec@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== + "@testim/chrome-version@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.4.tgz#86e04e677cd6c05fa230dd15ac223fa72d1d7090" integrity sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g== -"@testing-library/dom@^10.4.0": - version "10.4.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" - integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^5.0.1" - aria-query "5.3.0" - chalk "^4.1.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.5.0" - pretty-format "^27.0.2" - -"@testing-library/user-event@^14.6.1": - version "14.6.1" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" - integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== - "@tootallnate/quickjs-emscripten@^0.23.0": version "0.23.0" resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== -"@types/aria-query@^5.0.1": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" - integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== +"@tybys/wasm-util@^0.10.1": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz#12b3a1b33db1f9cad4ddff1f604ab7dd00bf464e" + integrity sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg== + dependencies: + tslib "^2.4.0" "@types/chai@^4.3.5": version "4.3.20" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== -"@types/cookie@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" - integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" -"@types/estree@1.0.8", "@types/estree@^1.0.0": +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + +"@types/estree@^1.0.0": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -2298,15 +2475,17 @@ "@types/node" "*" "@types/ws" "*" -"@types/statuses@^2.0.4": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.5.tgz#f61ab46d5352fd73c863a1ea4e1cef3b0b51ae63" - integrity sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A== +"@types/set-cookie-parser@^2.4.10": + version "2.4.10" + resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz#ad3a807d6d921db9720621ea3374c5d92020bcbc" + integrity sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw== + dependencies: + "@types/node" "*" -"@types/tough-cookie@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" - integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== +"@types/statuses@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa" + integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA== "@types/ws@*": version "8.18.1" @@ -2327,106 +2506,186 @@ resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.2.4.tgz#8b083a62ee665228bac08013fa516a3488528bb8" integrity sha512-u9Fd3k2qfMtn+0dxbCn/y0pzQ9Ucw6lWR984CrHcbxc+WzcMkJE4VjWHWSb9At40MjwMyHCkJNXroS55Osshhw== -"@vitejs/plugin-vue-jsx@^4.1.1": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.2.0.tgz#2738ec05d4705ed553a107342017192e37351640" - integrity sha512-DSTrmrdLp+0LDNF77fqrKfx7X0ErRbOcUAgJL/HbSesqQwoUvUQ4uYQqaex+rovqgGcoPqVk+AwUh3v9CuiYIw== - dependencies: - "@babel/core" "^7.27.1" - "@babel/plugin-transform-typescript" "^7.27.1" - "@rolldown/pluginutils" "^1.0.0-beta.9" - "@vue/babel-plugin-jsx" "^1.4.0" +"@valibot/to-json-schema@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@valibot/to-json-schema/-/to-json-schema-1.7.0.tgz#a0519849f78d180939befac82a218745b55a913a" + integrity sha512-Y3pPVibbIOHzohrlxSINvO7w/bvXkoYS3BQHoImV9ynE+bXKf171bdMucPurV2zp7gdmt0L1HCcNAsbo7cFRQw== -"@vitejs/plugin-vue@^5.2.1": - version "5.2.4" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8" - integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA== - -"@vitest/browser@^3.0.7": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/browser/-/browser-3.1.3.tgz#985f12382bc4aeddbffa4209850ab5cbaaa43e60" - integrity sha512-Dgyez9LbHJHl9ObZPo5mu4zohWLo7SMv8zRWclMF+dxhQjmOtEP0raEX13ac5ygcvihNoQPBZXdya5LMSbcCDQ== +"@vitejs/devtools-kit@0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@vitejs/devtools-kit/-/devtools-kit-0.3.1.tgz#03818777913d97d2419990fa2384d2217487870d" + integrity sha512-0zwX4IpFMbNWsiDMj/WnRZFdJU+zY8gU/uBf2jr5UktDicmwL+6yVZRF5zgOA6XZ3yj4+TLSdWQfVlaMerBWaw== dependencies: - "@testing-library/dom" "^10.4.0" - "@testing-library/user-event" "^14.6.1" - "@vitest/mocker" "3.1.3" - "@vitest/utils" "3.1.3" - magic-string "^0.30.17" - sirv "^3.0.1" - tinyrainbow "^2.0.0" - ws "^8.18.1" + "@devframes/hub" "^0.5.2" + birpc "^4.0.0" + devframe "^0.5.2" + mlly "^1.8.2" + nostics "^0.2.0" + pathe "^2.0.3" + perfect-debounce "^2.1.0" + tinyexec "^1.2.2" -"@vitest/expect@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.1.3.tgz#bbca175cd2f23d7de9448a215baed8f3d7abd7b7" - integrity sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg== +"@vitejs/devtools-rolldown@0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@vitejs/devtools-rolldown/-/devtools-rolldown-0.3.1.tgz#a866090dcaf38c08d071df22ebe0b22aef54ac37" + integrity sha512-yrlrezS7xaR/nxRRTqsJevUPeZOWIQKX3wwK38zGsEEEMn8oyls8DBmYVagtXNPqUk9XW9bt5sVIsrR2n2F8+w== dependencies: - "@vitest/spy" "3.1.3" - "@vitest/utils" "3.1.3" - chai "^5.2.0" - tinyrainbow "^2.0.0" + "@floating-ui/dom" "^1.7.6" + "@rolldown/debug" "^1.0.3" + "@vitejs/devtools-kit" "0.3.1" + birpc "^4.0.0" + cac "^7.0.0" + d3-shape "^3.2.0" + devframe "^0.5.2" + diff "^9.0.0" + get-port-please "^3.2.0" + h3 "2.0.1-rc.22" + mlly "^1.8.2" + mrmime "^2.0.1" + nostics "^0.2.0" + p-limit "^7.3.0" + pathe "^2.0.3" + publint "^0.3.21" + tinyglobby "^0.2.16" + unconfig "^7.5.0" + unstorage "^1.17.5" + vue-virtual-scroller "^3.0.4" + ws "^8.21.0" -"@vitest/mocker@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.1.3.tgz#121d0f2fcca20c9ccada9e2d6e761f7fc687f4ce" - integrity sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ== +"@vitejs/devtools@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@vitejs/devtools/-/devtools-0.3.1.tgz#58c6d8924038f8385f170a0d049c7871ab8344b0" + integrity sha512-uRgicpM7gzCJ4dHzs717uLvzvw2sdnVzxp7bui/cyezWyILjc0DYlPlFEwS2kIFLOnnQNGeryRbs/M96C7Ts8Q== dependencies: - "@vitest/spy" "3.1.3" + "@devframes/hub" "^0.5.2" + "@vitejs/devtools-kit" "0.3.1" + "@vitejs/devtools-rolldown" "0.3.1" + birpc "^4.0.0" + cac "^7.0.0" + devframe "^0.5.2" + h3 "2.0.1-rc.22" + mlly "^1.8.2" + nostics "^0.2.0" + obug "^2.1.1" + pathe "^2.0.3" + perfect-debounce "^2.1.0" + tinyexec "^1.2.2" + vue "^3.5.35" + ws "^8.21.0" + +"@vitejs/plugin-vue-jsx@^5.1.5": + version "5.1.5" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-5.1.5.tgz#23b9aa23e55fc81c40b3dab81728339c1f0d177f" + integrity sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA== + dependencies: + "@babel/core" "^7.29.0" + "@babel/plugin-syntax-typescript" "^7.28.6" + "@babel/plugin-transform-typescript" "^7.28.6" + "@rolldown/pluginutils" "^1.0.0-rc.2" + "@vue/babel-plugin-jsx" "^2.0.1" + +"@vitejs/plugin-vue@^6.0.7": + version "6.0.7" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz#194235d364a2c601c521b0410e524e521119059f" + integrity sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg== + dependencies: + "@rolldown/pluginutils" "^1.0.1" + +"@vitest/browser-playwright@^4.1.7": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/browser-playwright/-/browser-playwright-4.1.9.tgz#845e65017dfed8aff59931f91016e7595b8f5c1d" + integrity sha512-Bq1rOGf9waevzG3EOkO/dene6bvKTUsZMVg8S1i+WH3JcMjuXEjiahP9rAqZRELUqjBySOJsvvSWqK/B3wjKQw== + dependencies: + "@vitest/browser" "4.1.9" + "@vitest/mocker" "4.1.9" + tinyrainbow "^3.1.0" + +"@vitest/browser@4.1.9", "@vitest/browser@^4.1.7": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/browser/-/browser-4.1.9.tgz#838c5f215f4015089979cf49f930cd3db2888461" + integrity sha512-j1BKtWmPcqpMhmx/L9EPLgAJpCb0zKfwoWLmqBbxaogCXHjOwHFSEoHCBfnGtx93xKQwilZ26m+UOsHqHMkRNg== + dependencies: + "@blazediff/core" "1.9.1" + "@vitest/mocker" "4.1.9" + "@vitest/utils" "4.1.9" + magic-string "^0.30.21" + pngjs "^7.0.0" + sirv "^3.0.2" + tinyrainbow "^3.1.0" + ws "^8.19.0" + +"@vitest/expect@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.9.tgz#ba1af73ae53262e3dc9b518cb7b76fb614e0ef53" + integrity sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA== + dependencies: + "@standard-schema/spec" "^1.1.0" + "@types/chai" "^5.2.2" + "@vitest/spy" "4.1.9" + "@vitest/utils" "4.1.9" + chai "^6.2.2" + tinyrainbow "^3.1.0" + +"@vitest/mocker@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.9.tgz#a483de79b358aba3dd8f319a0d8ab17c89f5c75d" + integrity sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw== + dependencies: + "@vitest/spy" "4.1.9" estree-walker "^3.0.3" - magic-string "^0.30.17" + magic-string "^0.30.21" -"@vitest/pretty-format@3.1.3", "@vitest/pretty-format@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.1.3.tgz#760b9eab5f253d7d2e7dcd28ef34570f584023d4" - integrity sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA== +"@vitest/pretty-format@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.9.tgz#885cfe9fcb6ff3df4409ea66192cc1fb23d62fae" + integrity sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A== dependencies: - tinyrainbow "^2.0.0" + tinyrainbow "^3.1.0" -"@vitest/runner@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.1.3.tgz#b268fa90fca38fab363f1107f057c0a2a141ee45" - integrity sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA== +"@vitest/runner@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.9.tgz#bb742947ce4841dfb2d8984a2f9014850be10f51" + integrity sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg== dependencies: - "@vitest/utils" "3.1.3" + "@vitest/utils" "4.1.9" pathe "^2.0.3" -"@vitest/snapshot@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.1.3.tgz#39a8f9f8c6ba732ffde59adeacf0a549bef11e76" - integrity sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ== +"@vitest/snapshot@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.9.tgz#bdfb670ae5617613ea8776e93d0666a66defeeb7" + integrity sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA== dependencies: - "@vitest/pretty-format" "3.1.3" - magic-string "^0.30.17" + "@vitest/pretty-format" "4.1.9" + "@vitest/utils" "4.1.9" + magic-string "^0.30.21" pathe "^2.0.3" -"@vitest/spy@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.1.3.tgz#ca81e2b4f0c3d6c75f35defa77c3336f39c8f605" - integrity sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ== - dependencies: - tinyspy "^3.0.2" +"@vitest/spy@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.9.tgz#bfc40d48fb9bd1a1228bfbfde7f5555e7f6b3867" + integrity sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA== -"@vitest/ui@^3.0.7": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-3.1.3.tgz#ad3c3160e6c86d79f817e09b1f8f02f0e2799851" - integrity sha512-IipSzX+8DptUdXN/GWq3hq5z18MwnpphYdOMm0WndkRGYELzfq7NDP8dMpZT7JGW1uXFrIGxOW2D0Xi++ulByg== +"@vitest/ui@^4.1.7": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-4.1.9.tgz#8aeb5af7295ea04ef1064873334ced04ce20d646" + integrity sha512-U/cRvtqfEPj27FI1n9cyUvi4vXXdcLhjJiI+InYKdk8hP4VrS6RXOjGL7rfFaeBc37iRKANsR6eEzIoC7lmgBQ== dependencies: - "@vitest/utils" "3.1.3" + "@vitest/utils" "4.1.9" fflate "^0.8.2" - flatted "^3.3.3" + flatted "^3.4.2" pathe "^2.0.3" - sirv "^3.0.1" - tinyglobby "^0.2.13" - tinyrainbow "^2.0.0" + sirv "^3.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.1.0" -"@vitest/utils@3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.1.3.tgz#4f31bdfd646cd82d30bfa730d7410cb59d529669" - integrity sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg== +"@vitest/utils@4.1.9": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.9.tgz#0184c7e6eb3234739b2b6b3b985f78d1ed823ee1" + integrity sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA== dependencies: - "@vitest/pretty-format" "3.1.3" - loupe "^3.1.3" - tinyrainbow "^2.0.0" + "@vitest/pretty-format" "4.1.9" + convert-source-map "^2.0.0" + tinyrainbow "^3.1.0" "@vue/babel-helper-vue-jsx-merge-props@1.4.0": version "1.4.0" @@ -2438,7 +2697,12 @@ resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz#b7e99d37eeb144d7b9757d7a1f40cd977fde748a" integrity sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA== -"@vue/babel-plugin-jsx@1.5.0", "@vue/babel-plugin-jsx@^1.4.0": +"@vue/babel-helper-vue-transform-on@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-2.0.1.tgz#3cadaa769fda53b61f193ab63668ccc5c7dfe244" + integrity sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA== + +"@vue/babel-plugin-jsx@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz#1b988b497cb1f79725da94463e75cebe60b72e70" integrity sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw== @@ -2453,6 +2717,21 @@ "@vue/babel-plugin-resolve-type" "1.5.0" "@vue/shared" "^3.5.18" +"@vue/babel-plugin-jsx@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-2.0.1.tgz#5ee72f05d89d82dc8030df6d826c1efd54d3604b" + integrity sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.4" + "@babel/types" "^7.28.4" + "@vue/babel-helper-vue-transform-on" "2.0.1" + "@vue/babel-plugin-resolve-type" "2.0.1" + "@vue/shared" "^3.5.22" + "@vue/babel-plugin-resolve-type@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz#6881d7b1478e9fc0ea4bb08aaad1f4d206655568" @@ -2464,6 +2743,17 @@ "@babel/parser" "^7.28.0" "@vue/compiler-sfc" "^3.5.18" +"@vue/babel-plugin-resolve-type@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-2.0.1.tgz#4a191a0139a1bc106dae560abebf342bdeef5639" + integrity sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/parser" "^7.28.4" + "@vue/compiler-sfc" "^3.5.22" + "@vue/compiler-core@3.5.19": version "3.5.19" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.19.tgz#f141d35b61b55ce72c3cbb4dc9eeca3821d451aa" @@ -2486,6 +2776,17 @@ estree-walker "^2.0.2" source-map-js "^1.2.1" +"@vue/compiler-core@3.5.35": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.35.tgz#e789c89b0fee47683e5300c715bb9243c07a6bbd" + integrity sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw== + dependencies: + "@babel/parser" "^7.29.3" + "@vue/shared" "3.5.35" + entities "^7.0.1" + estree-walker "^2.0.2" + source-map-js "^1.2.1" + "@vue/compiler-dom@3.5.19": version "3.5.19" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.19.tgz#fc57d9dca4987df67d6ce64dbddaac6d73f1a6ef" @@ -2502,6 +2803,14 @@ "@vue/compiler-core" "3.5.22" "@vue/shared" "3.5.22" +"@vue/compiler-dom@3.5.35": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz#523a42dcc49af499588c51e3db52e672b9491325" + integrity sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA== + dependencies: + "@vue/compiler-core" "3.5.35" + "@vue/shared" "3.5.35" + "@vue/compiler-sfc@3.5.22": version "3.5.22" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz#663a8483b1dda8de83b6fa1aab38a52bf73dd965" @@ -2517,6 +2826,21 @@ postcss "^8.5.6" source-map-js "^1.2.1" +"@vue/compiler-sfc@3.5.35", "@vue/compiler-sfc@^3.5.22": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz#e0310f48a37cf326b5c926ef0006d75c87e34d33" + integrity sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw== + dependencies: + "@babel/parser" "^7.29.3" + "@vue/compiler-core" "3.5.35" + "@vue/compiler-dom" "3.5.35" + "@vue/compiler-ssr" "3.5.35" + "@vue/shared" "3.5.35" + estree-walker "^2.0.2" + magic-string "^0.30.21" + postcss "^8.5.15" + source-map-js "^1.2.1" + "@vue/compiler-sfc@^3.5.18": version "3.5.19" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.19.tgz#7f9792ad7de5d4be9b6a32129c75e1f6cd4da015" @@ -2548,24 +2872,32 @@ "@vue/compiler-dom" "3.5.22" "@vue/shared" "3.5.22" +"@vue/compiler-ssr@3.5.35": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz#206cfc3d741c43d605ef0629509c00166022783b" + integrity sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw== + dependencies: + "@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": version "6.6.4" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== -"@vue/devtools-api@^7.7.2": - version "7.7.7" - resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz#5ef5f55f60396220725a273548c0d7ee983d5d34" - integrity sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg== +"@vue/devtools-api@^7.7.7": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz#999dbea50da6b00cf59a1336f11fdc2b43d9e063" + integrity sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g== dependencies: - "@vue/devtools-kit" "^7.7.7" + "@vue/devtools-kit" "^7.7.9" -"@vue/devtools-kit@^7.7.7": - version "7.7.7" - resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz#41a64f9526e9363331c72405544df020ce2e3641" - integrity sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA== +"@vue/devtools-kit@^7.7.9": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz#bc218a815616e8987df7ab3e10fc1fb3b8706c58" + integrity sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA== dependencies: - "@vue/devtools-shared" "^7.7.7" + "@vue/devtools-shared" "^7.7.9" birpc "^2.3.0" hookable "^5.5.3" mitt "^3.0.1" @@ -2573,10 +2905,10 @@ speakingurl "^14.0.1" superjson "^2.2.2" -"@vue/devtools-shared@^7.7.7": - version "7.7.7" - resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz#ff14aa8c1262ebac8c0397d3b09f767cd489750c" - integrity sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw== +"@vue/devtools-shared@^7.7.9": + version "7.7.9" + resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz#fa4c096b744927081a7dda5fcf05f34b1ae6ca14" + integrity sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA== dependencies: rfdc "^1.4.1" @@ -2587,6 +2919,13 @@ dependencies: "@vue/shared" "3.5.22" +"@vue/reactivity@3.5.35": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.35.tgz#45b9794ca77ff5ade2eee74bcdeacbc00ae8e7a2" + integrity sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ== + dependencies: + "@vue/shared" "3.5.35" + "@vue/runtime-core@3.5.22": version "3.5.22" resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.22.tgz#e004c1e35f423555a0e4c10646ef3e9d380643d1" @@ -2595,6 +2934,14 @@ "@vue/reactivity" "3.5.22" "@vue/shared" "3.5.22" +"@vue/runtime-core@3.5.35": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.35.tgz#6f16679bc95ffe38af38c5d0bc1414ed4277458e" + integrity sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg== + dependencies: + "@vue/reactivity" "3.5.35" + "@vue/shared" "3.5.35" + "@vue/runtime-dom@3.5.22": version "3.5.22" resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz#01276cea7cb9ac2b9aba046adfb5903b494e2e7e" @@ -2605,6 +2952,16 @@ "@vue/shared" "3.5.22" csstype "^3.1.3" +"@vue/runtime-dom@3.5.35": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz#473f0fbf97b82652780ac9e6bb5873f7c0dde7e7" + integrity sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA== + dependencies: + "@vue/reactivity" "3.5.35" + "@vue/runtime-core" "3.5.35" + "@vue/shared" "3.5.35" + csstype "^3.2.3" + "@vue/server-renderer@3.5.22": version "3.5.22" resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.22.tgz#d134e3409094044bd066d9803714677457756157" @@ -2613,6 +2970,14 @@ "@vue/compiler-ssr" "3.5.22" "@vue/shared" "3.5.22" +"@vue/server-renderer@3.5.35": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.35.tgz#31c7b473de1000472444a0c0a336d1e799b209c3" + integrity sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA== + dependencies: + "@vue/compiler-ssr" "3.5.35" + "@vue/shared" "3.5.35" + "@vue/shared@3.5.19", "@vue/shared@^3.5.18": version "3.5.19" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.19.tgz#5301967a910cb62145e4f17131f3bee88b463c83" @@ -2623,6 +2988,11 @@ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.22.tgz#9d56a1644a3becb8af1e34655928b0e288d827f8" integrity sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w== +"@vue/shared@3.5.35", "@vue/shared@^3.5.22": + version "3.5.35" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.35.tgz#192eb3d720c40715db79313454c4937432a4e86d" + integrity sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA== + "@vue/test-utils@2.4.6": version "2.4.6" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.4.6.tgz#7d534e70c4319d2a587d6a3b45a39e9695ade03c" @@ -2678,6 +3048,11 @@ acorn@^8.15.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== +acorn@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + agent-base@^7.1.0, agent-base@^7.1.2: version "7.1.3" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" @@ -2715,13 +3090,6 @@ ansi-colors@4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-escapes@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - ansi-regex@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" @@ -2751,11 +3119,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" @@ -2768,7 +3131,7 @@ ansi-to-html@0.7.2: dependencies: entities "^2.2.0" -anymatch@~3.1.2: +anymatch@^3.1.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -2833,13 +3196,6 @@ aria-query@5.1.3: dependencies: deep-equal "^2.0.5" -aria-query@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" - array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" @@ -3048,6 +3404,11 @@ birpc@^2.3.0: resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.4.0.tgz#045368a4a30d659c6c06c9215b11cb384903249c" integrity sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg== +birpc@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/birpc/-/birpc-4.0.0.tgz#cceef485926b93496735201896d86c3a182ad30f" + integrity sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw== + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -3176,10 +3537,10 @@ bytes@3.1.2, bytes@^3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -cac@^6.7.14: - version "6.7.14" - resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" - integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== +cac@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cac/-/cac-7.0.0.tgz#7dda83da2268f75f840ab89ac3bcc36c120a78da" + integrity sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ== cacheable@^2.0.3: version "2.0.3" @@ -3266,16 +3627,10 @@ chai@5.3.3: loupe "^3.1.0" pathval "^2.0.0" -chai@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-5.2.0.tgz#1358ee106763624114addf84ab02697e411c9c05" - integrity sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw== - dependencies: - assertion-error "^2.0.1" - check-error "^2.1.1" - deep-eql "^5.0.1" - loupe "^3.1.0" - pathval "^2.0.0" +chai@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e" + integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== chalk@2.4.2, chalk@^2.4.2: version "2.4.2" @@ -3324,12 +3679,12 @@ chokidar@3.5.3: optionalDependencies: fsevents "~2.3.2" -chokidar@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" - integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== dependencies: - readdirp "^4.0.1" + readdirp "^5.0.0" chromatism@3.0.0: version "3.0.0" @@ -3460,6 +3815,11 @@ colord@^2.9.3: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== +colorjs.io@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/colorjs.io/-/colorjs.io-0.5.2.tgz#63b20139b007591ebc3359932bef84628eb3fcef" + integrity sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -3497,6 +3857,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + config-chain@^1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -3527,16 +3892,26 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-es@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-1.2.3.tgz#06ca3c5f5f3531684a2059666a361173f74a89c8" + integrity sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw== + cookie-signature@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@^0.7.1, cookie@^0.7.2: +cookie@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +cookie@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + copy-anything@^3.0.2: version "3.0.5" resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0" @@ -3596,6 +3971,13 @@ cross-spawn@7.0.6, cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" +crossws@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.3.5.tgz#daad331d44148ea6500098bc858869f3a5ab81a6" + integrity sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA== + dependencies: + uncrypto "^0.1.3" + css-functions-list@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.3.tgz#95652b0c24f0f59b291a9fc386041a19d4f40dbe" @@ -3627,11 +4009,28 @@ csstype@^3.1.3: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +csstype@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + custom-event-polyfill@1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee" integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w== +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-shape@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + data-uri-to-buffer@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" @@ -3800,6 +4199,11 @@ define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +defu@^6.1.4, defu@^6.1.6: + version "6.1.7" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.7.tgz#72543567c8e9f97ff13ce402b6dbe09ac5ae4d23" + integrity sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ== + degenerator@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" @@ -3819,15 +4223,30 @@ depd@2.0.0, depd@^2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -dequal@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +destr@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb" + integrity sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA== -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +devframe@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/devframe/-/devframe-0.5.2.tgz#bc709c0d780f98593081c8053da346f79d700f9a" + integrity sha512-8dIdlOmuY+6NcCsaI2qS0uRLTZ3SvpejY8OYVbXvdWSQV7pvjdWaYNZhVfOfCSd/a5dSCgSge4vW4DCyJSf7+g== + dependencies: + "@valibot/to-json-schema" "^1.7.0" + birpc "^4.0.0" + cac "^7.0.0" + h3 "2.0.1-rc.22" + mrmime "^2.0.1" + nostics "^0.2.0" + pathe "^2.0.3" + valibot "^1.4.1" + ws "^8.21.0" devtools-protocol@^0.0.1140464: version "0.0.1140464" @@ -3849,6 +4268,11 @@ diff@^7.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== +diff@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-9.0.0.tgz#297c31cd7c280f13dfe335791ec2063bd4a73a6f" + integrity sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw== + dijkstrajs@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" @@ -3868,11 +4292,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.9: - version "0.5.16" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" - integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== - dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -3994,6 +4413,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@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" + integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== + env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -4153,10 +4577,10 @@ es-get-iterator@^1.1.3: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" -es-module-lexer@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" - integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== +es-module-lexer@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz#1dfcbb5ea3bbfb63f28e1fc3676c3676d1c9624c" + integrity sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" @@ -4191,38 +4615,6 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" -esbuild@^0.25.0: - version "0.25.11" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.11.tgz#0f31b82f335652580f75ef6897bba81962d9ae3d" - integrity sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q== - optionalDependencies: - "@esbuild/aix-ppc64" "0.25.11" - "@esbuild/android-arm" "0.25.11" - "@esbuild/android-arm64" "0.25.11" - "@esbuild/android-x64" "0.25.11" - "@esbuild/darwin-arm64" "0.25.11" - "@esbuild/darwin-x64" "0.25.11" - "@esbuild/freebsd-arm64" "0.25.11" - "@esbuild/freebsd-x64" "0.25.11" - "@esbuild/linux-arm" "0.25.11" - "@esbuild/linux-arm64" "0.25.11" - "@esbuild/linux-ia32" "0.25.11" - "@esbuild/linux-loong64" "0.25.11" - "@esbuild/linux-mips64el" "0.25.11" - "@esbuild/linux-ppc64" "0.25.11" - "@esbuild/linux-riscv64" "0.25.11" - "@esbuild/linux-s390x" "0.25.11" - "@esbuild/linux-x64" "0.25.11" - "@esbuild/netbsd-arm64" "0.25.11" - "@esbuild/netbsd-x64" "0.25.11" - "@esbuild/openbsd-arm64" "0.25.11" - "@esbuild/openbsd-x64" "0.25.11" - "@esbuild/openharmony-arm64" "0.25.11" - "@esbuild/sunos-x64" "0.25.11" - "@esbuild/win32-arm64" "0.25.11" - "@esbuild/win32-ia32" "0.25.11" - "@esbuild/win32-x64" "0.25.11" - escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -4539,10 +4931,10 @@ execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -expect-type@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.1.tgz#af76d8b357cf5fa76c41c09dafb79c549e75f71f" - integrity sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw== +expect-type@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" + integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== express@5.1.0: version "5.1.0" @@ -4619,11 +5011,30 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-string-truncated-width@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz#23afe0da67d752ca0727538f1e6967759728ce49" + integrity sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g== + +fast-string-width@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-string-width/-/fast-string-width-3.0.2.tgz#16dbabb491ce5585b5ecb675b65c165d71688eeb" + integrity sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg== + dependencies: + fast-string-truncated-width "^3.0.2" + fast-uri@^3.0.1: version "3.0.6" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== +fast-wrap-ansi@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz#95e952a0145bce3f59ad56e179f84c48d4072935" + integrity sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q== + dependencies: + fast-string-width "^3.0.2" + fastest-levenshtein@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" @@ -4643,7 +5054,7 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fdir@^6.4.4, fdir@^6.5.0: +fdir@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== @@ -4752,6 +5163,11 @@ flatted@^3.2.9, flatted@^3.3.3: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== +flatted@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== + follow-redirects@^1.0.0, follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" @@ -4870,6 +5286,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.2, get-intrinsic@^1.2.4, get-intrinsic@ hasown "^2.0.2" math-intrinsics "^1.1.0" +get-port-please@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.2.0.tgz#0ce3cee194c448ac640ec39dc357a500f5d7d2bb" + integrity sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A== + get-proto@^1.0.0, get-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" @@ -5040,10 +5461,33 @@ graceful-fs@^4.2.0, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphql@^16.8.1: - version "16.10.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" - integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== +graphql@^16.13.2: + version "16.14.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.14.2.tgz#83faf25869e3df727cc855161db5da85b0e5b2c0" + integrity sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA== + +h3@2.0.1-rc.22: + version "2.0.1-rc.22" + resolved "https://registry.yarnpkg.com/h3/-/h3-2.0.1-rc.22.tgz#12c94d2f99e9afa42b490ebfda6163035e98be2a" + integrity sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA== + dependencies: + rou3 "^0.8.1" + srvx "^0.11.15" + +h3@^1.15.10: + version "1.15.11" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.15.11.tgz#831179fc6b4bc06de8ad1077e7a5c7d63b796577" + integrity sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg== + dependencies: + cookie-es "^1.2.3" + crossws "^0.3.5" + defu "^6.1.6" + destr "^2.0.5" + iron-webcrypto "^1.2.1" + node-mock-http "^1.0.4" + radix3 "^1.1.2" + ufo "^1.6.3" + uncrypto "^0.1.3" has-bigints@^1.0.2: version "1.1.0" @@ -5103,10 +5547,13 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -headers-polyfill@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07" - integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ== +headers-polyfill@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-5.0.1.tgz#9554eb2892b666db1c7a3380a91b6cfd467a6b19" + integrity sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA== + dependencies: + "@types/set-cookie-parser" "^2.4.10" + set-cookie-parser "^3.0.1" hookable@^5.5.3: version "5.5.3" @@ -5220,10 +5667,10 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -immutable@^5.0.2: - version "5.1.1" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.1.tgz#d4cb552686f34b076b3dcf23c4384c04424d8354" - integrity sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg== +immutable@^5.1.5: + version "5.1.6" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.6.tgz#21639bc80f9a0713e141a5f5a154ef9fdabf36dd" + integrity sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ== import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.1" @@ -5283,6 +5730,11 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +iron-webcrypto@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz#aa60ff2aa10550630f4c0b11fd2442becdb35a6f" + integrity sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg== + is-arguments@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" @@ -5612,6 +6064,11 @@ jake@^10.8.5: filelist "^1.0.4" minimatch "^3.1.2" +jiti@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64" + integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== + js-beautify@^1.14.9: version "1.15.4" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.4.tgz#f579f977ed4c930cef73af8f98f3f0a608acd51e" @@ -5792,6 +6249,80 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" +lightningcss-android-arm64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" + integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== + +lightningcss-darwin-arm64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5" + integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== + +lightningcss-darwin-x64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e" + integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w== + +lightningcss-freebsd-x64@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575" + integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig== + +lightningcss-linux-arm-gnueabihf@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d" + integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw== + +lightningcss-linux-arm64-gnu@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335" + integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== + +lightningcss-linux-arm64-musl@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133" + integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg== + +lightningcss-linux-x64-gnu@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6" + integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA== + +lightningcss-linux-x64-musl@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b" + integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg== + +lightningcss-win32-arm64-msvc@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38" + integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw== + +lightningcss-win32-x64-msvc@1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a" + integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== + +lightningcss@^1.32.0: + version "1.32.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9" + integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-android-arm64 "1.32.0" + lightningcss-darwin-arm64 "1.32.0" + lightningcss-darwin-x64 "1.32.0" + lightningcss-freebsd-x64 "1.32.0" + lightningcss-linux-arm-gnueabihf "1.32.0" + lightningcss-linux-arm64-gnu "1.32.0" + lightningcss-linux-arm64-musl "1.32.0" + lightningcss-linux-x64-gnu "1.32.0" + lightningcss-linux-x64-musl "1.32.0" + lightningcss-win32-arm64-msvc "1.32.0" + lightningcss-win32-x64-msvc "1.32.0" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -5891,7 +6422,7 @@ loupe@^2.3.7: dependencies: get-func-name "^2.0.1" -loupe@^3.1.0, loupe@^3.1.3: +loupe@^3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== @@ -5901,6 +6432,11 @@ lru-cache@^10.2.0, lru-cache@^10.4.3: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^11.2.7: + version "11.5.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.5.1.tgz#f3daa3540847b9737ebc02499ddb36765e54db4a" + integrity sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -5920,11 +6456,6 @@ lru-cache@^7.14.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -lz-string@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" - integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== - magic-string@^0.30.17: version "0.30.18" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.18.tgz#905bfbbc6aa5692703a93db26a9edcaa0007d2bb" @@ -5939,6 +6470,13 @@ magic-string@^0.30.19: dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -5992,7 +6530,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.5, micromatch@^4.0.8: +micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -6089,6 +6627,16 @@ mitt@^3.0.1: resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== +mlly@^1.7.4, mlly@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.8.2.tgz#e7f7919a82d13b174405613117249a3f449d78bb" + integrity sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA== + dependencies: + acorn "^8.16.0" + pathe "^2.0.3" + pkg-types "^1.3.1" + ufo "^1.6.3" + mocha@10.3.0: version "10.3.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.3.0.tgz#0e185c49e6dccf582035c05fa91084a4ff6e3fe9" @@ -6115,7 +6663,12 @@ mocha@10.3.0: yargs-parser "20.2.4" yargs-unparser "2.0.0" -mrmime@^2.0.0: +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +mrmime@^2.0.0, mrmime@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc" integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ== @@ -6130,40 +6683,45 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msw@2.10.5: - version "2.10.5" - resolved "https://registry.yarnpkg.com/msw/-/msw-2.10.5.tgz#3e43f12e97581c260bf38d8817732b9fec3bfdb0" - integrity sha512-0EsQCrCI1HbhpBWd89DvmxY6plmvrM96b0sCIztnvcNHQbXn5vqwm1KlXslo6u4wN9LFGLC1WFjjgljcQhe40A== +msw@2.14.6: + version "2.14.6" + resolved "https://registry.yarnpkg.com/msw/-/msw-2.14.6.tgz#d30fa6ce8ec3299c6d9bf644cee3a5cc3c3f1197" + integrity sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg== dependencies: - "@bundled-es-modules/cookie" "^2.0.1" - "@bundled-es-modules/statuses" "^1.0.1" - "@bundled-es-modules/tough-cookie" "^0.1.6" - "@inquirer/confirm" "^5.0.0" - "@mswjs/interceptors" "^0.39.1" - "@open-draft/deferred-promise" "^2.2.0" - "@open-draft/until" "^2.1.0" - "@types/cookie" "^0.6.0" - "@types/statuses" "^2.0.4" - graphql "^16.8.1" - headers-polyfill "^4.0.2" + "@inquirer/confirm" "^6.0.11" + "@mswjs/interceptors" "^0.41.3" + "@open-draft/deferred-promise" "^3.0.0" + "@types/statuses" "^2.0.6" + cookie "^1.1.1" + graphql "^16.13.2" + headers-polyfill "^5.0.1" is-node-process "^1.2.0" outvariant "^1.4.3" path-to-regexp "^6.3.0" picocolors "^1.1.1" + rettime "^0.11.11" + statuses "^2.0.2" strict-event-emitter "^0.5.1" - type-fest "^4.26.1" + tough-cookie "^6.0.1" + type-fest "^5.5.0" + until-async "^3.0.2" yargs "^17.7.2" -mute-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" - integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== +mute-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-3.0.0.tgz#cd8014dd2acb72e1e91bb67c74f0019e620ba2d1" + integrity sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw== nanoid@^3.3.11, nanoid@^3.3.8: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +nanoid@^3.3.12: + version "3.3.12" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05" + integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6231,6 +6789,16 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== +node-fetch-native@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71" + integrity sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q== + +node-mock-http@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/node-mock-http/-/node-mock-http-1.0.4.tgz#21f2ab4ce2fe4fbe8a660d7c5195a1db85e042a4" + integrity sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ== + node-releases@^2.0.19: version "2.0.19" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" @@ -6253,6 +6821,15 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== +nostics@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/nostics/-/nostics-0.2.0.tgz#8c9274d7352bf9c6c556d2b63e179431c88a1a6d" + integrity sha512-/WQpI46UMbqvy1okYb+V+9wW3J8/m6GJ33wm691n/tyi6YtJiZ6ssJjENAU7y4evfYrrgYN9HllKDzPvffil1w== + dependencies: + magic-string "^0.30.21" + oxc-parser "^0.132.0" + unplugin "^3.0.0" + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -6331,6 +6908,20 @@ object.values@^1.2.1: define-properties "^1.2.1" es-object-atoms "^1.0.0" +obug@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/obug/-/obug-2.1.1.tgz#2cba74ff241beb77d63055ddf4cd1e9f90b538be" + integrity sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ== + +ofetch@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.5.1.tgz#5c43cc56e03398b273014957060344254505c5c7" + integrity sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA== + dependencies: + destr "^2.0.5" + node-fetch-native "^1.6.7" + ufo "^1.6.1" + on-finished@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -6402,6 +6993,39 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" +oxc-parser@^0.132.0: + version "0.132.0" + resolved "https://registry.yarnpkg.com/oxc-parser/-/oxc-parser-0.132.0.tgz#4f0ffad5ccfd0235a8ba79f7e6fc988be6f45476" + integrity sha512-+0LAPHaqtfQlvWdpaAa09SmOaZZgP8C552xosEkGJ4+ruEwP1Vgx+sqBgcBCNfR6KDCmagGOZTde8wmAvcI/Hg== + dependencies: + "@oxc-project/types" "^0.132.0" + optionalDependencies: + "@oxc-parser/binding-android-arm-eabi" "0.132.0" + "@oxc-parser/binding-android-arm64" "0.132.0" + "@oxc-parser/binding-darwin-arm64" "0.132.0" + "@oxc-parser/binding-darwin-x64" "0.132.0" + "@oxc-parser/binding-freebsd-x64" "0.132.0" + "@oxc-parser/binding-linux-arm-gnueabihf" "0.132.0" + "@oxc-parser/binding-linux-arm-musleabihf" "0.132.0" + "@oxc-parser/binding-linux-arm64-gnu" "0.132.0" + "@oxc-parser/binding-linux-arm64-musl" "0.132.0" + "@oxc-parser/binding-linux-ppc64-gnu" "0.132.0" + "@oxc-parser/binding-linux-riscv64-gnu" "0.132.0" + "@oxc-parser/binding-linux-riscv64-musl" "0.132.0" + "@oxc-parser/binding-linux-s390x-gnu" "0.132.0" + "@oxc-parser/binding-linux-x64-gnu" "0.132.0" + "@oxc-parser/binding-linux-x64-musl" "0.132.0" + "@oxc-parser/binding-openharmony-arm64" "0.132.0" + "@oxc-parser/binding-wasm32-wasi" "0.132.0" + "@oxc-parser/binding-win32-arm64-msvc" "0.132.0" + "@oxc-parser/binding-win32-ia32-msvc" "0.132.0" + "@oxc-parser/binding-win32-x64-msvc" "0.132.0" + +oxc@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/oxc/-/oxc-1.0.1.tgz#27d3abc73ca8cbc7884b86bf5ed0de278634c9af" + integrity sha512-MJ18y2Ekl329i3zdZpRVOqFdEUjoRKC1+uy1f4kuRp9ygindCVVUIhhKxwyAhTsWt3jIV8UczKtlTwWWahcaWQ== + p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -6416,6 +7040,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-limit@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-7.3.0.tgz#821398d91491c6b6a1340ecd09cdc402a9c8d0ee" + integrity sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw== + dependencies: + yocto-queue "^1.2.1" + p-locate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" @@ -6469,6 +7100,11 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +package-manager-detector@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz#70d0cf0aa02c877eeaf66c4d984ede0be9130734" + integrity sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA== + pako@~1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -6558,7 +7194,7 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pathe@^2.0.3: +pathe@^2.0.1, pathe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== @@ -6583,6 +7219,11 @@ perfect-debounce@^1.0.0: resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== +perfect-debounce@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz#e7078e38f231cb191855c3136a4423aef725d261" + integrity sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g== + phoenix@1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.8.1.tgz#cc247b29f844f22d54291b558689e2381ed8cc43" @@ -6603,17 +7244,22 @@ picomatch@^4.0.2, picomatch@^4.0.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== +picomatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" + integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pinia@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/pinia/-/pinia-3.0.3.tgz#f412019bdeb2f45e85927b432803190343e12d89" - integrity sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA== +pinia@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/pinia/-/pinia-3.0.4.tgz#75dde12784a61e34c1fa6abcd13c1a1061c360c0" + integrity sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw== dependencies: - "@vue/devtools-api" "^7.7.2" + "@vue/devtools-api" "^7.7.7" pirates@^4.0.6: version "4.0.7" @@ -6634,17 +7280,26 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -playwright-core@1.57.0: - version "1.57.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.57.0.tgz#3dcc9a865af256fa9f0af0d67fc8dd54eecaebf5" - integrity sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ== - -playwright@1.57.0: - version "1.57.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.57.0.tgz#74d1dacff5048dc40bf4676940b1901e18ad0f46" - integrity sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw== +pkg-types@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== dependencies: - playwright-core "1.57.0" + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +playwright-core@1.61.0: + version "1.61.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.61.0.tgz#caf8078b2a39cd7738dc75ec11cb3b47f385c3f0" + integrity sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA== + +playwright@1.61.0: + version "1.61.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.61.0.tgz#7082df3df08ffa82b11420ea5fae84a40bd16e3f" + integrity sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ== + dependencies: + playwright-core "1.61.0" optionalDependencies: fsevents "2.3.2" @@ -6653,6 +7308,11 @@ pngjs@^5.0.0: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== +pngjs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" + integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== + pointer-tracker@^2.0.3: version "2.5.3" resolved "https://registry.yarnpkg.com/pointer-tracker/-/pointer-tracker-2.5.3.tgz#5ed01f5ff023c649b2d7b20b07d68c3ac40642a6" @@ -6711,7 +7371,7 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.5.6, postcss@^8.5.3, postcss@^8.5.6: +postcss@8.5.6, postcss@^8.5.6: version "8.5.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== @@ -6729,20 +7389,20 @@ postcss@^8.5.0: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.5.15: + version "8.5.15" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c" + integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A== + dependencies: + nanoid "^3.3.12" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -pretty-format@^27.0.2: - version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" - integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== - dependencies: - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^17.0.1" - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -6787,6 +7447,16 @@ psl@^1.1.33: dependencies: punycode "^2.3.1" +publint@^0.3.21: + version "0.3.21" + resolved "https://registry.yarnpkg.com/publint/-/publint-0.3.21.tgz#91e1425f638e2128343d5543f77551915d57409a" + integrity sha512-OqejcnMV6E9zel2oCrUOJEiiFkGiAAni0A6ibfQNh1k9Gu5z4F+Yso8lllam7AzmV6Do0vp7u3UpZNRBwuXaHQ== + dependencies: + "@publint/pack" "^0.1.4" + package-manager-detector "^1.6.0" + picocolors "^1.1.1" + sade "^1.8.1" + pump@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" @@ -6826,6 +7496,11 @@ qs@^6.12.3, qs@^6.14.0: dependencies: side-channel "^1.1.0" +quansync@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/quansync/-/quansync-1.0.0.tgz#1c29acccd544cd68d97a7350c5099e0a9bc7e5ee" + integrity sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA== + querystring-es3@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -6841,6 +7516,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +radix3@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.2.tgz#fd27d2af3896c6bf4bcdfab6427c69c2afc69ec0" + integrity sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -6863,11 +7543,6 @@ raw-body@^3.0.0: iconv-lite "0.6.3" unpipe "1.0.0" -react-is@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -6897,10 +7572,10 @@ readdir-glob@^1.1.2: dependencies: minimatch "^5.1.0" -readdirp@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" - integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== +readdirp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.0.0.tgz#fbf1f71a727891d685bb1786f9ba74084f6e2f91" + integrity sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ== readdirp@~3.6.0: version "3.6.0" @@ -7028,6 +7703,11 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +rettime@^0.11.11: + version "0.11.11" + resolved "https://registry.yarnpkg.com/rettime/-/rettime-0.11.11.tgz#fe8fb192e1877bb0080fc1a640cb08eededd7d12" + integrity sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ== + reusify@^1.0.4: version "1.1.0" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" @@ -7038,36 +7718,34 @@ rfdc@^1.4.1: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== -rollup@^4.34.9: - version "4.52.5" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.52.5.tgz#96982cdcaedcdd51b12359981f240f94304ec235" - integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw== +rolldown@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac" + integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g== dependencies: - "@types/estree" "1.0.8" + "@oxc-project/types" "=0.133.0" + "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.52.5" - "@rollup/rollup-android-arm64" "4.52.5" - "@rollup/rollup-darwin-arm64" "4.52.5" - "@rollup/rollup-darwin-x64" "4.52.5" - "@rollup/rollup-freebsd-arm64" "4.52.5" - "@rollup/rollup-freebsd-x64" "4.52.5" - "@rollup/rollup-linux-arm-gnueabihf" "4.52.5" - "@rollup/rollup-linux-arm-musleabihf" "4.52.5" - "@rollup/rollup-linux-arm64-gnu" "4.52.5" - "@rollup/rollup-linux-arm64-musl" "4.52.5" - "@rollup/rollup-linux-loong64-gnu" "4.52.5" - "@rollup/rollup-linux-ppc64-gnu" "4.52.5" - "@rollup/rollup-linux-riscv64-gnu" "4.52.5" - "@rollup/rollup-linux-riscv64-musl" "4.52.5" - "@rollup/rollup-linux-s390x-gnu" "4.52.5" - "@rollup/rollup-linux-x64-gnu" "4.52.5" - "@rollup/rollup-linux-x64-musl" "4.52.5" - "@rollup/rollup-openharmony-arm64" "4.52.5" - "@rollup/rollup-win32-arm64-msvc" "4.52.5" - "@rollup/rollup-win32-ia32-msvc" "4.52.5" - "@rollup/rollup-win32-x64-gnu" "4.52.5" - "@rollup/rollup-win32-x64-msvc" "4.52.5" - fsevents "~2.3.2" + "@rolldown/binding-android-arm64" "1.0.3" + "@rolldown/binding-darwin-arm64" "1.0.3" + "@rolldown/binding-darwin-x64" "1.0.3" + "@rolldown/binding-freebsd-x64" "1.0.3" + "@rolldown/binding-linux-arm-gnueabihf" "1.0.3" + "@rolldown/binding-linux-arm64-gnu" "1.0.3" + "@rolldown/binding-linux-arm64-musl" "1.0.3" + "@rolldown/binding-linux-ppc64-gnu" "1.0.3" + "@rolldown/binding-linux-s390x-gnu" "1.0.3" + "@rolldown/binding-linux-x64-gnu" "1.0.3" + "@rolldown/binding-linux-x64-musl" "1.0.3" + "@rolldown/binding-openharmony-arm64" "1.0.3" + "@rolldown/binding-wasm32-wasi" "1.0.3" + "@rolldown/binding-win32-arm64-msvc" "1.0.3" + "@rolldown/binding-win32-x64-msvc" "1.0.3" + +rou3@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/rou3/-/rou3-0.8.1.tgz#d18c9dae42bdd9cd4fffa77bc6731d5cfe92129a" + integrity sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA== router@^2.2.0: version "2.2.0" @@ -7097,6 +7775,20 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@^7.4.0: + version "7.8.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== + dependencies: + tslib "^2.1.0" + +sade@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + safe-array-concat@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" @@ -7140,13 +7832,139 @@ safe-regex-test@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass@1.93.2: - version "1.93.2" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.93.2.tgz#e97d225d60f59a3b3dbb6d2ae3c1b955fd1f2cd1" - integrity sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg== +sass-embedded-all-unknown@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.100.0.tgz#efee7111ec5e2d6c34a2f57d090e6416c01890ff" + integrity sha512-auFtXY/kwYILmSVjtBDwyj0axcLbYYiffOKWoaXHnI5bsYwiRbBh3EneR1rpbX2ZIZCrwX93i5pxKLTZF/662Q== dependencies: - chokidar "^4.0.0" - immutable "^5.0.2" + sass "1.100.0" + +sass-embedded-android-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.100.0.tgz#0a4456a751f76a378ac9f3516a034f56eca9b171" + integrity sha512-W+Ru9JwTnfU0UX3jSZcbqFdtKFMcYdfFwytc57h2DgnqCOIiAqI2E06mABZBZC+r3LwXCBuS5GbXAGeVgvVDkA== + +sass-embedded-android-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.100.0.tgz#7de61d65cfa997c58df426a8b20a096a60497313" + integrity sha512-70f3HgX2pFNmzpGQ86n5e6QfWn2fP4QUQGfFQK0P1XH73ZLIzLo2YqygrGKGKeeqtc5eU2Wl1/xQzhzuKnO4kw== + +sass-embedded-android-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.100.0.tgz#cc6721c1526ef5b10d9da02f1e44e7dbaffc11e0" + integrity sha512-icU3o0V/uCSytSpf+tX5Lf51BvyQEbLzDUJfUi9etSauYBGHpPKkdtdZH0si4v98phq11Kl8rSV1SggksxF1Hg== + +sass-embedded-android-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.100.0.tgz#dc38f6c633f8bf61b470ac00452ebb50e36f421f" + integrity sha512-mevF9VQk6gEYByy8+jusaHGmd7Usb2ytX/DsEOd0JtOGCtcf1kh575xJ6OUBDIcJ15uLnbau/0iy1eP6WVBvWA== + +sass-embedded-darwin-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.100.0.tgz#7773c0586e78caab599d53370626c28ca08110eb" + integrity sha512-1PVlYi61POo93IT/FfrG1mc1tAHxeSTyUALF2aOFmXGWjVXr3bQzEQiBGCOvQbj/ix+5hNyXFXcEMEyKvtUJJA== + +sass-embedded-darwin-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.100.0.tgz#91066f64875b87eba0cb699ba8254b4863d33901" + integrity sha512-x97o3JnGyImZNCIVs9wQHJUE5QCvmVIKaH1cwrz/5dK7OT1FpeNiW+u9TUomP9hG6Ekjd8EL8NBHpxTfIhdjmg== + +sass-embedded-linux-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.100.0.tgz#88d2f51a0882f399c327a1d6cd06757d13de4f82" + integrity sha512-Dwjmj8Z6VRy7rAi53JAdEwIyUjpfl7PhpSc2/LpQPQx+aO5Dp7Spaipkax0ufJl1SoDUdchCsM4y/88YaluorQ== + +sass-embedded-linux-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.100.0.tgz#9721dca177348c1448847ceee1d031a692df0771" + integrity sha512-9Ul7O1eKrc5YlhwWjkp8tZPSe3UEwSZ1uwUZOQom1HL0pRlBA6F/IlGZYFTLwnHMIP1fc77MMNaBRfc05mKMpw== + +sass-embedded-linux-musl-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.100.0.tgz#63a1e0618a2a3acf10bab915585c4dc495327581" + integrity sha512-XpACJB2KjSLjf2e9uuvGVdOURsoNrFqgRiihhXyUHK9W0t3LIHb7z5MA/7XGPIT9bWSOO2zyw+rH/FHtDV/Yrg== + +sass-embedded-linux-musl-arm@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.100.0.tgz#44f9472ae663411de82b7792a918e162c7bcbc02" + integrity sha512-sl0JgbGloPyJg66XXx5UDSDScZ0oU85DpMQU4JU/sCUCFj1Z8zZ69SJWKTCNE4/jwnce7WI2zPCV5AG+RHOZJw== + +sass-embedded-linux-musl-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.100.0.tgz#21954e480cfb55d4ba8167626cde1cc8560d7eee" + integrity sha512-ShvI0Kx04mwoCARwZ0UjiT97isQvzO80tAt91zmFyHLN9kelc/IrQi940farSm2xQVPCKdeVyeG0ekBsokSpYQ== + +sass-embedded-linux-musl-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.100.0.tgz#a6c93d51999a8b22795d383b67e61962d32c96cd" + integrity sha512-TDBCRWNuS4RDLQXvRc1gjZlWiWTWaWGp0Bwu/IKwJxov81lsvrCs3TihTyNXtW7V5aoN4Ky3r0QOkNb3mwmBnA== + +sass-embedded-linux-riscv64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.100.0.tgz#01f20471a1be16d18e150993e5ac771d618abd34" + integrity sha512-j4ENJGOheO+fm3j/yorLxCjBP6/XskrZx7dTLlT+lXYwN/qqCqoA/gsNLI0McS3DFM6GBwPiffzWsdWS8t6sEQ== + +sass-embedded-linux-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.100.0.tgz#809649a0df17f7afb1dfd02e76dbf0676dd8471e" + integrity sha512-0vUSN8j0WGtCJIOPh//EmUvYGHW0QOe5iul8qyhPk50MAcw49MA0r34AhftjDdx94ILPF6vApFs0gwHPQRlpVA== + +sass-embedded-unknown-all@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.100.0.tgz#fdc210a25a06e9d79655a02c1c77d656fa6f2ca0" + integrity sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A== + dependencies: + sass "1.100.0" + +sass-embedded-win32-arm64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.100.0.tgz#fd8cec45b41390c3db40d13c4d291be7f35b6b0b" + integrity sha512-iE+yxj+hUXwwbqpHkXxgAWTzeRfcWxJ7SSTQEPMk48lwq3oCrWLlz5sQuWHbuTK/i0GKQfROdP+hOmPi89yjUg== + +sass-embedded-win32-x64@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.100.0.tgz#a758862b1455d6616be301b363e7e1c93e6361b7" + integrity sha512-qI4F8MI7/KYoy9NdjJfhSspG42WPkADSNDvwEV7qWvCSFC83koJssRsKO2/PfY+niZz6BG65Ic/D+A11h959hw== + +sass-embedded@^1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass-embedded/-/sass-embedded-1.100.0.tgz#fe4742f2f80c21b287e3b90399b9a3f7ca1dcf6f" + integrity sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw== + dependencies: + "@bufbuild/protobuf" "^2.5.0" + colorjs.io "^0.5.0" + immutable "^5.1.5" + rxjs "^7.4.0" + supports-color "^8.1.1" + sync-child-process "^1.0.2" + varint "^6.0.0" + optionalDependencies: + sass-embedded-all-unknown "1.100.0" + sass-embedded-android-arm "1.100.0" + sass-embedded-android-arm64 "1.100.0" + sass-embedded-android-riscv64 "1.100.0" + sass-embedded-android-x64 "1.100.0" + sass-embedded-darwin-arm64 "1.100.0" + sass-embedded-darwin-x64 "1.100.0" + sass-embedded-linux-arm "1.100.0" + sass-embedded-linux-arm64 "1.100.0" + sass-embedded-linux-musl-arm "1.100.0" + sass-embedded-linux-musl-arm64 "1.100.0" + sass-embedded-linux-musl-riscv64 "1.100.0" + sass-embedded-linux-musl-x64 "1.100.0" + sass-embedded-linux-riscv64 "1.100.0" + sass-embedded-linux-x64 "1.100.0" + sass-embedded-unknown-all "1.100.0" + sass-embedded-win32-arm64 "1.100.0" + sass-embedded-win32-x64 "1.100.0" + +sass@1.100.0: + version "1.100.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.100.0.tgz#b4cab1bed286fe22ac6c879c514f71cd36aa06c8" + integrity sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ== + dependencies: + chokidar "^5.0.0" + immutable "^5.1.5" source-map-js ">=0.6.2 <2.0.0" optionalDependencies: "@parcel/watcher" "^2.4.1" @@ -7244,6 +8062,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz#e0b1d94c8660c68e6a24dc4e2b5c9e955ccf7e28" + integrity sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -7383,10 +8206,10 @@ sinon@20.0.0: diff "^7.0.0" supports-color "^7.2.0" -sirv@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.1.tgz#32a844794655b727f9e2867b777e0060fbe07bf3" - integrity sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A== +sirv@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.2.tgz#f775fccf10e22a40832684848d636346f41cd970" + integrity sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g== dependencies: "@polka/url" "^1.0.0-next.24" mrmime "^2.0.0" @@ -7456,6 +8279,11 @@ sprintf-js@^1.1.3: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== +srvx@^0.11.15: + version "0.11.16" + resolved "https://registry.yarnpkg.com/srvx/-/srvx-0.11.16.tgz#750c66514e26cfa26507067156cbf1932b21ada0" + integrity sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw== + stackback@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" @@ -7473,10 +8301,15 @@ statuses@2.0.1, statuses@^2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -std-env@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" - integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== +statuses@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +std-env@^4.0.0-rc.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.1.0.tgz#45899abc590d86d682e87f0acd1033a75084cd3f" + integrity sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ== stop-iteration-iterator@^1.0.0, stop-iteration-iterator@^1.1.0: version "1.1.0" @@ -7712,7 +8545,7 @@ superjson@^2.2.2: dependencies: copy-anything "^3.0.2" -supports-color@8.1.1: +supports-color@8.1.1, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -7756,6 +8589,18 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +sync-child-process@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/sync-child-process/-/sync-child-process-1.0.2.tgz#45e7c72e756d1243e80b547ea2e17957ab9e367f" + integrity sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA== + dependencies: + sync-message-port "^1.0.0" + +sync-message-port@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/sync-message-port/-/sync-message-port-1.2.0.tgz#4b0d622085f21496061037125dec61755d96e330" + integrity sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg== + table@^6.9.0: version "6.9.0" resolved "https://registry.yarnpkg.com/table/-/table-6.9.0.tgz#50040afa6264141c7566b3b81d4d82c47a8668f5" @@ -7767,6 +8612,11 @@ table@^6.9.0: string-width "^4.2.3" strip-ansi "^6.0.1" +tagged-tag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" + integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== + tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -7801,33 +8651,35 @@ tinybench@^2.9.0: resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== -tinyexec@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" - integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== +tinyexec@^1.0.2, tinyexec@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.4.tgz#ae45bb2edebda94c70f4ea897e0f1243e470db71" + integrity sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg== -tinyglobby@^0.2.13: - version "0.2.15" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" - integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== +tinyglobby@^0.2.15, tinyglobby@^0.2.16, tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== dependencies: fdir "^6.5.0" - picomatch "^4.0.3" + picomatch "^4.0.4" -tinypool@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" - integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== +tinyrainbow@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz#1d8a623893f95cf0a2ddb9e5d11150e191409421" + integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw== -tinyrainbow@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" - integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== +tldts-core@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.4.3.tgz#d43401c0499cd884eeaf1ccf073df841a1e4e2dd" + integrity sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw== -tinyspy@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" - integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== +tldts@^7.0.5: + version "7.4.3" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.4.3.tgz#536c93aecffc96d41ce5627a4b7e12f9c2cfceb5" + integrity sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg== + dependencies: + tldts-core "^7.4.3" tmp@^0.2.3: version "0.2.3" @@ -7861,6 +8713,13 @@ tough-cookie@^4.1.4: universalify "^0.2.0" url-parse "^1.5.3" +tough-cookie@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.1.tgz#a495f833836609ed983c19bc65639cfbceb54c76" + integrity sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw== + dependencies: + tldts "^7.0.5" + tr46@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.0.tgz#4a077922360ae807e172075ce5beb79b36e4a101" @@ -7885,7 +8744,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.1: +tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -7912,20 +8771,17 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - type-fest@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== -type-fest@^4.26.1: - version "4.39.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.39.0.tgz#c7758be50a83a5b879e7a59ea52421e9816b3928" - integrity sha512-w2IGJU1tIgcrepg9ZJ82d8UmItNQtOFJG0HCUE3SzMokKkTsruVDALl2fAdiEzJlfduoU+VyXJWIIUZ+6jV+nw== +type-fest@^5.5.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.7.0.tgz#bae586d3b7c2596bd9c7e62195f33c7fcada1c91" + integrity sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg== + dependencies: + tagged-tag "^1.0.0" type-is@^2.0.0, type-is@^2.0.1: version "2.0.1" @@ -7981,6 +8837,11 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" +ufo@^1.6.1, ufo@^1.6.3: + version "1.6.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.4.tgz#7a8fb875fcc6382d2c7d0b3692738b0500a92467" + integrity sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA== + unbox-primitive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" @@ -7991,6 +8852,30 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" +unconfig-core@7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/unconfig-core/-/unconfig-core-7.5.0.tgz#68f3d000701288418f3d36203d886f38c3986677" + integrity sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w== + dependencies: + "@quansync/fs" "^1.0.0" + quansync "^1.0.0" + +unconfig@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/unconfig/-/unconfig-7.5.0.tgz#122d8ef27e27aedf5551485069161c0852ab534d" + integrity sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA== + dependencies: + "@quansync/fs" "^1.0.0" + defu "^6.1.4" + jiti "^2.6.1" + quansync "^1.0.0" + unconfig-core "7.5.0" + +uncrypto@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" + integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== + undici-types@~6.20.0: version "6.20.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" @@ -8029,6 +8914,34 @@ unpipe@1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unplugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-3.0.0.tgz#01e40c474bf74d363744f4cb262569d26dd9bb43" + integrity sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg== + dependencies: + "@jridgewell/remapping" "^2.3.5" + picomatch "^4.0.3" + webpack-virtual-modules "^0.6.2" + +unstorage@^1.17.5: + version "1.17.5" + resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.17.5.tgz#e76c82fdc1d2c04cb0e2c0a1de08aa08b2253f51" + integrity sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg== + dependencies: + anymatch "^3.1.3" + chokidar "^5.0.0" + destr "^2.0.5" + h3 "^1.15.10" + lru-cache "^11.2.7" + node-fetch-native "^1.6.7" + ofetch "^1.5.1" + ufo "^1.6.3" + +until-async@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/until-async/-/until-async-3.0.2.tgz#447f1531fdd7bb2b4c7a98869bdb1a4c2a23865f" + integrity sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw== + untildify@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" @@ -8085,77 +8998,74 @@ uuid@8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +valibot@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/valibot/-/valibot-1.4.1.tgz#68f812ae16ec9fffc5f203c33f9d117893df8da8" + integrity sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g== + +varint@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" + integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== + vary@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vite-node@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.1.3.tgz#d021ced40b5a057305eaea9ce62c610c33b60a48" - integrity sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA== +vite-plugin-eslint2@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/vite-plugin-eslint2/-/vite-plugin-eslint2-5.1.0.tgz#c796d4dc852b35f91db508946a4833589adea319" + integrity sha512-fNuO/D7b+EZ5ejhuBA80tiaxWztZWDHc+lCZaXMOHgYfqFXq8WKmGwrudS+/jscp0UNAKGB71du+xoP8azSXiw== dependencies: - cac "^6.7.14" - debug "^4.4.0" - es-module-lexer "^1.7.0" - pathe "^2.0.3" - vite "^5.0.0 || ^6.0.0" + "@rollup/pluginutils" "^5.3.0" + debug "^4.4.3" -vite-plugin-eslint2@^5.0.3: - version "5.0.4" - resolved "https://registry.yarnpkg.com/vite-plugin-eslint2/-/vite-plugin-eslint2-5.0.4.tgz#7276c9179742d3c19faa94f30f9eb5cb7ac04888" - integrity sha512-3Yc7K2R/RrONB9JtwEh2Y40YP3tQi/3UiNHrwcYDsDBKDKnEu7B8PwmXLm7piDFRbxcnTPvgrV2LZnBpKP8JUw== +vite-plugin-stylelint@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/vite-plugin-stylelint/-/vite-plugin-stylelint-6.1.0.tgz#8c039d1d9bc1db1e81e55ddd2dc6bc6bbda95300" + integrity sha512-tMw0gum2gRtul0FA4fbYp5qIvympqUf1YJ7owRjzqMCaWZYoUVN4dXFlauo1ll2UE2ujzBFKfW9VFaXdmODVPQ== dependencies: - "@rollup/pluginutils" "^5.2.0" - debug "^4.4.1" + "@rollup/pluginutils" "^5.3.0" + debug "^4.4.3" -vite-plugin-stylelint@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/vite-plugin-stylelint/-/vite-plugin-stylelint-6.0.2.tgz#8a44345dacb6710877cd841e0467811c6b90253d" - integrity sha512-whqm2m5rvfd4cYA+cpwZ3BROR/5enRGdRr65hxQNHYn6YFmP8M1xrVKEbLIEBSmmSZ7G7AEZWccS8X+UAksIXA== +"vite@^6.0.0 || ^7.0.0 || ^8.0.0", vite@^8.0.0: + version "8.0.16" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6" + integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw== dependencies: - "@rollup/pluginutils" "^5.2.0" - debug "^4.4.1" - -"vite@^5.0.0 || ^6.0.0", vite@^6.1.0: - version "6.4.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-6.4.1.tgz#afbe14518cdd6887e240a4b0221ab6d0ce733f96" - integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g== - dependencies: - esbuild "^0.25.0" - fdir "^6.4.4" - picomatch "^4.0.2" - postcss "^8.5.3" - rollup "^4.34.9" - tinyglobby "^0.2.13" + lightningcss "^1.32.0" + picomatch "^4.0.4" + postcss "^8.5.15" + rolldown "1.0.3" + tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" -vitest@^3.0.7: - version "3.1.3" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.1.3.tgz#0b0b01932408cd3af61867f4468d28bd83406ffb" - integrity sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw== +vitest@^4.1.7: + version "4.1.9" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.9.tgz#98f22fbd70e2a18c4a92bb20624bc92e5dfac5f3" + integrity sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ== dependencies: - "@vitest/expect" "3.1.3" - "@vitest/mocker" "3.1.3" - "@vitest/pretty-format" "^3.1.3" - "@vitest/runner" "3.1.3" - "@vitest/snapshot" "3.1.3" - "@vitest/spy" "3.1.3" - "@vitest/utils" "3.1.3" - chai "^5.2.0" - debug "^4.4.0" - expect-type "^1.2.1" - magic-string "^0.30.17" + "@vitest/expect" "4.1.9" + "@vitest/mocker" "4.1.9" + "@vitest/pretty-format" "4.1.9" + "@vitest/runner" "4.1.9" + "@vitest/snapshot" "4.1.9" + "@vitest/spy" "4.1.9" + "@vitest/utils" "4.1.9" + es-module-lexer "^2.0.0" + expect-type "^1.3.0" + magic-string "^0.30.21" + obug "^2.1.1" pathe "^2.0.3" - std-env "^3.9.0" + picomatch "^4.0.3" + std-env "^4.0.0-rc.1" tinybench "^2.9.0" - tinyexec "^0.3.2" - tinyglobby "^0.2.13" - tinypool "^1.0.2" - tinyrainbow "^2.0.0" - vite "^5.0.0 || ^6.0.0" - vite-node "3.1.3" + tinyexec "^1.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.1.0" + vite "^6.0.0 || ^7.0.0 || ^8.0.0" why-is-node-running "^2.3.0" vue-component-type-helpers@^2.0.0: @@ -8215,6 +9125,11 @@ vue-virtual-scroller@^2.0.0-beta.7: vue-observe-visibility "^2.0.0-alpha.1" vue-resize "^2.0.0-alpha.1" +vue-virtual-scroller@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/vue-virtual-scroller/-/vue-virtual-scroller-3.0.4.tgz#b97214b0e8f32e4667921550a461a3ac0741389e" + integrity sha512-3qh3c9VUVysuXynaa4fVZ3ncx3VgD7EPRiQcj+jUVZl5u/TTkD3c27XvSEu3JGJfsJt/vVTVziZ3djiiHtW4cQ== + vue@3.5.22: version "3.5.22" resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.22.tgz#2b8ddb94ee4b640ef12fe7f6efe1cf16f3b582e7" @@ -8226,6 +9141,17 @@ vue@3.5.22: "@vue/server-renderer" "3.5.22" "@vue/shared" "3.5.22" +vue@^3.5.35: + version "3.5.35" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.35.tgz#ba502479bd781825514c8af0f40744eeeb6223d6" + integrity sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q== + dependencies: + "@vue/compiler-dom" "3.5.35" + "@vue/compiler-sfc" "3.5.35" + "@vue/runtime-dom" "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" @@ -8252,6 +9178,11 @@ webidl-conversions@^7.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== +webpack-virtual-modules@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" + integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== + whatwg-encoding@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" @@ -8423,10 +9354,10 @@ ws@^8.18.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== -ws@^8.18.1: - version "8.18.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" - integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== +ws@^8.19.0, ws@^8.21.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951" + integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== xml-name-validator@^4.0.0: version "4.0.0" @@ -8557,10 +9488,10 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yoctocolors-cjs@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" - integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== +yocto-queue@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.2.tgz#3e09c95d3f1aa89a58c114c99223edf639152c00" + integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== zip-stream@^4.1.0: version "4.1.1"