diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..3de57a360 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +logs/ +.DS_Store +.git/ +config/local.json +pleroma-backend/ +test/e2e/reports/ +test/e2e-playwright/test-results/ +test/e2e-playwright/playwright-report/ +__screenshots__/ + diff --git a/.gitignore b/.gitignore index 01ffda9a8..c4a96ee1e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,11 @@ dist/ npm-debug.log test/unit/coverage test/e2e/reports +test/e2e-playwright/test-results +test/e2e-playwright/playwright-report selenium-debug.log .idea/ +.gitlab-ci-local/ config/local.json src/assets/emoji.json logs/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 99c85dd36..06fbf45f9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,12 +34,23 @@ check-changelog: - apk add git - sh ./tools/check-changelog -lint: +lint-eslint: stage: lint script: - yarn - - yarn lint - - yarn stylelint + - yarn ci-eslint + +lint-biome: + stage: lint + script: + - yarn + - yarn ci-biome + +lint-stylelint: + stage: lint + script: + - yarn + - yarn ci-stylelint test: stage: test @@ -60,6 +71,135 @@ test: - test/**/__screenshots__ when: on_failure +e2e-pleroma: + stage: test + image: mcr.microsoft.com/playwright:v1.57.0-jammy + services: + - name: postgres:15-alpine + alias: db + - name: $PLEROMA_IMAGE + alias: pleroma + entrypoint: ["/bin/ash", "-c"] + command: + - | + set -eu + + SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded + CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs + + echo '-- Waiting for database...' + while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do + sleep 1s + done + + echo '-- Writing E2E config overrides...' + cat > $CONFIG_OVERRIDE_PATH </dev/null; then + kill -TERM $PLEROMA_PID + wait $PLEROMA_PID || true + fi + } + + trap cleanup INT TERM + + echo '-- Waiting for API...' + api_ok=false + for _i in $(seq 1 120); do + if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then + api_ok=true + break + fi + sleep 1s + done + + if [ $api_ok != true ]; then + echo 'Timed out waiting for Pleroma API to become available' + exit 1 + fi + + if [ ! -f $SEED_SENTINEL_PATH ]; then + if [ -n ${E2E_ADMIN_USERNAME:-} ] && [ -n ${E2E_ADMIN_PASSWORD:-} ] && [ -n ${E2E_ADMIN_EMAIL:-} ]; then + echo '-- Seeding admin user' $E2E_ADMIN_USERNAME '...' + if ! /opt/pleroma/bin/pleroma_ctl user new $E2E_ADMIN_USERNAME $E2E_ADMIN_EMAIL --admin --password $E2E_ADMIN_PASSWORD -y; then + echo '-- User already exists or creation failed, ensuring admin + confirmed...' + /opt/pleroma/bin/pleroma_ctl user set $E2E_ADMIN_USERNAME --admin --confirmed + fi + else + echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)' + fi + + touch $SEED_SENTINEL_PATH + fi + + wait $PLEROMA_PID + tags: + - amd64 + - himem + variables: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" + FF_NETWORK_PER_BUILD: "true" + PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable + POSTGRES_USER: pleroma + POSTGRES_PASSWORD: pleroma + POSTGRES_DB: pleroma + DB_USER: pleroma + DB_PASS: pleroma + DB_NAME: pleroma + DB_HOST: db + DB_PORT: 5432 + DOMAIN: localhost + INSTANCE_NAME: Pleroma E2E + E2E_ADMIN_USERNAME: admin + E2E_ADMIN_PASSWORD: adminadmin + E2E_ADMIN_EMAIL: admin@example.com + ADMIN_EMAIL: $E2E_ADMIN_EMAIL + NOTIFY_EMAIL: $E2E_ADMIN_EMAIL + VITE_PROXY_TARGET: http://pleroma:4000 + VITE_PROXY_ORIGIN: http://localhost:4000 + E2E_BASE_URL: http://localhost:8080 + script: + - npm install -g yarn@1.22.22 + - yarn --frozen-lockfile + - | + echo "-- Waiting for Pleroma API..." + api_ok="false" + for _i in $(seq 1 120); do + if wget -qO- http://pleroma:4000/api/v1/instance >/dev/null 2>&1; then + api_ok="true" + break + fi + sleep 1s + done + if [ "$api_ok" != "true" ]; then + echo "Timed out waiting for Pleroma API to become available" + exit 1 + fi + - yarn e2e:pw + artifacts: + when: on_failure + paths: + - test/e2e-playwright/test-results + - test/e2e-playwright/playwright-report + build: stage: build tags: diff --git a/.gitlab/merge_request_templates/Release.md b/.gitlab/merge_request_templates/Release.md new file mode 100644 index 000000000..d02e14a73 --- /dev/null +++ b/.gitlab/merge_request_templates/Release.md @@ -0,0 +1,8 @@ +### Release checklist +* [ ] Bump version in `package.json` +* [ ] Compile a changelog with the `tools/collect-changelog` script +* [ ] Create an MR with an announcement to pleroma.social +#### post-merge +* [ ] Tag the release on the merge commit +* [ ] Make the tag into a Gitlab Releaseā„¢ +* [ ] Merge `master` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs) diff --git a/.stylelintrc.json b/.stylelintrc.json index c91107595..afdfd5f5b 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -12,6 +12,8 @@ "custom-property-pattern": null, "keyframes-name-pattern": null, "scss/operator-no-newline-after": null, + "declaration-property-value-no-unknown": true, + "scss/declaration-property-value-no-unknown": true, "declaration-block-no-redundant-longhand-properties": [ true, { diff --git a/CHANGELOG.md b/CHANGELOG.md index c2f0e7d17..b3d38ea24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,67 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## 2.10.0 +### Changed +- Temporary changes modal now shows actual countdown instead of fixed timeout +- Disabled elements are more disabled now +- Rearranged and split settings to make more sense and be less of a wall of text +- On mobile settings now take up full width and presented in navigation style +improved styles for settings + +### Added +- Most of the remaining AdminFE tabs were added into Admin Dashboard +- It's now possible to customize PWA Manfiest from PleromaFE +- Make every configuration option default-overridable by instance admins + +### Fixed +- Fixed settings not appearing if user never touched "show advanced" toggle +- Fix display of the broken/deleted/banned users +- Fixed incorrect emoji display in post interaction lists +- Fixed list title not being saved when editing +- Fixed poll notifications not being expandable + + +## 2.9.3 +### Fixed +- Being unable to update profile + +## 2.9.2 +### Changed +- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible +- User card/profile got an overhaul +- Profile editing overhaul +- Visually combined subject and content fields in post form +- Moved post form's emoji button into input field +- Minor visual changes and fixes +- Clicking on fav/rt/emoji notifications' contents expands/collapses it +- Reduced time taken processing theme by half +- Splash screen only appears if loading takes more than 2 seconds + +### Added +- Mutes received an update, adding support for regex, muting based on username and expiration time. +- Mutes are now synchronized across sessions +- Support for expiring mutes and blocks (if available) +- Clicking on emoji shows bigger version of it alongside with its shortcode + - Admins also are able to copy it into a local pack +- Added support for Akkoma and IceShrimp.NET backends +- Compatibility with stricter CSP (Akkoma backend) +- Added a way to upload new packs from a URL or ZIP file via the Admin Dashboard +- Unify show/hide content buttons +- Add support for detachable scrollTop button +- Option to left-align user bio +- Cache assets and emojis with service worker +- Indicate currently active V3 theme as a body element class +- Add arithmetic blend ISS function + +### Fixed +- Display counter for status action buttons when they are in the menu +- Fix bookmark button alignment in the extra actions menu +- Instance favicons are no longer stretched +- A lot more scalable UI fixes + - Emoji picker now should work fine when emoji size is increased + ## 2.8.0 ### Changed - BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image @@ -34,8 +95,8 @@ This does not guarantee that browsers will or will not work. - Support displaying time in absolute format - Add draft management system - Compress most kinds of images on upload. -- Added option to always convert images to JPEG format instead of using WebP when compressing images. -- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload. +- Added option to always convert images to JPEG format instead of using WebP when compressing images. +- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload. - Inform users that Smithereen public polls are public - Splash screen + loading indicator to make process of identifying initialization issues and load performance - UI for making v3 themes and palettes, support for bundling v3 themes diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..d64639d52 --- /dev/null +++ b/biome.json @@ -0,0 +1,145 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**", "!!**/dist", "!!tools/emojis.json"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "domains": { + "vue": "recommended" + }, + "rules": { + "recommended": false, + "complexity": { + "noAdjacentSpacesInRegex": "error", + "noExtraBooleanCast": "error", + "noUselessCatch": "error", + "noUselessEscapeInRegex": "error" + }, + "correctness": { + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "error", + "noGlobalObjectCalls": "error", + "noInvalidBuiltinInstantiation": "error", + "noInvalidConstructorSuper": "error", + "noNonoctalDecimalEscape": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSetterReturn": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnsafeOptionalChaining": "error", + "noUnusedLabels": "error", + "noUnusedPrivateClassMembers": "error", + "noUnusedVariables": "error", + "useIsNan": "error", + "useValidForDirection": "error", + "useValidTypeof": "error", + "useYield": "error" + }, + "suspicious": { + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCompareNegZero": "error", + "noConstantBinaryExpressions": "error", + "noControlCharactersInRegex": "error", + "noDebugger": "error", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateElseIf": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "error", + "noFallthroughSwitchClause": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noIrregularWhitespace": "error", + "noMisleadingCharacterClass": "error", + "noPrototypeBuiltins": "error", + "noRedeclare": "error", + "noShadowRestrictedNames": "error", + "noSparseArray": "error", + "noUnsafeNegation": "error", + "noUselessRegexBackrefs": "error", + "noWith": "error", + "useGetterReturn": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded" + }, + "globals": [] + }, + "overrides": [ + { + "includes": ["**/*.spec.js", "test/fixtures/*.js"], + "javascript": { + "globals": [ + "vi", + "describe", + "it", + "test", + "expect", + "before", + "beforeEach", + "after", + "afterEach" + ] + } + }, + { + "includes": ["**/*.vue"], + "linter": { + "rules": { + "style": { + "useConst": "off", + "useImportType": "off" + }, + "correctness": { + "noUnusedVariables": "off", + "noUnusedImports": "off" + } + } + } + } + ], + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": { + "level": "on", + "options": { + "groups": [ + [":NODE:", ":PACKAGE:", "!src/**", "!@fortawesome/**"], + ":BLANK_LINE:", + [":PATH:", "src/**"], + ":BLANK_LINE:", + "@fortawesome/fontawesome-svg-core", + "@fortawesome/*" + ] + } + } + } + } + } +} diff --git a/build/check-versions.mjs b/build/check-versions.mjs index 73c1eeb15..8c5968a30 100644 --- a/build/check-versions.mjs +++ b/build/check-versions.mjs @@ -1,5 +1,5 @@ -import semver from 'semver' import chalk from 'chalk' +import semver from 'semver' import packageConfig from '../package.json' with { type: 'json' } @@ -7,8 +7,8 @@ var versionRequirements = [ { name: 'node', currentVersion: semver.clean(process.version), - versionRequirement: packageConfig.engines.node - } + versionRequirement: packageConfig.engines.node, + }, ] export default function () { @@ -16,15 +16,22 @@ export default function () { for (let i = 0; i < versionRequirements.length; i++) { const mod = versionRequirements[i] if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { - warnings.push(mod.name + ': ' + - chalk.red(mod.currentVersion) + ' should be ' + - chalk.green(mod.versionRequirement) + warnings.push( + mod.name + + ': ' + + chalk.red(mod.currentVersion) + + ' should be ' + + chalk.green(mod.versionRequirement), ) } } if (warnings.length) { - console.warn(chalk.yellow('\nTo use this template, you must update following to modules:\n')) + console.warn( + chalk.yellow( + '\nTo use this template, you must update following to modules:\n', + ), + ) for (let i = 0; i < warnings.length; i++) { const warning = warnings[i] console.warn(' ' + warning) diff --git a/build/commit_hash.js b/build/commit_hash.js index c104af5d9..c60355804 100644 --- a/build/commit_hash.js +++ b/build/commit_hash.js @@ -1,8 +1,8 @@ import childProcess from 'child_process' -export const getCommitHash = (() => { - const subst = "$Format:%h$" - if(!subst.match(/Format:/)) { +export const getCommitHash = () => { + const subst = '$Format:%h$' + if (!subst.match(/Format:/)) { return subst } else { try { @@ -15,4 +15,4 @@ export const getCommitHash = (() => { return 'UNKNOWN' } } -}) +} diff --git a/build/copy_plugin.js b/build/copy_plugin.js index a783fe7ff..4f020f359 100644 --- a/build/copy_plugin.js +++ b/build/copy_plugin.js @@ -1,8 +1,8 @@ -import serveStatic from 'serve-static' -import { resolve } from 'node:path' import { cp } from 'node:fs/promises' +import { resolve } from 'node:path' +import serveStatic from 'serve-static' -const getPrefix = s => { +const getPrefix = (s) => { const padEnd = s.endsWith('/') ? s : s + '/' return padEnd.startsWith('/') ? padEnd : '/' + padEnd } @@ -13,28 +13,31 @@ const copyPlugin = ({ inUrl, inFs }) => { let copyTarget const handler = serveStatic(inFs) - return [{ - name: 'copy-plugin-serve', - apply: 'serve', - configureServer (server) { - server.middlewares.use(prefix, handler) - } - }, { - name: 'copy-plugin-build', - apply: 'build', - configResolved (config) { - copyTarget = resolve(config.root, config.build.outDir, subdir) + return [ + { + name: 'copy-plugin-serve', + apply: 'serve', + configureServer(server) { + server.middlewares.use(prefix, handler) + }, }, - closeBundle: { - order: 'post', - sequential: true, - async handler () { - console.log(`Copying '${inFs}' to ${copyTarget}...`) - await cp(inFs, copyTarget, { recursive: true }) - console.log('Done.') - } - } - }] + { + name: 'copy-plugin-build', + apply: 'build', + configResolved(config) { + copyTarget = resolve(config.root, config.build.outDir, subdir) + }, + closeBundle: { + order: 'post', + sequential: true, + async handler() { + console.info(`Copying '${inFs}' to ${copyTarget}...`) + await cp(inFs, copyTarget, { recursive: true }) + console.info('Done.') + }, + }, + }, + ] } export default copyPlugin diff --git a/build/emojis_plugin.js b/build/emojis_plugin.js index aed52066d..7979086dd 100644 --- a/build/emojis_plugin.js +++ b/build/emojis_plugin.js @@ -1,21 +1,23 @@ -import { resolve } from 'node:path' import { access } from 'node:fs/promises' -import { languages, langCodeToCldrName } from '../src/i18n/languages.js' +import { resolve } from 'node:path' + +import { languages } from '../src/i18n/languages.js' const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/' const specialAnnotationsLocale = { - ja_easy: 'ja' + ja_easy: 'ja', } -const internalToAnnotationsLocale = (internal) => specialAnnotationsLocale[internal] || internal +const internalToAnnotationsLocale = (internal) => + specialAnnotationsLocale[internal] || internal // This gets all the annotations that are accessible (whose language // can be chosen in the settings). Data for other languages are // discarded because there is no way for it to be fetched. const getAllAccessibleAnnotations = async (projectRoot) => { - const imports = (await Promise.all( - languages - .map(async lang => { + const imports = ( + await Promise.all( + languages.map(async (lang) => { const destLang = internalToAnnotationsLocale(lang) const importModule = `${annotationsImportPrefix}${destLang}.json` const importFile = resolve(projectRoot, 'node_modules', importModule) @@ -23,11 +25,14 @@ const getAllAccessibleAnnotations = async (projectRoot) => { await access(importFile) return `'${lang}': () => import('${importModule}')` } catch (e) { + console.error(e) return } - }))) - .filter(k => k) - .join(',\n') + }), + ) + ) + .filter((k) => k) + .join(',\n') return ` export const annotationsLoader = { @@ -43,21 +48,21 @@ const emojisPlugin = () => { let projectRoot return { name: 'emojis-plugin', - configResolved (conf) { + configResolved(conf) { projectRoot = conf.root }, - resolveId (id) { + resolveId(id) { if (id === emojiAnnotationsId) { return emojiAnnotationsIdResolved } return null }, - async load (id) { + async load(id) { if (id === emojiAnnotationsIdResolved) { return await getAllAccessibleAnnotations(projectRoot) } return null - } + }, } } diff --git a/build/msw_plugin.js b/build/msw_plugin.js index f544348fc..c4e9098c5 100644 --- a/build/msw_plugin.js +++ b/build/msw_plugin.js @@ -1,5 +1,5 @@ -import { resolve } from 'node:path' import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' const target = 'node_modules/msw/lib/mockServiceWorker.js' @@ -8,10 +8,10 @@ const mswPlugin = () => { return { name: 'msw-plugin', apply: 'serve', - configResolved (conf) { + configResolved(conf) { projectRoot = conf.root }, - configureServer (server) { + configureServer(server) { server.middlewares.use(async (req, res, next) => { if (req.path === '/mockServiceWorker.js') { const file = await readFile(resolve(projectRoot, target)) @@ -21,7 +21,7 @@ const mswPlugin = () => { next() } }) - } + }, } } diff --git a/build/service_worker_messages.js b/build/service_worker_messages.js index c078e8563..0948aa919 100644 --- a/build/service_worker_messages.js +++ b/build/service_worker_messages.js @@ -1,11 +1,12 @@ -import { languages, langCodeToJsonName } from '../src/i18n/languages.js' import { readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' +import { langCodeToJsonName, languages } from '../src/i18n/languages.js' + const i18nDir = resolve( dirname(dirname(fileURLToPath(import.meta.url))), - 'src/i18n' + 'src/i18n', ) export const i18nFiles = languages.reduce((acc, lang) => { @@ -16,13 +17,15 @@ export const i18nFiles = languages.reduce((acc, lang) => { }, {}) export const generateServiceWorkerMessages = async () => { - const msgArray = await Promise.all(Object.entries(i18nFiles).map(async ([lang, file]) => { - const fileContent = await readFile(file, 'utf-8') - const msg = { - notifications: JSON.parse(fileContent).notifications || {} - } - return [lang, msg] - })) + const msgArray = await Promise.all( + Object.entries(i18nFiles).map(async ([lang, file]) => { + const fileContent = await readFile(file, 'utf-8') + const msg = { + notifications: JSON.parse(fileContent).notifications || {}, + } + return [lang, msg] + }), + ) return msgArray.reduce((acc, [lang, msg]) => { acc[lang] = msg return acc diff --git a/build/sw_plugin.js b/build/sw_plugin.js index a2c792b7d..03c5978d7 100644 --- a/build/sw_plugin.js +++ b/build/sw_plugin.js @@ -1,9 +1,13 @@ -import { fileURLToPath } from 'node:url' -import { dirname, resolve } from 'node:path' import { readFile } from 'node:fs/promises' -import { build } from 'vite' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' import * as esbuild from 'esbuild' -import { generateServiceWorkerMessages, i18nFiles } from './service_worker_messages.js' +import { build } from 'vite' + +import { + generateServiceWorkerMessages, + i18nFiles, +} from './service_worker_messages.js' const getSWMessagesAsText = async () => { const messages = await generateServiceWorkerMessages() @@ -14,14 +18,10 @@ 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)} };` +const getProdSwEnv = ({ assets }) => + `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };` -export const devSwPlugin = ({ - swSrc, - swDest, - transformSW, - alias -}) => { +export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { const swFullSrc = resolve(projectRoot, swSrc) const esbuildAlias = {} Object.entries(alias).forEach(([source, dest]) => { @@ -31,9 +31,10 @@ export const devSwPlugin = ({ return { name: 'dev-sw-plugin', apply: 'serve', - configResolved (conf) { + configResolved() { + /* no-op */ }, - resolveId (id) { + resolveId(id) { const name = id.startsWith('/') ? id.slice(1) : id if (name === swDest) { return swFullSrc @@ -42,7 +43,7 @@ export const devSwPlugin = ({ } return null }, - async load (id) { + async load(id) { if (id === swFullSrc) { return readFile(swFullSrc, 'utf-8') } else if (id === swEnvNameResolved) { @@ -55,7 +56,7 @@ export const devSwPlugin = ({ * during dev, and firefox does not support ESM as service worker * https://bugzilla.mozilla.org/show_bug.cgi?id=1360870 */ - async transform (code, id) { + async transform(code, id) { if (id === swFullSrc && transformSW) { const res = await esbuild.build({ entryPoints: [swSrc], @@ -63,52 +64,54 @@ export const devSwPlugin = ({ 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' + plugins: [ + { + name: 'vite-like-root-resolve', + setup(b) { + b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({ + path: resolve(projectRoot, args.path.slice(1)), })) - b.onLoad( - { filter: /.*/, namespace: 'sw-messages' }, - async () => ({ - contents: await getSWMessagesAsText() + }, + }, + { + 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(), })) - } - }, { - 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 } - } + }, } } @@ -118,16 +121,13 @@ export const devSwPlugin = ({ // however, we must compile the service worker to iife because of browser support. // Run another vite build just for the service worker targeting iife at // the end of the build. -export const buildSwPlugin = ({ - swSrc, - swDest, -}) => { +export const buildSwPlugin = ({ swSrc, swDest }) => { let config return { name: 'build-sw-plugin', enforce: 'post', apply: 'build', - configResolved (resolvedConfig) { + configResolved(resolvedConfig) { config = { define: resolvedConfig.define, resolve: resolvedConfig.resolve, @@ -138,50 +138,50 @@ export const buildSwPlugin = ({ lib: { entry: swSrc, formats: ['iife'], - name: 'sw_pleroma' + name: 'sw_pleroma', }, emptyOutDir: false, rollupOptions: { output: { - entryFileNames: swDest - } - } + entryFileNames: swDest, + }, + }, }, - configFile: false + configFile: false, } }, generateBundle: { order: 'post', sequential: true, - async handler (_, bundle) { + async handler(_, bundle) { const assets = Object.keys(bundle) - .filter(name => !/\.map$/.test(name)) - .map(name => '/' + name) + .filter((name) => !/\.map$/.test(name)) + .map((name) => '/' + name) config.plugins.push({ name: 'build-sw-env-plugin', - resolveId (id) { + resolveId(id) { if (id === swEnvName) { return swEnvNameResolved } return null }, - load (id) { + load(id) { if (id === swEnvNameResolved) { return getProdSwEnv({ assets }) } return null - } + }, }) - } + }, }, closeBundle: { order: 'post', sequential: true, - async handler () { - console.log('Building service worker for production') + async handler() { + console.info('Building service worker for production') await build(config) - } - } + }, + }, } } @@ -191,9 +191,9 @@ const swMessagesNameResolved = '\0' + swMessagesName export const swMessagesPlugin = () => { return { name: 'sw-messages-plugin', - resolveId (id) { + resolveId(id) { if (id === swMessagesName) { - Object.values(i18nFiles).forEach(f => { + Object.values(i18nFiles).forEach((f) => { this.addWatchFile(f) }) return swMessagesNameResolved @@ -201,11 +201,11 @@ export const swMessagesPlugin = () => { return null } }, - async load (id) { + async load(id) { if (id === swMessagesNameResolved) { return await getSWMessagesAsText() } return null - } + }, } } diff --git a/build/update-emoji.js b/build/update-emoji.js index 5d578ba61..4ff7e1de8 100644 --- a/build/update-emoji.js +++ b/build/update-emoji.js @@ -1,22 +1,21 @@ - -import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' } +import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { + type: 'json', +} import fs from 'fs' -Object.keys(emojis) - .map(k => { - emojis[k].map(e => { - delete e.unicode_version - delete e.emoji_version - delete e.skin_tone_support_unicode_version - }) +Object.keys(emojis).map((k) => { + emojis[k].map((e) => { + delete e.unicode_version + delete e.emoji_version + delete e.skin_tone_support_unicode_version }) +}) const res = {} -Object.keys(emojis) - .map(k => { - const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() - res[groupId] = emojis[k] - }) +Object.keys(emojis).map((k) => { + const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() + res[groupId] = emojis[k] +}) console.info('Updating emojis...') fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res)) diff --git a/changelog.d/action-button-extra-counter.add b/changelog.d/action-button-extra-counter.add deleted file mode 100644 index 7d5c77447..000000000 --- a/changelog.d/action-button-extra-counter.add +++ /dev/null @@ -1 +0,0 @@ -Display counter for status action buttons when they are on the menu diff --git a/changelog.d/actor-type.fix b/changelog.d/actor-type.fix new file mode 100644 index 000000000..a2c873c1a --- /dev/null +++ b/changelog.d/actor-type.fix @@ -0,0 +1 @@ +fixed being unable to set actor type from profile page diff --git a/changelog.d/akkoma-sharkey-net-support.add b/changelog.d/akkoma-sharkey-net-support.add deleted file mode 100644 index 4b4bff7fe..000000000 --- a/changelog.d/akkoma-sharkey-net-support.add +++ /dev/null @@ -1 +0,0 @@ -Added support for Akkoma and IceShrimp.NET backend diff --git a/changelog.d/arithmetic-blend.add b/changelog.d/arithmetic-blend.add deleted file mode 100644 index c579dca28..000000000 --- a/changelog.d/arithmetic-blend.add +++ /dev/null @@ -1,2 +0,0 @@ -Add arithmetic blend ISS function - diff --git a/changelog.d/better-scroll-button.add b/changelog.d/better-scroll-button.add deleted file mode 100644 index b206869d1..000000000 --- a/changelog.d/better-scroll-button.add +++ /dev/null @@ -1 +0,0 @@ -Add support for detachable scrollTop button diff --git a/changelog.d/akkoma.skip b/changelog.d/biome.skip similarity index 100% rename from changelog.d/akkoma.skip rename to changelog.d/biome.skip diff --git a/changelog.d/bookmark-button-align.fix b/changelog.d/bookmark-button-align.fix deleted file mode 100644 index 64bc2c807..000000000 --- a/changelog.d/bookmark-button-align.fix +++ /dev/null @@ -1 +0,0 @@ -Fix bookmark button alignment in the extra actions menu diff --git a/changelog.d/csp.add b/changelog.d/csp.add deleted file mode 100644 index 260337b97..000000000 --- a/changelog.d/csp.add +++ /dev/null @@ -1 +0,0 @@ -Compatibility with stricter CSP (Akkoma backend) diff --git a/changelog.d/e2e-tests.add b/changelog.d/e2e-tests.add new file mode 100644 index 000000000..ba62b25ac --- /dev/null +++ b/changelog.d/e2e-tests.add @@ -0,0 +1 @@ +Add playwright E2E-tests with an optional docker-based backend diff --git a/changelog.d/e2e.skip b/changelog.d/e2e.skip new file mode 100644 index 000000000..e84c25121 --- /dev/null +++ b/changelog.d/e2e.skip @@ -0,0 +1 @@ +fix e2e diff --git a/changelog.d/filter-fixes.skip b/changelog.d/filter-fixes.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/fix-wrap.skip b/changelog.d/fix-wrap.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/migrate-auth-flow-pinia.skip b/changelog.d/migrate-auth-flow-pinia.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/migrate-oauth-tokens-module-to-pinia-store.skip b/changelog.d/migrate-oauth-tokens-module-to-pinia-store.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/mutes-sync.add b/changelog.d/mutes-sync.add deleted file mode 100644 index e8e0e462a..000000000 --- a/changelog.d/mutes-sync.add +++ /dev/null @@ -1 +0,0 @@ -Synchronized mutes, advanced mute control (regexp, expiry, naming) diff --git a/changelog.d/profile-error.fix b/changelog.d/profile-error.fix deleted file mode 100644 index f123db5ae..000000000 --- a/changelog.d/profile-error.fix +++ /dev/null @@ -1 +0,0 @@ -Fix error styling for user profiles diff --git a/changelog.d/small-fixes.skip b/changelog.d/small-fixes.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/sw-cache-assets.add b/changelog.d/sw-cache-assets.add deleted file mode 100644 index 5f7414eee..000000000 --- a/changelog.d/sw-cache-assets.add +++ /dev/null @@ -1 +0,0 @@ -Cache assets and emojis with service worker diff --git a/changelog.d/theme3-body-class.add b/changelog.d/theme3-body-class.add deleted file mode 100644 index f3d36fd70..000000000 --- a/changelog.d/theme3-body-class.add +++ /dev/null @@ -1 +0,0 @@ -Indicate currently active V3 theme as a body element class diff --git a/changelog.d/unify-show-hide-buttons.add b/changelog.d/unify-show-hide-buttons.add deleted file mode 100644 index 663bc38a5..000000000 --- a/changelog.d/unify-show-hide-buttons.add +++ /dev/null @@ -1 +0,0 @@ -Unify show/hide content buttons diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 000000000..75a4979a1 --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,57 @@ +services: + db: + image: postgres:15-alpine + environment: + POSTGRES_USER: pleroma + POSTGRES_PASSWORD: pleroma + POSTGRES_DB: pleroma + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pleroma -d pleroma"] + interval: 2s + timeout: 2s + retries: 30 + + pleroma: + image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable} + environment: + DB_USER: pleroma + DB_PASS: pleroma + DB_NAME: pleroma + DB_HOST: db + DB_PORT: 5432 + DOMAIN: localhost + INSTANCE_NAME: Pleroma E2E + ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com} + NOTIFY_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com} + E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin} + E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin} + E2E_ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com} + depends_on: + db: + condition: service_healthy + volumes: + - ./docker/pleroma/entrypoint.e2e.sh:/opt/pleroma/entrypoint.e2e.sh:ro + entrypoint: ["/bin/ash", "/opt/pleroma/entrypoint.e2e.sh"] + healthcheck: + # NOTE: "localhost" may resolve to ::1 in some images (IPv6) while Pleroma only + # listens on IPv4 in this container. Use 127.0.0.1 to avoid false negatives. + test: ["CMD-SHELL", "test -f /var/lib/pleroma/.e2e_seeded && wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null || exit 1"] + interval: 5s + timeout: 3s + retries: 60 + + e2e: + build: + context: . + dockerfile: docker/e2e/Dockerfile.e2e + depends_on: + pleroma: + condition: service_healthy + environment: + CI: "1" + VITE_PROXY_TARGET: http://pleroma:4000 + VITE_PROXY_ORIGIN: http://localhost:4000 + E2E_BASE_URL: http://localhost:8080 + 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 new file mode 100644 index 000000000..e84359ceb --- /dev/null +++ b/docker/e2e/Dockerfile.e2e @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/playwright:v1.57.0-jammy + +WORKDIR /app + +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + +RUN npm install -g yarn@1.22.22 + +COPY package.json yarn.lock ./ +RUN yarn --frozen-lockfile + +COPY . . + +ENV CI=1 + +CMD ["yarn", "e2e:pw"] diff --git a/docker/pleroma/entrypoint.e2e.sh b/docker/pleroma/entrypoint.e2e.sh new file mode 100644 index 000000000..96920eeae --- /dev/null +++ b/docker/pleroma/entrypoint.e2e.sh @@ -0,0 +1,71 @@ +#!/bin/ash + +set -eu + +SEED_SENTINEL_PATH="/var/lib/pleroma/.e2e_seeded" +CONFIG_OVERRIDE_PATH="/var/lib/pleroma/config.exs" + +echo "-- Waiting for database..." +while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do + sleep 1s +done + +echo "-- Writing E2E config overrides..." +cat > "$CONFIG_OVERRIDE_PATH" <<'EOF' +import Config + +config :pleroma, Pleroma.Captcha, + enabled: false + +config :pleroma, :instance, + registrations_open: true, + account_activation_required: false, + approval_required: false +EOF + +echo "-- Running migrations..." +/opt/pleroma/bin/pleroma_ctl migrate + +echo "-- Starting!" +/opt/pleroma/bin/pleroma start & +PLEROMA_PID="$!" + +cleanup() { + if [ -n "${PLEROMA_PID:-}" ] && kill -0 "$PLEROMA_PID" 2>/dev/null; then + kill -TERM "$PLEROMA_PID" + wait "$PLEROMA_PID" || true + fi +} + +trap cleanup INT TERM + +echo "-- Waiting for API..." +api_ok="false" +for _i in $(seq 1 120); do + if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then + api_ok="true" + break + fi + sleep 1s +done + +if [ "$api_ok" != "true" ]; then + echo "Timed out waiting for Pleroma API to become available" + exit 1 +fi + +if [ ! -f "$SEED_SENTINEL_PATH" ]; then + if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then + echo "-- Seeding admin user (${E2E_ADMIN_USERNAME})..." + if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then + echo "-- User already exists (or creation failed), ensuring admin + confirmed..." + /opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed + fi + else + echo "-- Skipping admin seeding (missing E2E_ADMIN_* env)" + fi + + touch "$SEED_SENTINEL_PATH" +fi + +wait "$PLEROMA_PID" diff --git a/eslint.config.mjs b/eslint.config.mjs index 01bdb2038..417ff8cf3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,37 +1,34 @@ -import vue from "eslint-plugin-vue"; -import js from "@eslint/js"; -import globals from "globals"; +import js from '@eslint/js' +import { defineConfig, globalIgnores } from 'eslint/config' +import vue from 'eslint-plugin-vue' +import globals from 'globals' - -export default [ +export default defineConfig([ ...vue.configs['flat/recommended'], - js.configs.recommended, + globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']), { - files: ["**/*.js", "**/*.mjs", "**/*.vue"], - ignores: ["build/*.js", "config/*.js"], - + files: ['src/**/*.vue'], + plugins: { js }, + extends: ['js/recommended'], languageOptions: { ecmaVersion: 2024, - sourceType: "module", + sourceType: 'module', parserOptions: { - parser: "@babel/eslint-parser", + parser: '@babel/eslint-parser', }, globals: { ...globals.browser, ...globals.vitest, ...globals.chai, ...globals.commonjs, - ...globals.serviceworker - } + ...globals.serviceworker, + }, }, rules: { - 'arrow-parens': 0, - 'generator-star-spacing': 0, - 'no-debugger': 0, 'vue/require-prop-types': 0, 'vue/multi-word-component-names': 0, - } - } -] + }, + }, +]) diff --git a/index.html b/index.html index 96c20c4b7..26eeee19b 100644 --- a/index.html +++ b/index.html @@ -11,14 +11,12 @@ - - - + -
+
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index c8bba4c44..cbe3dd80f 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -1,7 +1,7 @@