diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 3de57a360..000000000 --- a/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -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/.forgejo/issue_template/bug.yaml b/.forgejo/issue_template/bug.yaml deleted file mode 100644 index 082ee496e..000000000 --- a/.forgejo/issue_template/bug.yaml +++ /dev/null @@ -1,87 +0,0 @@ -name: 'Bug report' -about: 'Bug report for Pleroma FE' -labels: - - Bug -body: -- type: input - id: env-browser - attributes: - label: Browser and OS - description: What browser are you using, including version, and what OS are you running? - placeholder: Firefox 140, Arch Linux - validations: - required: true -- type: input - id: env-instance - attributes: - label: Instance URL - validations: - required: false -- type: input - id: env-backend - attributes: - label: Backend version information - description: Backend version being used. (See Settings->Show advanced->Developer) - placeholder: Pleroma BE 2.10 - validations: - required: true -- type: input - id: env-frontend - attributes: - label: Frontend version information - description: Frontend version being used. (See Settings->Show advanced->Developer) - placeholder: Pleroma FE 2.10 - validations: - required: true -- type: input - id: env-extensions - attributes: - label: Browser extensions - description: List of browser extensions you are using, like uBlock, rikaichamp etc. If none leave empty. - validations: - required: false -- type: input - id: env-modifications - attributes: - label: Known instance/user customizations - description: Whether you are using a Pleroma FE fork, any mods mods or instance level styles among others. - validations: - required: false -- type: textarea - id: bug-text - attributes: - label: Bug description - description: A short description of the bug. Images can be helpful. - validations: - required: true -- type: textarea - id: bug-reproducer - attributes: - label: Reproduction steps - description: Ordered list of reproduction steps needed to make the bug happen. If you don't have reproduction steps, leave this empty. - placeholder: | - 1. Log in with a fresh browser session - 2. Open timeline X - 3. Click on button Y - 4. Z broke - validations: - required: false -- type: textarea - id: bug-seriousness - attributes: - label: Bug seriousness - value: | - * How annoying it is: - * How often does it happen: - * How many people does it affect: - * Is there a workaround for it: -- type: checkboxes - id: duplicate-issues - attributes: - label: Duplicate issues - hide_label: true - description: Before submitting this issue, search for same or similar issues on the [Pleroma FE bug tracker](https://git.pleroma.social/pleroma/pleroma-fe/issues). - options: - - label: I've searched for same or similar issues before submitting this issue. - required: true - visible: [form] diff --git a/.forgejo/issue_template/suggestion.yaml b/.forgejo/issue_template/suggestion.yaml deleted file mode 100644 index c1531d8e3..000000000 --- a/.forgejo/issue_template/suggestion.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: 'Feature request / Suggestion / Improvement' -about: 'Feature requests, suggestions and improvements for Pleroma FE' -labels: - - Feature Request / Enhancement -body: -- type: textarea - id: issue-text - attributes: - label: Proposal - placeholder: Make groups happen! - validations: - required: true -- type: checkboxes - id: duplicate-issues - attributes: - label: Duplicate issues - hide_label: true - description: Before submitting this issue, search for same or similar requests on the [Pleroma FE bug tracker](https://git.pleroma.social/pleroma/pleroma-fe/issues). - options: - - label: I've searched for same or similar requests before submitting this issue. - required: true - visible: [form] diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md deleted file mode 100644 index d2d7689bd..000000000 --- a/.forgejo/pull_request_template.md +++ /dev/null @@ -1,12 +0,0 @@ -### Checklist -- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `.`. - - diff --git a/.gitignore b/.gitignore index c4a96ee1e..01ffda9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,8 @@ 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 06fbf45f9..99c85dd36 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,23 +34,12 @@ check-changelog: - apk add git - sh ./tools/check-changelog -lint-eslint: +lint: stage: lint script: - yarn - - yarn ci-eslint - -lint-biome: - stage: lint - script: - - yarn - - yarn ci-biome - -lint-stylelint: - stage: lint - script: - - yarn - - yarn ci-stylelint + - yarn lint + - yarn stylelint test: stage: test @@ -71,135 +60,6 @@ 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 deleted file mode 100644 index d02e14a73..000000000 --- a/.gitlab/merge_request_templates/Release.md +++ /dev/null @@ -1,8 +0,0 @@ -### 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 afdfd5f5b..c91107595 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -12,8 +12,6 @@ "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 1eb5a9cb4..c2f0e7d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,76 +2,6 @@ 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.1 -### Fixed -- fixed being unable to set actor type from profile page -- fixed error when clicking mute menu itself (instead of submenu items) -- fixed mute -> domain status submenu not working - -### Internal -- Add playwright E2E-tests with an optional docker-based backend - -## 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 @@ -104,8 +34,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/README.md b/README.md index 16d32dcd2..6a37195d5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # For Translators -To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/languages.js). +To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/languages.js). Pleroma-FE will set your language by your browser locale, but you can change language in settings. @@ -32,10 +32,10 @@ yarn unit # For Contributors: -You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/src/config/local.example.json)) to enable some convenience dev options: +You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options: * `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases. -* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/pleroma/frontend_configurations`. Only works in dev mode. +* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode. FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE. diff --git a/biome.json b/biome.json deleted file mode 100644 index 6a464a0e5..000000000 --- a/biome.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "$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/components/**"], - ":BLANK_LINE:", - [":PATH:", "src/stores/**"], - ":BLANK_LINE:", - [":PATH:", "src/**", "src/stores/**", "src/components/**"], - ":BLANK_LINE:", - "@fortawesome/fontawesome-svg-core", - "@fortawesome/*" - ] - } - } - } - } - } -} diff --git a/build/check-versions.mjs b/build/check-versions.mjs index 8c5968a30..73c1eeb15 100644 --- a/build/check-versions.mjs +++ b/build/check-versions.mjs @@ -1,5 +1,5 @@ -import chalk from 'chalk' import semver from 'semver' +import chalk from 'chalk' 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,22 +16,15 @@ 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 c60355804..c104af5d9 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 4f020f359..a783fe7ff 100644 --- a/build/copy_plugin.js +++ b/build/copy_plugin.js @@ -1,8 +1,8 @@ -import { cp } from 'node:fs/promises' -import { resolve } from 'node:path' import serveStatic from 'serve-static' +import { resolve } from 'node:path' +import { cp } from 'node:fs/promises' -const getPrefix = (s) => { +const getPrefix = s => { const padEnd = s.endsWith('/') ? s : s + '/' return padEnd.startsWith('/') ? padEnd : '/' + padEnd } @@ -13,31 +13,28 @@ const copyPlugin = ({ inUrl, inFs }) => { let copyTarget const handler = serveStatic(inFs) - return [ - { - name: 'copy-plugin-serve', - apply: 'serve', - configureServer(server) { - server.middlewares.use(prefix, handler) - }, + 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) }, - { - 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.') - }, - }, - }, - ] + closeBundle: { + order: 'post', + sequential: true, + async handler () { + console.log(`Copying '${inFs}' to ${copyTarget}...`) + await cp(inFs, copyTarget, { recursive: true }) + console.log('Done.') + } + } + }] } export default copyPlugin diff --git a/build/emojis_plugin.js b/build/emojis_plugin.js index 9872f5331..aed52066d 100644 --- a/build/emojis_plugin.js +++ b/build/emojis_plugin.js @@ -1,23 +1,21 @@ -import { access } from 'node:fs/promises' import { resolve } from 'node:path' - -import { languages } from '../src/i18n/languages.js' +import { access } from 'node:fs/promises' +import { languages, langCodeToCldrName } 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) @@ -25,18 +23,11 @@ const getAllAccessibleAnnotations = async (projectRoot) => { await access(importFile) return `'${lang}': () => import('${importModule}')` } catch (e) { - if (e.message.match(/ENOENT/)) { - console.warn(`Missing emoji annotations locale: ${destLang}`) - } else { - console.error('test', e.message) - } return } - }), - ) - ) - .filter((k) => k) - .join(',\n') + }))) + .filter(k => k) + .join(',\n') return ` export const annotationsLoader = { @@ -52,21 +43,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 c4e9098c5..f544348fc 100644 --- a/build/msw_plugin.js +++ b/build/msw_plugin.js @@ -1,5 +1,5 @@ -import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' +import { readFile } from 'node:fs/promises' 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 0948aa919..c078e8563 100644 --- a/build/service_worker_messages.js +++ b/build/service_worker_messages.js @@ -1,12 +1,11 @@ +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) => { @@ -17,15 +16,13 @@ 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 03c5978d7..a2c792b7d 100644 --- a/build/sw_plugin.js +++ b/build/sw_plugin.js @@ -1,13 +1,9 @@ -import { readFile } from 'node:fs/promises' -import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import * as esbuild from 'esbuild' +import { dirname, resolve } from 'node:path' +import { readFile } from 'node:fs/promises' import { build } from 'vite' - -import { - generateServiceWorkerMessages, - i18nFiles, -} from './service_worker_messages.js' +import * as esbuild from 'esbuild' +import { generateServiceWorkerMessages, i18nFiles } from './service_worker_messages.js' const getSWMessagesAsText = async () => { const messages = await generateServiceWorkerMessages() @@ -18,10 +14,14 @@ 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,10 +31,9 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { return { name: 'dev-sw-plugin', apply: 'serve', - configResolved() { - /* no-op */ + configResolved (conf) { }, - resolveId(id) { + resolveId (id) { const name = id.startsWith('/') ? id.slice(1) : id if (name === swDest) { return swFullSrc @@ -43,7 +42,7 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { } return null }, - async load(id) { + async load (id) { if (id === swFullSrc) { return readFile(swFullSrc, 'utf-8') } else if (id === swEnvNameResolved) { @@ -56,7 +55,7 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { * 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], @@ -64,54 +63,52 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { 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)), + 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' })) - }, - }, - { - 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(), + 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 } - }, + } } } @@ -121,13 +118,16 @@ export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { // 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 = ({ swSrc, swDest }) => { 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.info('Building service worker for production') + async handler () { + console.log('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 4ff7e1de8..5d578ba61 100644 --- a/build/update-emoji.js +++ b/build/update-emoji.js @@ -1,21 +1,22 @@ -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 new file mode 100644 index 000000000..7d5c77447 --- /dev/null +++ b/changelog.d/action-button-extra-counter.add @@ -0,0 +1 @@ +Display counter for status action buttons when they are on the menu diff --git a/changelog.d/akkoma-sharkey-net-support.add b/changelog.d/akkoma-sharkey-net-support.add new file mode 100644 index 000000000..4b4bff7fe --- /dev/null +++ b/changelog.d/akkoma-sharkey-net-support.add @@ -0,0 +1 @@ +Added support for Akkoma and IceShrimp.NET backend diff --git a/changelog.d/instance-store-migration.skip b/changelog.d/akkoma.skip similarity index 100% rename from changelog.d/instance-store-migration.skip rename to changelog.d/akkoma.skip diff --git a/changelog.d/arithmetic-blend.add b/changelog.d/arithmetic-blend.add new file mode 100644 index 000000000..c579dca28 --- /dev/null +++ b/changelog.d/arithmetic-blend.add @@ -0,0 +1,2 @@ +Add arithmetic blend ISS function + diff --git a/changelog.d/attrs-parsing.fix b/changelog.d/attrs-parsing.fix deleted file mode 100644 index e36e59a86..000000000 --- a/changelog.d/attrs-parsing.fix +++ /dev/null @@ -1 +0,0 @@ -Fix HTML attribute parsing for escaped quotes \ No newline at end of file diff --git a/changelog.d/better-scroll-button.add b/changelog.d/better-scroll-button.add new file mode 100644 index 000000000..b206869d1 --- /dev/null +++ b/changelog.d/better-scroll-button.add @@ -0,0 +1 @@ +Add support for detachable scrollTop button diff --git a/changelog.d/bookmark-button-align.fix b/changelog.d/bookmark-button-align.fix new file mode 100644 index 000000000..64bc2c807 --- /dev/null +++ b/changelog.d/bookmark-button-align.fix @@ -0,0 +1 @@ +Fix bookmark button alignment in the extra actions menu diff --git a/changelog.d/csp.add b/changelog.d/csp.add new file mode 100644 index 000000000..260337b97 --- /dev/null +++ b/changelog.d/csp.add @@ -0,0 +1 @@ +Compatibility with stricter CSP (Akkoma backend) diff --git a/changelog.d/filter-fixes.skip b/changelog.d/filter-fixes.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/fix-emojis-breaking-bio.fix b/changelog.d/fix-emojis-breaking-bio.fix deleted file mode 100644 index 62a607d8a..000000000 --- a/changelog.d/fix-emojis-breaking-bio.fix +++ /dev/null @@ -1 +0,0 @@ -Fix emojis breaking user bio/description editing diff --git a/changelog.d/fix-wrap.skip b/changelog.d/fix-wrap.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/migrate-auth-flow-pinia.skip b/changelog.d/migrate-auth-flow-pinia.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/migrate-oauth-tokens-module-to-pinia-store.skip b/changelog.d/migrate-oauth-tokens-module-to-pinia-store.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/mutes-sync.add b/changelog.d/mutes-sync.add new file mode 100644 index 000000000..e8e0e462a --- /dev/null +++ b/changelog.d/mutes-sync.add @@ -0,0 +1 @@ +Synchronized mutes, advanced mute control (regexp, expiry, naming) diff --git a/changelog.d/profile-error.fix b/changelog.d/profile-error.fix new file mode 100644 index 000000000..f123db5ae --- /dev/null +++ b/changelog.d/profile-error.fix @@ -0,0 +1 @@ +Fix error styling for user profiles diff --git a/changelog.d/small-fixes.skip b/changelog.d/small-fixes.skip new file mode 100644 index 000000000..e69de29bb diff --git a/changelog.d/sw-cache-assets.add b/changelog.d/sw-cache-assets.add new file mode 100644 index 000000000..5f7414eee --- /dev/null +++ b/changelog.d/sw-cache-assets.add @@ -0,0 +1 @@ +Cache assets and emojis with service worker diff --git a/changelog.d/theme3-body-class.add b/changelog.d/theme3-body-class.add new file mode 100644 index 000000000..f3d36fd70 --- /dev/null +++ b/changelog.d/theme3-body-class.add @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000..663bc38a5 --- /dev/null +++ b/changelog.d/unify-show-hide-buttons.add @@ -0,0 +1 @@ +Unify show/hide content buttons diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml deleted file mode 100644 index 75a4979a1..000000000 --- a/docker-compose.e2e.yml +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index e84359ceb..000000000 --- a/docker/e2e/Dockerfile.e2e +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 96920eeae..000000000 --- a/docker/pleroma/entrypoint.e2e.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/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/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8ca076931..dfc5f9dc3 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -7,9 +7,9 @@ PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it) -1. `/api/pleroma/frontend_configurations` - this is generated by backend and includes FE/Client-specific data. PleromaFE uses the `pleroma_fe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations) -2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/src/public/static/config.json). -3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/src/src/stores/instance.js) ) +1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations) +2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json). +3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) ) ## Instance-defaults diff --git a/docs/HACKING.md b/docs/HACKING.md index 88760b77a..a5c491136 100644 --- a/docs/HACKING.md +++ b/docs/HACKING.md @@ -79,7 +79,7 @@ server { In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](../backend/API/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure. -PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/src/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation. +PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation. For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters. diff --git a/eslint.config.mjs b/eslint.config.mjs index 417ff8cf3..01bdb2038 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,34 +1,37 @@ -import js from '@eslint/js' -import { defineConfig, globalIgnores } from 'eslint/config' -import vue from 'eslint-plugin-vue' -import globals from 'globals' +import vue from "eslint-plugin-vue"; +import js from "@eslint/js"; +import globals from "globals"; -export default defineConfig([ + +export default [ ...vue.configs['flat/recommended'], - globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']), + js.configs.recommended, { - files: ['src/**/*.vue'], - plugins: { js }, - extends: ['js/recommended'], + files: ["**/*.js", "**/*.mjs", "**/*.vue"], + ignores: ["build/*.js", "config/*.js"], + 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 26eeee19b..96c20c4b7 100644 --- a/index.html +++ b/index.html @@ -11,12 +11,14 @@ - + + + -
+
- { return null } if (!staticInitialResults) { - staticInitialResults = JSON.parse( - document.getElementById('initial-results').textContent, - ) + staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent) } return staticInitialResults } @@ -75,7 +54,7 @@ const preloadFetch = async (request) => { return { ok: true, json: () => requestData, - text: () => requestData, + text: () => requestData } } @@ -84,38 +63,20 @@ const getInstanceConfig = async ({ store }) => { const res = await preloadFetch('/api/v1/instance') if (res.ok) { const data = await res.json() - const textLimit = data.max_toot_chars + const textlimit = data.max_toot_chars const vapidPublicKey = data.pleroma.vapid_public_key - useInstanceCapabilitiesStore().set( - 'pleromaExtensionsAvailable', - data.pleroma, - ) - useInstanceStore().set({ - path: 'limits.textLimit', - value: textLimit, - }) - useInstanceStore().set({ - path: 'accountApprovalRequired', - value: data.approval_required, - }) - useInstanceStore().set({ - path: 'birthdayRequired', - value: !!data.pleroma?.metadata.birthday_required, - }) - useInstanceStore().set({ - path: 'birthdayMinAge', - value: data.pleroma?.metadata.birthday_min_age || 0, - }) + store.dispatch('setInstanceOption', { name: 'pleromaExtensionsAvailable', value: data.pleroma }) + store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) + store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) + store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma?.metadata.birthday_required }) + store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma?.metadata.birthday_min_age || 0 }) if (vapidPublicKey) { - useInstanceStore().set({ - path: 'vapidPublicKey', - value: vapidPublicKey, - }) + store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) } } else { - throw res + throw (res) } } catch (error) { console.error('Could not load instance config, potentially fatal') @@ -132,12 +93,10 @@ const getBackendProvidedConfig = async () => { const data = await res.json() return data.pleroma_fe } else { - throw res + throw (res) } } catch (error) { - console.error( - 'Could not load backend-provided frontend config, potentially fatal', - ) + console.error('Could not load backend-provided frontend config, potentially fatal') console.error(error) } } @@ -148,7 +107,7 @@ const getStaticConfig = async () => { if (res.ok) { return res.json() } else { - throw res + throw (res) } } catch (error) { console.warn('Failed to load static/config.json, continuing without it.') @@ -170,21 +129,51 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { config = Object.assign({}, staticConfig, apiConfig) } - Object.keys(INSTANCE_IDENTITY_DEFAULT_DEFINITIONS).forEach((source) => - useInstanceStore().set({ - value: config[source], - path: `instanceIdentity.${source}`, - }), - ) + const copyInstanceOption = (name) => { + store.dispatch('setInstanceOption', { name, value: config[name] }) + } - Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) => - useInstanceStore().set({ - value: config[source], - path: `prefsStorage.${source}`, - }), - ) + copyInstanceOption('theme') + copyInstanceOption('style') + copyInstanceOption('palette') + copyInstanceOption('embeddedToS') + copyInstanceOption('nsfwCensorImage') + copyInstanceOption('background') + copyInstanceOption('hidePostStats') + copyInstanceOption('hideBotIndication') + copyInstanceOption('hideUserStats') + copyInstanceOption('hideFilteredStatuses') + copyInstanceOption('logo') + store.dispatch('setInstanceOption', { + name: 'logoMask', + value: typeof config.logoMask === 'undefined' + ? true + : config.logoMask + }) + + store.dispatch('setInstanceOption', { + name: 'logoMargin', + value: typeof config.logoMargin === 'undefined' + ? 0 + : config.logoMargin + }) + copyInstanceOption('logoLeft') useAuthFlowStore().setInitialStrategy(config.loginMethod) + + copyInstanceOption('redirectRootNoLogin') + copyInstanceOption('redirectRootLogin') + copyInstanceOption('showInstanceSpecificPanel') + copyInstanceOption('minimalScopesMode') + copyInstanceOption('hideMutedPosts') + copyInstanceOption('collapseMessageWithSubject') + copyInstanceOption('scopeCopy') + copyInstanceOption('subjectLineBehavior') + copyInstanceOption('postContentType') + copyInstanceOption('alwaysShowSubjectInput') + copyInstanceOption('showFeaturesPanel') + copyInstanceOption('hideSitename') + copyInstanceOption('sidebarRight') } const getTOS = async ({ store }) => { @@ -192,9 +181,9 @@ const getTOS = async ({ store }) => { const res = await window.fetch('/static/terms-of-service.html') if (res.ok) { const html = await res.text() - useInstanceStore().set({ path: 'instanceIdentity.tos', value: html }) + store.dispatch('setInstanceOption', { name: 'tos', value: html }) } else { - throw res + throw (res) } } catch (e) { console.warn("Can't load TOS\n", e) @@ -206,12 +195,9 @@ const getInstancePanel = async ({ store }) => { const res = await preloadFetch('/instance/panel.html') if (res.ok) { const html = await res.text() - useInstanceStore().set({ - path: 'instanceIdentity.instanceSpecificPanelContent', - value: html, - }) + store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) } else { - throw res + throw (res) } } catch (e) { console.warn("Can't load instance panel\n", e) @@ -223,27 +209,25 @@ const getStickers = async ({ store }) => { const res = await window.fetch('/static/stickers.json') if (res.ok) { const values = await res.json() - const stickers = ( - await Promise.all( - Object.entries(values).map(async ([name, path]) => { - const resPack = await window.fetch(path + 'pack.json') - let meta = {} - if (resPack.ok) { - meta = await resPack.json() - } - return { - pack: name, - path, - meta, - } - }), - ) - ).sort((a, b) => { + const stickers = (await Promise.all( + Object.entries(values).map(async ([name, path]) => { + const resPack = await window.fetch(path + 'pack.json') + let meta = {} + if (resPack.ok) { + meta = await resPack.json() + } + return { + pack: name, + path, + meta + } + }) + )).sort((a, b) => { return a.meta.title.localeCompare(b.meta.title) }) - useEmojiStore().setStickers(stickers) + store.dispatch('setInstanceOption', { name: 'stickers', value: stickers }) } else { - throw res + throw (res) } } catch (e) { console.warn("Can't load stickers\n", e) @@ -253,19 +237,13 @@ const getStickers = async ({ store }) => { const getAppSecret = async ({ store }) => { const oauth = useOAuthStore() if (oauth.userToken) { - store.commit( - 'setBackendInteractor', - backendInteractorService(oauth.getToken), - ) + store.commit('setBackendInteractor', backendInteractorService(oauth.getToken)) } } const resolveStaffAccounts = ({ store, accounts }) => { - const nicknames = accounts.map((uri) => uri.split('/').pop()) - useInstanceStore().set({ - path: 'staffAccounts', - value: nicknames, - }) + const nicknames = accounts.map(uri => uri.split('/').pop()) + store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames }) } const getNodeInfo = async ({ store }) => { @@ -276,165 +254,76 @@ const getNodeInfo = async ({ store }) => { const data = await res.json() const metadata = data.metadata const features = metadata.features - useInstanceStore().set({ - path: 'name', - value: metadata.nodeName, + store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName }) + store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) + store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) + store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) + store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') }) + store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) + store.dispatch('setInstanceOption', { + name: 'pleromaCustomEmojiReactionsAvailable', + value: + features.includes('pleroma_custom_emoji_reactions') || + features.includes('custom_emoji_reactions') }) - useInstanceStore().set({ - path: 'registrationOpen', - value: data.openRegistrations, - }) - useInstanceCapabilitiesStore().set( - 'mediaProxyAvailable', - features.includes('media_proxy'), - ) - useInstanceCapabilitiesStore().set( - 'safeDM', - features.includes('safe_dm_mentions'), - ) - useInstanceCapabilitiesStore().set( - 'shoutAvailable', - features.includes('chat'), - ) - useInstanceCapabilitiesStore().set( - 'pleromaChatMessagesAvailable', - features.includes('pleroma_chat_messages'), - ) - useInstanceCapabilitiesStore().set( - 'pleromaCustomEmojiReactionsAvailable', + store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') }) + store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) + store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) + store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') }) + store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) + store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) + store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') }) + store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') }) + store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] }) - features.includes('pleroma_custom_emoji_reactions') || - features.includes('custom_emoji_reactions'), - ) - useInstanceCapabilitiesStore().set( - 'pleromaBookmarkFoldersAvailable', - features.includes('pleroma:bookmark_folders'), - ) - useInstanceCapabilitiesStore().set( - 'gopherAvailable', - features.includes('gopher'), - ) - useInstanceCapabilitiesStore().set( - 'pollsAvailable', - features.includes('polls'), - ) - useInstanceCapabilitiesStore().set( - 'editingAvailable', - features.includes('editing'), - ) - useInstanceCapabilitiesStore().set( - 'mailerEnabled', - metadata.mailerEnabled, - ) - useInstanceCapabilitiesStore().set( - 'quotingAvailable', - features.includes('quote_posting'), - ) - useInstanceCapabilitiesStore().set( - 'groupActorAvailable', - features.includes('pleroma:group_actors'), - ) - useInstanceCapabilitiesStore().set( - 'blockExpiration', - features.includes('pleroma:block_expiration'), - ) - useInstanceStore().set({ - path: 'localBubbleInstances', - value: metadata.localBubbleInstances ?? [], - }) - useInstanceCapabilitiesStore().set( - 'localBubble', - (metadata.localBubbleInstances ?? []).length > 0, - ) - - useInstanceStore().set({ - path: 'limits.pollLimits', - value: metadata.pollLimits, - }) const uploadLimits = metadata.uploadLimits - useInstanceStore().set({ - path: 'limits.uploadlimit', - value: parseInt(uploadLimits.general), - }) - useInstanceStore().set({ - path: 'limits.avatarlimit', - value: parseInt(uploadLimits.avatar), - }) - useInstanceStore().set({ - path: 'limits.backgroundlimit', - value: parseInt(uploadLimits.background), - }) - useInstanceStore().set({ - path: 'limits.bannerlimit', - value: parseInt(uploadLimits.banner), - }) - useInstanceStore().set({ - path: 'limits.fieldsLimits', - value: metadata.fieldsLimits, - }) + store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) + store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) }) + store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) }) + store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) }) + store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits }) - useInstanceStore().set({ - path: 'restrictedNicknames', - value: metadata.restrictedNicknames, - }) - useInstanceCapabilitiesStore().set('postFormats', metadata.postFormats) + store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) + store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) const suggestions = metadata.suggestions - useInstanceCapabilitiesStore().set( - 'suggestionsEnabled', - suggestions.enabled, - ) - // this is unused, why? - useInstanceCapabilitiesStore().set('suggestionsWeb', suggestions.web) + store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) + store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) const software = data.software - useInstanceStore().set({ - path: 'backendVersion', - value: software.version, - }) - useInstanceStore().set({ - path: 'backendRepository', - value: software.repository, - }) + store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) + store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository }) const priv = metadata.private - useInstanceStore().set({ path: 'privateMode', value: priv }) + store.dispatch('setInstanceOption', { name: 'private', value: priv }) const frontendVersion = window.___pleromafe_commit_hash - useInstanceStore().set({ - path: 'frontendVersion', - value: frontendVersion, - }) + store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) const federation = metadata.federation - useInstanceCapabilitiesStore().set( - 'tagPolicyAvailable', - typeof federation.mrf_policies === 'undefined' + store.dispatch('setInstanceOption', { + name: 'tagPolicyAvailable', + value: typeof federation.mrf_policies === 'undefined' ? false - : metadata.federation.mrf_policies.includes('TagPolicy'), - ) - - useInstanceStore().set({ - path: 'federationPolicy', - value: federation, + : metadata.federation.mrf_policies.includes('TagPolicy') }) - useInstanceStore().set({ - path: 'federating', - value: - typeof federation.enabled === 'undefined' ? true : federation.enabled, + + store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation }) + store.dispatch('setInstanceOption', { + name: 'federating', + value: typeof federation.enabled === 'undefined' + ? true + : federation.enabled }) const accountActivationRequired = metadata.accountActivationRequired - useInstanceStore().set({ - path: 'accountActivationRequired', - value: accountActivationRequired, - }) + store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired }) const accounts = metadata.staffAccounts resolveStaffAccounts({ store, accounts }) } else { - throw res + throw (res) } } catch (e) { console.warn('Could not load nodeinfo') @@ -444,10 +333,7 @@ const getNodeInfo = async ({ store }) => { const setConfig = async ({ store }) => { // apiConfig, staticConfig - const configInfos = await Promise.all([ - getBackendProvidedConfig({ store }), - getStaticConfig(), - ]) + const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()]) const apiConfig = configInfos[0] const staticConfig = configInfos[1] @@ -478,37 +364,29 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { if (process.env.NODE_ENV === 'development') { // do some checks to avoid common errors if (!Object.keys(allStores).length) { - throw new Error( - 'No stores are available. Check the code in src/boot/after_store.js', - ) + throw new Error('No stores are available. Check the code in src/boot/after_store.js') } } await Promise.all( - Object.entries(allStores).map(async ([name, mod]) => { - const isStoreName = (name) => name.startsWith('use') - if (process.env.NODE_ENV === 'development') { - if (Object.keys(mod).filter(isStoreName).length !== 1) { - throw new Error( - 'Each store file must export exactly one store as a named export. Check your code in src/stores/', - ) + Object.entries(allStores) + .map(async ([name, mod]) => { + const isStoreName = name => name.startsWith('use') + if (process.env.NODE_ENV === 'development') { + if (Object.keys(mod).filter(isStoreName).length !== 1) { + throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/') + } } - } - const storeFuncName = Object.keys(mod).find(isStoreName) - if (storeFuncName && typeof mod[storeFuncName] === 'function') { - const p = mod[storeFuncName]().$persistLoaded - if (!(p instanceof Promise)) { - throw new Error( - `${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`, - ) + const storeFuncName = Object.keys(mod).find(isStoreName) + if (storeFuncName && typeof mod[storeFuncName] === 'function') { + const p = mod[storeFuncName]().$persistLoaded + if (!(p instanceof Promise)) { + throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`) + } + await p + } else { + throw new Error(`Store module ${name} does not export a 'use...' function`) } - await p - } else { - throw new Error( - `Store module ${name} does not export a 'use...' function`, - ) - } - }), - ) + })) } try { @@ -519,18 +397,11 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { } if (storageError) { - useInterfaceStore().pushGlobalNotice({ - messageKey: 'errors.storage_unavailable', - level: 'error', - }) + useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' }) } useInterfaceStore().setLayoutWidth(windowWidth()) useInterfaceStore().setLayoutHeight(windowHeight()) - window.syncConfig = useSyncConfigStore() - window.mergedConfig = useMergedConfigStore() - window.localConfig = useLocalConfigStore() - window.highlightConfig = useUserHighlightStore() FaviconService.initFaviconService() initServiceWorker(store) @@ -538,25 +409,18 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { window.addEventListener('focus', () => updateFocus()) const overrides = window.___pleromafe_dev_overrides || {} - const server = - typeof overrides.target !== 'undefined' - ? overrides.target - : window.location.origin - useInstanceStore().set({ path: 'server', value: server }) + const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin + store.dispatch('setInstanceOption', { name: 'server', value: server }) await setConfig({ store }) try { - await useInterfaceStore() - .applyTheme() - .catch((e) => { - console.error('Error setting theme', e) - }) + await useInterfaceStore().applyTheme().catch((e) => { console.error('Error setting theme', e) }) } catch (e) { window.splashError(e) return Promise.reject(e) } - applyStyleConfig(useMergedConfigStore().mergedConfig, i18n.global) + applyConfig(store.state.config, i18n.global) // Now we can try getting the server settings and logging in // Most of these are preloaded into the index.html so blocking is minimized @@ -564,8 +428,8 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { checkOAuthToken({ store }), getInstancePanel({ store }), getNodeInfo({ store }), - getInstanceConfig({ store }), - ]).catch((e) => Promise.reject(e)) + getInstanceConfig({ store }) + ]).catch(e => Promise.reject(e)) // Start fetching things that don't need to block the UI store.dispatch('fetchMutes') @@ -578,11 +442,11 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { history: createWebHistory(), routes: routes(store), scrollBehavior: (to, _from, savedPosition) => { - if (to.matched.some((m) => m.meta.dontScroll)) { + if (to.matched.some(m => m.meta.dontScroll)) { return false } return savedPosition || { left: 0, top: 0 } - }, + } }) useI18nStore().setI18n(i18n) diff --git a/src/boot/routes.js b/src/boot/routes.js index 193daf4a7..02abf8ce6 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -1,48 +1,42 @@ -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 BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue' +import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue' +import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue' import TagTimeline from 'components/tag_timeline/tag_timeline.vue' +import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue' +import ConversationPage from 'components/conversation-page/conversation-page.vue' +import Interactions from 'components/interactions/interactions.vue' +import DMs from 'components/dm_timeline/dm_timeline.vue' +import ChatList from 'components/chat_list/chat_list.vue' +import Chat from 'components/chat/chat.vue' import UserProfile from 'components/user_profile/user_profile.vue' +import Search from 'components/search/search.vue' +import Registration from 'components/registration/registration.vue' +import PasswordReset from 'components/password_reset/password_reset.vue' +import FollowRequests from 'components/follow_requests/follow_requests.vue' +import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' +import Notifications from 'components/notifications/notifications.vue' +import AuthForm from 'components/auth_form/auth_form.js' +import ShoutPanel from 'components/shout_panel/shout_panel.vue' import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' - +import About from 'components/about/about.vue' +import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue' +import Lists from 'components/lists/lists.vue' +import ListsTimeline from 'components/lists_timeline/lists_timeline.vue' +import ListsEdit from 'components/lists_edit/lists_edit.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 AnnouncementsPage from 'components/announcements_page/announcements_page.vue' import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' - -import { useInstanceStore } from 'src/stores/instance.js' -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' +import Drafts from 'components/drafts/drafts.vue' +import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue' +import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue' export default (store) => { const validateAuthenticatedRoute = (to, from, next) => { if (store.state.users.currentUser) { next() } else { - next( - useInstanceStore().instanceIdentity.redirectRootNoLogin || '/main/all', - ) + next(store.state.instance.redirectRootNoLogin || '/main/all') } } @@ -51,125 +45,46 @@ export default (store) => { name: 'root', path: '/', redirect: () => { - return ( - (store.state.users.currentUser - ? useInstanceStore().instanceIdentity.redirectRootLogin - : useInstanceStore().instanceIdentity.redirectRootNoLogin) || - '/main/all' - ) - }, - }, - { - name: 'public-external-timeline', - path: '/main/all', - component: PublicAndExternalTimeline, - }, - { - name: 'public-timeline', - path: '/main/public', - component: PublicTimeline, - }, - { - name: 'friends', - path: '/main/friends', - component: FriendsTimeline, - beforeEnter: validateAuthenticatedRoute, + return (store.state.users.currentUser + ? store.state.instance.redirectRootLogin + : store.state.instance.redirectRootNoLogin) || '/main/all' + } }, + { name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline }, + { name: 'public-timeline', path: '/main/public', component: PublicTimeline }, + { name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'bubble', path: '/bubble', component: BubbleTimeline }, - { - name: 'conversation', - path: '/notice/:id', - component: ConversationPage, - meta: { dontScroll: true }, - }, + { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline }, { name: 'remote-user-profile-acct', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', component: RemoteUserResolver, - beforeEnter: validateAuthenticatedRoute, + beforeEnter: validateAuthenticatedRoute }, { name: 'remote-user-profile', path: '/remote-users/:hostname/:username', component: RemoteUserResolver, - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'external-user-profile', - path: '/users/$:id', - component: UserProfile, - }, - { - name: 'interactions', - path: '/users/:username/interactions', - component: Interactions, - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'dms', - path: '/users/:username/dms', - component: DMs, - beforeEnter: validateAuthenticatedRoute, + beforeEnter: validateAuthenticatedRoute }, + { name: 'external-user-profile', path: '/users/$:id', component: UserProfile }, + { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, + { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'registration', path: '/registration', component: Registration }, - { - name: 'password-reset', - path: '/password-reset', - component: PasswordReset, - props: true, - }, - { - name: 'registration-token', - path: '/registration/:token', - component: Registration, - }, - { - name: 'friend-requests', - path: '/friend-requests', - component: FollowRequests, - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'notifications', - path: '/:username/notifications', - component: Notifications, - props: () => ({ disableTeleport: true }), - beforeEnter: validateAuthenticatedRoute, - }, + { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true }, + { name: 'registration-token', path: '/registration/:token', component: Registration }, + { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, + { name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute }, { name: 'login', path: '/login', component: AuthForm }, - { - name: 'shout-panel', - path: '/shout-panel', - component: ShoutPanel, - props: () => ({ floating: false }), - }, - { - name: 'oauth-callback', - path: '/oauth-callback', - component: OAuthCallback, - props: (route) => ({ code: route.query.code }), - }, - { - name: 'search', - path: '/search', - component: Search, - props: (route) => ({ query: route.query.query }), - }, - { - name: 'who-to-follow', - path: '/who-to-follow', - component: WhoToFollow, - beforeEnter: validateAuthenticatedRoute, - }, + { name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) }, + { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, + { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, + { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, { name: 'about', path: '/about', component: About }, - { - name: 'announcements', - path: '/announcements', - component: AnnouncementsPage, - }, + { name: 'announcements', path: '/announcements', component: AnnouncementsPage }, { name: 'drafts', path: '/drafts', component: Drafts }, { name: 'user-profile', path: '/users/:name', component: UserProfile }, { name: 'legacy-user-profile', path: '/:name', component: UserProfile }, @@ -177,51 +92,17 @@ export default (store) => { { 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', - component: NavPanel, - props: () => ({ forceExpand: true, forceEditMode: true }), - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'bookmark-folders', - path: '/bookmark_folders', - component: BookmarkFolders, - }, - { - name: 'bookmark-folder-new', - path: '/bookmarks/new-folder', - component: BookmarkFolderEdit, - }, - { - name: 'bookmark-folder', - path: '/bookmarks/:id', - component: BookmarkTimeline, - }, - { - name: 'bookmark-folder-edit', - path: '/bookmarks/:id/edit', - component: BookmarkFolderEdit, - }, + { name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute }, + { name: 'bookmark-folders', path: '/bookmark_folders', component: BookmarkFolders }, + { name: 'bookmark-folder-new', path: '/bookmarks/new-folder', component: BookmarkFolderEdit }, + { name: 'bookmark-folder', path: '/bookmarks/:id', component: BookmarkTimeline }, + { name: 'bookmark-folder-edit', path: '/bookmarks/:id/edit', component: BookmarkFolderEdit } ] - if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) { + if (store.state.instance.pleromaChatMessagesAvailable) { routes = routes.concat([ - { - name: 'chat', - path: '/users/:username/chats/:recipient_id', - component: Chat, - meta: { dontScroll: false }, - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'chats', - path: '/users/:username/chats', - component: ChatList, - meta: { dontScroll: false }, - beforeEnter: validateAuthenticatedRoute, - }, + { name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }, + { name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute } ]) } diff --git a/src/components/about/about.js b/src/components/about/about.js index 404843e8b..1df258450 100644 --- a/src/components/about/about.js +++ b/src/components/about/about.js @@ -1,11 +1,8 @@ -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 FeaturesPanel from '../features_panel/features_panel.vue' import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue' - -import { useInstanceStore } from 'src/stores/instance.js' -import { useMergedConfigStore } from 'src/stores/merged_config.js' +import StaffPanel from '../staff_panel/staff_panel.vue' +import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue' const About = { components: { @@ -13,20 +10,16 @@ const About = { FeaturesPanel, TermsOfServicePanel, StaffPanel, - MRFTransparencyPanel, + MRFTransparencyPanel }, computed: { - showFeaturesPanel() { - return useInstanceStore().instanceIdentity.showFeaturesPanel - }, - showInstanceSpecificPanel() { - return ( - useInstanceStore().instanceIdentity.showInstanceSpecificPanel && - !useMergedConfigStore().mergedConfig.hideISP && - useInstanceStore().instanceIdentity.instanceSpecificPanelContent - ) - }, - }, + showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, + showInstanceSpecificPanel () { + return this.$store.state.instance.showInstanceSpecificPanel && + !this.$store.getters.mergedConfig.hideISP && + this.$store.state.instance.instanceSpecificPanelContent + } + } } export default About diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index f204adbde..9a63f57eb 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -1,105 +1,99 @@ -import { mapState } from 'pinia' - -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 { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' - -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' -import { useMergedConfigStore } from 'src/stores/merged_config.js' +import Popover from '../popover/popover.vue' +import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisV +} from '@fortawesome/free-solid-svg-icons' import { useReportsStore } from 'src/stores/reports' -import { library } from '@fortawesome/fontawesome-svg-core' -import { faEllipsisV } from '@fortawesome/free-solid-svg-icons' - -library.add(faEllipsisV) +library.add( + faEllipsisV +) const AccountActions = { - props: ['user', 'relationship'], - data() { + props: [ + 'user', 'relationship' + ], + data () { return { showingConfirmBlock: false, - showingConfirmRemoveFollower: false, + showingConfirmRemoveFollower: false } }, components: { ProgressButton, Popover, UserListMenu, - ConfirmModal, - UserTimedFilterModal, + ConfirmModal }, methods: { - showConfirmRemoveUserFromFollowers() { - this.showingConfirmRemoveFollower = true + showConfirmBlock () { + this.showingConfirmBlock = true }, - hideConfirmRemoveUserFromFollowers() { - this.showingConfirmRemoveFollower = false - }, - hideConfirmBlock() { + hideConfirmBlock () { this.showingConfirmBlock = false }, - showRepeats() { + showConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = true + }, + hideConfirmRemoveUserFromFollowers () { + this.showingConfirmRemoveFollower = false + }, + showRepeats () { this.$store.dispatch('showReblogs', this.user.id) }, - hideRepeats() { + hideRepeats () { this.$store.dispatch('hideReblogs', this.user.id) }, - blockUser() { - if (this.$refs.timedBlockDialog) { - this.$refs.timedBlockDialog.optionallyPrompt() + blockUser () { + if (!this.shouldConfirmBlock) { + this.doBlockUser() } else { - if (!this.shouldConfirmBlock) { - this.doBlockUser() - } else { - this.showingConfirmBlock = true - } + this.showConfirmBlock() } }, - doBlockUser() { - this.$store.dispatch('blockUser', { id: this.user.id }) + doBlockUser () { + this.$store.dispatch('blockUser', this.user.id) this.hideConfirmBlock() }, - unblockUser() { + unblockUser () { this.$store.dispatch('unblockUser', this.user.id) }, - removeUserFromFollowers() { + removeUserFromFollowers () { if (!this.shouldConfirmRemoveUserFromFollowers) { this.doRemoveUserFromFollowers() } else { this.showConfirmRemoveUserFromFollowers() } }, - doRemoveUserFromFollowers() { + doRemoveUserFromFollowers () { this.$store.dispatch('removeUserFromFollowers', this.user.id) this.hideConfirmRemoveUserFromFollowers() }, - reportUser() { + reportUser () { useReportsStore().openUserReportingModal({ userId: this.user.id }) }, - openChat() { + openChat () { this.$router.push({ name: 'chat', - params: { - username: this.$store.state.users.currentUser.screen_name, - recipient_id: this.user.id, - }, + params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id } }) - }, + } }, computed: { - shouldConfirmBlock() { - return useMergedConfigStore().mergedConfig.modalOnBlock + shouldConfirmBlock () { + return this.$store.getters.mergedConfig.modalOnBlock }, - shouldConfirmRemoveUserFromFollowers() { - return useMergedConfigStore().mergedConfig.modalOnRemoveUserFromFollowers + shouldConfirmRemoveUserFromFollowers () { + return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers }, - ...mapState(useInstanceCapabilitiesStore, [ - 'blockExpiration', - 'pleromaChatMessagesAvailable', - ]), - }, + ...mapState({ + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + }) + } } export default AccountActions diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 94cb91ee0..fd4837ee4 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -3,6 +3,7 @@ diff --git a/src/components/alert.style.js b/src/components/alert.style.js index 8a6f842ed..868514764 100644 --- a/src/components/alert.style.js +++ b/src/components/alert.style.js @@ -1,51 +1,57 @@ export default { name: 'Alert', selector: '.alert', - validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'], + validInnerComponents: [ + 'Text', + 'Icon', + 'Link', + 'Border', + 'ButtonUnstyled' + ], variants: { normal: '.neutral', error: '.error', warning: '.warning', - success: '.success', + success: '.success' }, editor: { border: 1, - aspect: '3 / 1', + aspect: '3 / 1' }, defaultRules: [ { directives: { background: '--text', opacity: 0.5, - blur: '9px', - }, + blur: '9px' + } }, { parent: { - component: 'Alert', + component: 'Alert' }, component: 'Border', directives: { - textColor: '--parent', - }, + textColor: '--parent' + } }, { variant: 'error', directives: { - background: '--cRed', - }, + background: '--cRed' + } }, { variant: 'warning', directives: { - background: '--cOrange', - }, + background: '--cOrange' + } }, { variant: 'success', directives: { - background: '--cGreen', - }, - }, - ], + background: '--cGreen' + } + } + ] } diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js index 13d55c159..d1b8257d8 100644 --- a/src/components/announcement/announcement.js +++ b/src/components/announcement/announcement.js @@ -1,130 +1,109 @@ import { mapState } from 'vuex' - -import localeService from '../../services/locale/locale.service.js' import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' import RichContent from '../rich_content/rich_content.jsx' - -import { useAnnouncementsStore } from 'src/stores/announcements.js' +import localeService from '../../services/locale/locale.service.js' +import { useAnnouncementsStore } from 'src/stores/announcements' const Announcement = { components: { AnnouncementEditor, - RichContent, + RichContent }, - data() { + data () { return { editing: false, editedAnnouncement: { content: '', startsAt: undefined, endsAt: undefined, - allDay: undefined, + allDay: undefined }, - editError: '', + editError: '' } }, props: { - announcement: Object, + announcement: Object }, computed: { ...mapState({ - currentUser: (state) => state.users.currentUser, + currentUser: state => state.users.currentUser }), - canEditAnnouncement() { - return ( - this.currentUser && - this.currentUser.privileges.includes( - 'announcements_manage_announcements', - ) - ) + canEditAnnouncement () { + return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements') }, - content() { + content () { return this.announcement.content }, - isRead() { + isRead () { return this.announcement.read }, - publishedAt() { + publishedAt () { const time = this.announcement.published_at if (!time) { return } - return this.formatTimeOrDate( - time, - localeService.internalToBrowserLocale(this.$i18n.locale), - ) + return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) }, - startsAt() { + startsAt () { const time = this.announcement.starts_at if (!time) { return } - return this.formatTimeOrDate( - time, - localeService.internalToBrowserLocale(this.$i18n.locale), - ) + return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) }, - endsAt() { + endsAt () { const time = this.announcement.ends_at if (!time) { return } - return this.formatTimeOrDate( - time, - localeService.internalToBrowserLocale(this.$i18n.locale), - ) + return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale)) }, - inactive() { + inactive () { return this.announcement.inactive - }, + } }, methods: { - markAsRead() { + markAsRead () { if (!this.isRead) { - return useAnnouncementsStore().markAnnouncementAsRead( - this.announcement.id, - ) + return useAnnouncementsStore().markAnnouncementAsRead(this.announcement.id) } }, - deleteAnnouncement() { + deleteAnnouncement () { return useAnnouncementsStore().deleteAnnouncement(this.announcement.id) }, - formatTimeOrDate(time, locale) { + formatTimeOrDate (time, locale) { const d = new Date(time) - return this.announcement.all_day - ? d.toLocaleDateString(locale) - : d.toLocaleString(locale) + return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale) }, - enterEditMode() { + enterEditMode () { this.editedAnnouncement.content = this.announcement.pleroma.raw_content this.editedAnnouncement.startsAt = this.announcement.starts_at this.editedAnnouncement.endsAt = this.announcement.ends_at this.editedAnnouncement.allDay = this.announcement.all_day this.editing = true }, - submitEdit() { - useAnnouncementsStore() - .editAnnouncement({ - id: this.announcement.id, - ...this.editedAnnouncement, - }) + submitEdit () { + useAnnouncementsStore().editAnnouncement({ + id: this.announcement.id, + ...this.editedAnnouncement + }) .then(() => { this.editing = false }) - .catch((error) => { + .catch(error => { this.editError = error.error }) }, - cancelEdit() { + cancelEdit () { this.editing = false }, - clearError() { + clearError () { this.editError = undefined - }, - }, + } + } } export default Announcement diff --git a/src/components/announcement_editor/announcement_editor.js b/src/components/announcement_editor/announcement_editor.js index 6d22ac1fd..79a03afe1 100644 --- a/src/components/announcement_editor/announcement_editor.js +++ b/src/components/announcement_editor/announcement_editor.js @@ -2,12 +2,12 @@ import Checkbox from '../checkbox/checkbox.vue' const AnnouncementEditor = { components: { - Checkbox, + Checkbox }, props: { announcement: Object, - disabled: Boolean, - }, + disabled: Boolean + } } export default AnnouncementEditor diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js index b8b1f000a..9ce0b45f5 100644 --- a/src/components/announcements_page/announcements_page.js +++ b/src/components/announcements_page/announcements_page.js @@ -1,67 +1,59 @@ import { mapState } from 'vuex' - import Announcement from '../announcement/announcement.vue' import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' - -import { useAnnouncementsStore } from 'src/stores/announcements.js' +import { useAnnouncementsStore } from 'src/stores/announcements' const AnnouncementsPage = { components: { Announcement, - AnnouncementEditor, + AnnouncementEditor }, - data() { + data () { return { newAnnouncement: { content: '', startsAt: undefined, endsAt: undefined, - allDay: false, + allDay: false }, posting: false, - error: undefined, + error: undefined } }, - mounted() { + mounted () { useAnnouncementsStore().fetchAnnouncements() }, computed: { ...mapState({ - currentUser: (state) => state.users.currentUser, + currentUser: state => state.users.currentUser }), - announcements() { + announcements () { return useAnnouncementsStore().announcements }, - canPostAnnouncement() { - return ( - this.currentUser && - this.currentUser.privileges.includes( - 'announcements_manage_announcements', - ) - ) - }, + canPostAnnouncement () { + return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements') + } }, methods: { - postAnnouncement() { + postAnnouncement () { this.posting = true - useAnnouncementsStore() - .postAnnouncement(this.newAnnouncement) + useAnnouncementsStore().postAnnouncement(this.newAnnouncement) .then(() => { this.newAnnouncement.content = '' this.startsAt = undefined this.endsAt = undefined }) - .catch((error) => { + .catch(error => { this.error = error.error }) .finally(() => { this.posting = false }) }, - clearError() { + clearError () { this.error = undefined - }, - }, + } + } } export default AnnouncementsPage diff --git a/src/components/async_component_error/async_component_error.vue b/src/components/async_component_error/async_component_error.vue index baf430950..2ff8974c1 100644 --- a/src/components/async_component_error/async_component_error.vue +++ b/src/components/async_component_error/async_component_error.vue @@ -21,10 +21,10 @@ export default { emits: ['resetAsyncComponent'], methods: { - retry() { + retry () { this.$emit('resetAsyncComponent') - }, - }, + } + } } diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index fbe77a687..21d793930 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -1,29 +1,24 @@ -import { mapState } from 'pinia' - -import nsfwImage from '../../assets/nsfw.png' -import Flash from '../flash/flash.vue' import StillImage from '../still-image/still-image.vue' +import Flash from '../flash/flash.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' - -import { useInstanceStore } from 'src/stores/instance.js' -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' -import { useMediaViewerStore } from 'src/stores/media_viewer' -import { useMergedConfigStore } from 'src/stores/merged_config.js' - +import nsfwImage from '../../assets/nsfw.png' +import fileTypeService from '../../services/file_type/file_type.service.js' +import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { - faAlignRight, faFile, - faImage, faMusic, - faPencilAlt, - faPlayCircle, - faSearchPlus, - faStop, - faTimes, - faTrashAlt, + faImage, faVideo, + faPlayCircle, + faTimes, + faStop, + faSearchPlus, + faTrashAlt, + faPencilAlt, + faAlignRight } from '@fortawesome/free-solid-svg-icons' +import { useMediaViewerStore } from 'src/stores/media_viewer' library.add( faFile, @@ -36,7 +31,7 @@ library.add( faSearchPlus, faTrashAlt, faPencilAlt, - faAlignRight, + faAlignRight ) const Attachment = { @@ -51,72 +46,72 @@ const Attachment = { 'remove', 'shiftUp', 'shiftDn', - 'edit', + 'edit' ], - data() { + data () { return { localDescription: this.description || this.attachment.description, - nsfwImage: - useInstanceStore().instanceIdentity.nsfwCensorImage || nsfwImage, - hideNsfwLocal: useMergedConfigStore().mergedConfig.hideNsfw, - preloadImage: useMergedConfigStore().mergedConfig.preloadImage, + nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, + hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, + preloadImage: this.$store.getters.mergedConfig.preloadImage, loading: false, - img: this.attachment.type === 'image' && document.createElement('img'), + img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), modalOpen: false, showHidden: false, flashLoaded: false, - showDescription: false, + showDescription: false } }, components: { Flash, StillImage, - VideoAttachment, + VideoAttachment }, computed: { - classNames() { + classNames () { return [ { '-loading': this.loading, '-nsfw-placeholder': this.hidden, '-editable': this.edit !== undefined, - '-compact': this.compact, + '-compact': this.compact }, - '-type-' + this.attachment.type, + '-type-' + this.type, this.size && '-size-' + this.size, - `-${this.useContainFit ? 'contain' : 'cover'}-fit`, + `-${this.useContainFit ? 'contain' : 'cover'}-fit` ] }, - usePlaceholder() { + usePlaceholder () { return this.size === 'hide' }, - useContainFit() { - return this.mergedConfig.useContainFit + useContainFit () { + return this.$store.getters.mergedConfig.useContainFit }, - placeholderName() { + placeholderName () { if (this.attachment.description === '' || !this.attachment.description) { - return this.attachment.type.toUpperCase() + return this.type.toUpperCase() } return this.attachment.description }, - placeholderIconClass() { - if (this.attachment.type === 'image') return 'image' - if (this.attachment.type === 'video') return 'video' - if (this.attachment.type === 'audio') return 'music' + placeholderIconClass () { + if (this.type === 'image') return 'image' + if (this.type === 'video') return 'video' + if (this.type === 'audio') return 'music' return 'file' }, - referrerpolicy() { - return useInstanceCapabilitiesStore().mediaProxyAvailable - ? '' - : 'no-referrer' + referrerpolicy () { + return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' }, - hidden() { + type () { + return fileTypeService.fileType(this.attachment.mimetype) + }, + hidden () { return this.nsfw && this.hideNsfwLocal && !this.showHidden }, - isEmpty() { - return this.attachment.type === 'html' && !this.attachment.oembed + isEmpty () { + return (this.type === 'html' && !this.attachment.oembed) }, - useModal() { + useModal () { let modalTypes = [] switch (this.size) { case 'hide': @@ -129,66 +124,64 @@ const Attachment = { : ['image'] break } - return modalTypes.includes(this.attachment.type) + return modalTypes.includes(this.type) }, - videoTag() { + videoTag () { return this.useModal ? 'button' : 'span' }, - ...mapState(useMergedConfigStore, ['mergedConfig']), + ...mapGetters(['mergedConfig']) }, watch: { - 'attachment.description'(newVal) { + 'attachment.description' (newVal) { this.localDescription = newVal }, - localDescription(newVal) { + localDescription (newVal) { this.onEdit(newVal) - }, + } }, methods: { - linkClicked({ target }) { + linkClicked ({ target }) { if (target.tagName === 'A') { window.open(target.href, '_blank') } }, - openModal() { + openModal () { if (this.useModal) { this.$emit('setMedia') useMediaViewerStore().setCurrentMedia(this.attachment) - } else if (this.attachment.type === 'unknown') { + } else if (this.type === 'unknown') { window.open(this.attachment.url) } }, - openModalForce() { + openModalForce () { this.$emit('setMedia') useMediaViewerStore().setCurrentMedia(this.attachment) }, - onEdit(event) { + onEdit (event) { this.edit && this.edit(this.attachment, event) }, - onRemove() { + onRemove () { this.remove && this.remove(this.attachment) }, - onShiftUp() { + onShiftUp () { this.shiftUp && this.shiftUp(this.attachment) }, - onShiftDn() { + onShiftDn () { this.shiftDn && this.shiftDn(this.attachment) }, - stopFlash() { + stopFlash () { this.$refs.flash.closePlayer() }, - setFlashLoaded(event) { + setFlashLoaded (event) { this.flashLoaded = event }, - toggleDescription() { + toggleDescription () { this.showDescription = !this.showDescription }, - toggleHidden(event) { + toggleHidden (event) { if ( - this.mergedConfig.useOneClickNsfw && - !this.showHidden && - (this.attachment.type !== 'video' || - this.mergedConfig.playVideosInModal) + (this.mergedConfig.useOneClickNsfw && !this.showHidden) && + (this.type !== 'video' || this.mergedConfig.playVideosInModal) ) { this.openModal(event) return @@ -208,12 +201,12 @@ const Attachment = { this.showHidden = !this.showHidden } }, - onImageLoad(image) { + onImageLoad (image) { const width = image.naturalWidth const height = image.naturalHeight this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height }) - }, - }, + } + } } export default Attachment diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss index 97515eb32..16346c97c 100644 --- a/src/components/attachment/attachment.scss +++ b/src/components/attachment/attachment.scss @@ -107,9 +107,9 @@ .play-icon { position: absolute; - font-size: 4.5em; - top: calc(50% - 2.25rem); - left: calc(50% - 2.25rem); + font-size: 64px; + top: calc(50% - 32px); + left: calc(50% - 32px); color: rgb(255 255 255 / 75%); text-shadow: 0 0 2px rgb(0 0 0 / 40%); diff --git a/src/components/attachment/attachment.style.js b/src/components/attachment/attachment.style.js new file mode 100644 index 000000000..a9455e367 --- /dev/null +++ b/src/components/attachment/attachment.style.js @@ -0,0 +1,27 @@ +export default { + name: 'Attachment', + selector: '.Attachment', + notEditable: true, + validInnerComponents: [ + 'Border', + 'Button', + 'Input' + ], + defaultRules: [ + { + directives: { + roundness: 3 + } + }, + { + component: 'Button', + parent: { + component: 'Attachment' + }, + directives: { + background: '#FFFFFF', + opacity: 0.5 + } + } + ] +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 0db86ff8a..0701a393e 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -6,7 +6,7 @@ @click="openModal" >
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index cbe3dd80f..c8bba4c44 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -1,7 +1,7 @@ diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue index 1f8b62809..93799e4c2 100644 --- a/src/components/contrast_ratio/contrast_ratio.vue +++ b/src/components/contrast_ratio/contrast_ratio.vue @@ -63,68 +63,54 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { faAdjust, faExclamationTriangle, - faThumbsUp, + faThumbsUp } from '@fortawesome/free-solid-svg-icons' -library.add(faAdjust, faExclamationTriangle, faThumbsUp) +library.add( + faAdjust, + faExclamationTriangle, + faThumbsUp +) export default { components: { - Tooltip, + Tooltip }, props: { large: { required: false, type: Boolean, - default: false, + default: false }, // TODO: Make theme switcher compute theme initially so that contrast // component won't be called without contrast data contrast: { required: false, type: Object, - default: () => ({ - /* no-op */ - }), + default: () => ({}) }, showRatio: { required: false, type: Boolean, - default: false, - }, + default: false + } }, computed: { - hint() { - const levelVal = this.contrast.aaa - ? 'aaa' - : this.contrast.aa - ? 'aa' - : 'bad' + hint () { + const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad') const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const context = this.$t('settings.style.common.contrast.context.text') const ratio = this.contrast.text - return this.$t('settings.style.common.contrast.hint', { - level, - context, - ratio, - }) + return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) }, - hint_18pt() { - const levelVal = this.contrast.laaa - ? 'aaa' - : this.contrast.laa - ? 'aa' - : 'bad' + hint_18pt () { + const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad') const level = this.$t(`settings.style.common.contrast.level.${levelVal}`) const context = this.$t('settings.style.common.contrast.context.18pt') const ratio = this.contrast.text - return this.$t('settings.style.common.contrast.hint', { - level, - context, - ratio, - }) - }, - }, + return this.$t('settings.style.common.contrast.hint', { level, context, ratio }) + } + } } diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js index d4705303e..8f996be12 100644 --- a/src/components/conversation-page/conversation-page.js +++ b/src/components/conversation-page/conversation-page.js @@ -2,13 +2,13 @@ import Conversation from '../conversation/conversation.vue' const conversationPage = { components: { - Conversation, + Conversation }, computed: { - statusId() { + statusId () { return this.$route.params.id - }, - }, + } + } } export default conversationPage diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index b0d3a304c..491a8543f 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,24 +1,25 @@ -import { clone, filter, findIndex, get, reduce } from 'lodash' -import { mapState as mapPiniaState } from 'pinia' -import { mapState } from 'vuex' - -import { WSConnectionStatus } from '../../services/api/api.service.js' -import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' -import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' +import { reduce, filter, findIndex, clone, get } from 'lodash' import Status from '../status/status.vue' import ThreadTree from '../thread_tree/thread_tree.vue' - +import { WSConnectionStatus } from '../../services/api/api.service.js' +import { mapGetters, mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' +import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' +import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' import { useInterfaceStore } from 'src/stores/interface' -import { useMergedConfigStore } from 'src/stores/merged_config.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faAngleDoubleDown, faAngleDoubleLeft, - faChevronLeft, + faChevronLeft } from '@fortawesome/free-solid-svg-icons' -library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft) +library.add( + faAngleDoubleDown, + faAngleDoubleLeft, + faChevronLeft +) const sortById = (a, b) => { const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id @@ -42,25 +43,23 @@ const sortAndFilterConversation = (conversation, statusoid) => { if (statusoid.type === 'retweet') { conversation = filter( conversation, - (status) => - status.type === 'retweet' || - status.id !== statusoid.retweeted_status.id, + (status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id) ) } else { conversation = filter(conversation, (status) => status.type !== 'retweet') } - return conversation.filter((_) => _).sort(sortById) + return conversation.filter(_ => _).sort(sortById) } const conversation = { - data() { + data () { return { highlight: null, expanded: false, threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' statusContentPropertiesObject: {}, inlineDivePosition: null, - loadStatusError: null, + loadStatusError: null } }, props: [ @@ -70,80 +69,76 @@ const conversation = { 'pinnedStatusIdsObject', 'inProfile', 'profileUserId', - 'virtualHidden', + 'virtualHidden' ], - created() { + created () { if (this.isPage) { this.fetchConversation() } }, computed: { - maxDepthToShowByDefault() { + maxDepthToShowByDefault () { // maxDepthInThread = max number of depths that is *visible* // since our depth starts with 0 and "showing" means "showing children" // there is a -2 here - const maxDepth = this.mergedConfig.maxDepthInThread - 2 + const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 return maxDepth >= 1 ? maxDepth : 1 }, - streamingEnabled() { - return ( - this.mergedConfig.useStreamingApi && - this.mastoUserSocketStatus === WSConnectionStatus.JOINED - ) + streamingEnabled () { + return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED }, - displayStyle() { - return this.mergedConfig.conversationDisplay + displayStyle () { + return this.$store.getters.mergedConfig.conversationDisplay }, - isTreeView() { + isTreeView () { return !this.isLinearView }, - treeViewIsSimple() { - return !this.mergedConfig.conversationTreeAdvanced + treeViewIsSimple () { + return !this.$store.getters.mergedConfig.conversationTreeAdvanced }, - isLinearView() { + isLinearView () { return this.displayStyle === 'linear' }, - shouldFadeAncestors() { - return this.mergedConfig.conversationTreeFadeAncestors + shouldFadeAncestors () { + return this.$store.getters.mergedConfig.conversationTreeFadeAncestors }, - otherRepliesButtonPosition() { - return this.mergedConfig.conversationOtherRepliesButton + otherRepliesButtonPosition () { + return this.$store.getters.mergedConfig.conversationOtherRepliesButton }, - showOtherRepliesButtonBelowStatus() { + showOtherRepliesButtonBelowStatus () { return this.otherRepliesButtonPosition === 'below' }, - showOtherRepliesButtonInsideStatus() { + showOtherRepliesButtonInsideStatus () { return this.otherRepliesButtonPosition === 'inside' }, - suspendable() { + suspendable () { if (this.isTreeView) { - return Object.entries(this.statusContentProperties).every( - ([, prop]) => !prop.replying && prop.mediaPlaying.length === 0, - ) + return Object.entries(this.statusContentProperties) + .every(([, prop]) => !prop.replying && prop.mediaPlaying.length === 0) } if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { - return this.$refs.statusComponent.every((s) => s.suspendable) + return this.$refs.statusComponent.every(s => s.suspendable) } else { return true } }, - hideStatus() { + hideStatus () { return this.virtualHidden && this.suspendable }, - status() { + status () { return this.$store.state.statuses.allStatusesObject[this.statusId] }, - originalStatusId() { + originalStatusId () { if (this.status.retweeted_status) { return this.status.retweeted_status.id } else { return this.statusId } }, - conversationId() { + conversationId () { return this.getConversationId(this.statusId) }, - conversation() { + conversation () { if (!this.status) { return [] } @@ -152,9 +147,7 @@ const conversation = { return [this.status] } - const conversation = clone( - this.$store.state.statuses.conversationsObject[this.conversationId], - ) + const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId]) const statusIndex = findIndex(conversation, { id: this.originalStatusId }) if (statusIndex !== -1) { conversation[statusIndex] = this.status @@ -162,188 +155,144 @@ const conversation = { return sortAndFilterConversation(conversation, this.status) }, - statusMap() { + statusMap () { return this.conversation.reduce((res, s) => { res[s.id] = s return res }, {}) }, - threadTree() { - const reverseLookupTable = this.conversation.reduce( - (table, status, index) => { - table[status.id] = index - return table - }, - {}, - ) + threadTree () { + const reverseLookupTable = this.conversation.reduce((table, status, index) => { + table[status.id] = index + return table + }, {}) - const threads = this.conversation.reduce( - (a, cur) => { - const id = cur.id - a.forest[id] = this.getReplies(id).map((s) => s.id) + const threads = this.conversation.reduce((a, cur) => { + const id = cur.id + a.forest[id] = this.getReplies(id) + .map(s => s.id) - return a - }, - { - forest: {}, - }, - ) + return a + }, { + forest: {} + }) - const walk = (forest, topLevel, depth = 0, processed = {}) => - topLevel - .map((id) => { - if (processed[id]) { - return [] - } + const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => { + if (processed[id]) { + return [] + } - processed[id] = true - return [ - { - status: this.conversation[reverseLookupTable[id]], - id, - depth, - }, - walk(forest, forest[id], depth + 1, processed), - ].reduce((a, b) => a.concat(b), []) - }) - .reduce((a, b) => a.concat(b), []) + processed[id] = true + return [{ + status: this.conversation[reverseLookupTable[id]], + id, + depth + }, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), []) + }).reduce((a, b) => a.concat(b), []) - const linearized = walk( - threads.forest, - this.topLevel.map((k) => k.id), - ) + const linearized = walk(threads.forest, this.topLevel.map(k => k.id)) return linearized }, - replyIds() { - return this.conversation - .map((k) => k.id) + replyIds () { + return this.conversation.map(k => k.id) .reduce((res, id) => { - res[id] = (this.replies[id] || []).map((k) => k.id) + res[id] = (this.replies[id] || []).map(k => k.id) return res }, {}) }, - totalReplyCount() { + totalReplyCount () { const sizes = {} const subTreeSizeFor = (id) => { if (sizes[id]) { return sizes[id] } - sizes[id] = - 1 + - this.replyIds[id] - .map((cid) => subTreeSizeFor(cid)) - .reduce((a, b) => a + b, 0) + sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0) return sizes[id] } - this.conversation.map((k) => k.id).map(subTreeSizeFor) + this.conversation.map(k => k.id).map(subTreeSizeFor) return Object.keys(sizes).reduce((res, id) => { res[id] = sizes[id] - 1 // exclude itself return res }, {}) }, - totalReplyDepth() { + totalReplyDepth () { const depths = {} const subTreeDepthFor = (id) => { if (depths[id]) { return depths[id] } - depths[id] = - 1 + - this.replyIds[id] - .map((cid) => subTreeDepthFor(cid)) - .reduce((a, b) => (a > b ? a : b), 0) + depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0) return depths[id] } - this.conversation.map((k) => k.id).map(subTreeDepthFor) + this.conversation.map(k => k.id).map(subTreeDepthFor) return Object.keys(depths).reduce((res, id) => { res[id] = depths[id] - 1 // exclude itself return res }, {}) }, - depths() { + depths () { return this.threadTree.reduce((a, k) => { a[k.id] = k.depth return a }, {}) }, - topLevel() { - const topLevel = this.conversation.reduce( - (tl, cur) => - tl.filter( - (k) => - this.getReplies(cur.id) - .map((v) => v.id) - .indexOf(k.id) === -1, - ), - this.conversation, - ) + topLevel () { + const topLevel = this.conversation.reduce((tl, cur) => + tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation) return topLevel }, - otherTopLevelCount() { + otherTopLevelCount () { return this.topLevel.length - 1 }, - showingTopLevel() { + showingTopLevel () { if (this.canDive && this.diveRoot) { return [this.statusMap[this.diveRoot]] } return this.topLevel }, - diveRoot() { + diveRoot () { const statusId = this.inlineDivePosition || this.statusId const isTopLevel = !this.parentOf(statusId) return isTopLevel ? null : statusId }, - diveDepth() { + diveDepth () { return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0 }, - diveMode() { + diveMode () { return this.canDive && !!this.diveRoot }, - shouldShowAllConversationButton() { + shouldShowAllConversationButton () { // The "show all conversation" button tells the user that there exist // other toplevel statuses, so do not show it if there is only a single root - return ( - this.isTreeView && - this.isExpanded && - this.diveMode && - this.topLevel.length > 1 - ) + return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1 }, - shouldShowAncestors() { - return ( - this.isTreeView && - this.isExpanded && - this.ancestorsOf(this.diveRoot).length - ) + shouldShowAncestors () { + return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length }, - replies() { + replies () { let i = 1 - return reduce( - this.conversation, - (result, { id, in_reply_to_status_id: irid }) => { - if (irid) { - result[irid] = result[irid] || [] - result[irid].push({ - name: `#${i}`, - id, - }) - } - i++ - return result - }, - {}, - ) + return reduce(this.conversation, (result, { id, in_reply_to_status_id: irid }) => { + if (irid) { + result[irid] = result[irid] || [] + result[irid].push({ + name: `#${i}`, + id + }) + } + i++ + return result + }, {}) }, - isExpanded() { + isExpanded () { return !!(this.expanded || this.isPage) }, - hiddenStyle() { + hiddenStyle () { const height = (this.status && this.status.virtualHeight) || '120px' return this.virtualHidden ? { height } : {} }, - threadDisplayStatus() { + threadDisplayStatus () { return this.conversation.reduce((a, k) => { const id = k.id const depth = this.depths[id] @@ -351,7 +300,7 @@ const conversation = { if (this.threadDisplayStatusObject[id]) { return this.threadDisplayStatusObject[id] } - if (depth - this.diveDepth <= this.maxDepthToShowByDefault) { + if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) { return 'showing' } else { return 'hidden' @@ -362,7 +311,7 @@ const conversation = { return a }, {}) }, - statusContentProperties() { + statusContentProperties () { return this.conversation.reduce((a, k) => { const id = k.id const props = (() => { @@ -371,13 +320,13 @@ const conversation = { expandingSubject: false, showingLongSubject: false, isReplying: false, - mediaPlaying: [], + mediaPlaying: [] } if (this.statusContentPropertiesObject[id]) { return { ...def, - ...this.statusContentPropertiesObject[id], + ...this.statusContentPropertiesObject[id] } } return def @@ -387,59 +336,54 @@ const conversation = { return a }, {}) }, - canDive() { + canDive () { return this.isTreeView && this.isExpanded }, - maybeHighlight() { + maybeHighlight () { return this.isExpanded ? this.highlight : null }, - ...mapPiniaState(useMergedConfigStore, ['mergedConfig']), + ...mapGetters(['mergedConfig']), ...mapState({ - mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus, + mastoUserSocketStatus: state => state.api.mastoUserSocketStatus }), ...mapPiniaState(useInterfaceStore, { - mobileLayout: (store) => store.layoutType === 'mobile', - }), + mobileLayout: store => store.layoutType === 'mobile' + }) }, components: { Status, ThreadTree, QuickFilterSettings, - QuickViewSettings, + QuickViewSettings }, watch: { - statusId(newVal, oldVal) { + statusId (newVal, oldVal) { const newConversationId = this.getConversationId(newVal) const oldConversationId = this.getConversationId(oldVal) - if ( - newConversationId && - oldConversationId && - newConversationId === oldConversationId - ) { + if (newConversationId && oldConversationId && newConversationId === oldConversationId) { this.setHighlight(this.originalStatusId) } else { this.fetchConversation() } }, - expanded(value) { + expanded (value) { if (value) { this.fetchConversation() } else { this.resetDisplayState() } }, - virtualHidden() { - this.$store.dispatch('setVirtualHeight', { - statusId: this.statusId, - height: `${this.$el.clientHeight}px`, - }) - }, + virtualHidden () { + this.$store.dispatch( + 'setVirtualHeight', + { statusId: this.statusId, height: `${this.$el.clientHeight}px` } + ) + } }, methods: { - fetchConversation() { + fetchConversation () { if (this.status) { - this.$store.state.api.backendInteractor - .fetchConversation({ id: this.statusId }) + this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId }) .then(({ ancestors, descendants }) => { this.$store.dispatch('addNewStatuses', { statuses: ancestors }) this.$store.dispatch('addNewStatuses', { statuses: descendants }) @@ -447,8 +391,7 @@ const conversation = { }) } else { this.loadStatusError = null - this.$store.state.api.backendInteractor - .fetchStatus({ id: this.statusId }) + this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) .then((status) => { this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.fetchConversation() @@ -458,16 +401,16 @@ const conversation = { }) } }, - isFocused(id) { - return this.isExpanded && id === this.highlight + isFocused (id) { + return (this.isExpanded) && id === this.highlight }, - getReplies(id) { + getReplies (id) { return this.replies[id] || [] }, - getHighlight() { + getHighlight () { return this.isExpanded ? this.highlight : null }, - setHighlight(id) { + setHighlight (id) { if (!id) return this.highlight = id @@ -478,54 +421,44 @@ const conversation = { this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchEmojiReactionsBy', id) }, - toggleExpanded() { + toggleExpanded () { this.expanded = !this.expanded }, - getConversationId(statusId) { + getConversationId (statusId) { const status = this.$store.state.statuses.allStatusesObject[statusId] - return get( - status, - 'retweeted_status.statusnet_conversation_id', - get(status, 'statusnet_conversation_id'), - ) + return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) }, - setThreadDisplay(id, nextStatus) { + setThreadDisplay (id, nextStatus) { this.threadDisplayStatusObject = { ...this.threadDisplayStatusObject, - [id]: nextStatus, + [id]: nextStatus } }, - toggleThreadDisplay(id) { + toggleThreadDisplay (id) { const curStatus = this.threadDisplayStatus[id] const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing' this.setThreadDisplay(id, nextStatus) }, - setThreadDisplayRecursively(id, nextStatus) { + setThreadDisplayRecursively (id, nextStatus) { this.setThreadDisplay(id, nextStatus) - this.getReplies(id) - .map((k) => k.id) - .map((id) => this.setThreadDisplayRecursively(id, nextStatus)) + this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus)) }, - showThreadRecursively(id) { + showThreadRecursively (id) { this.setThreadDisplayRecursively(id, 'showing') }, - setStatusContentProperty(id, name, value) { + setStatusContentProperty (id, name, value) { this.statusContentPropertiesObject = { ...this.statusContentPropertiesObject, [id]: { ...this.statusContentPropertiesObject[id], - [name]: value, - }, + [name]: value + } } }, - toggleStatusContentProperty(id, name) { - this.setStatusContentProperty( - id, - name, - !this.statusContentProperties[id][name], - ) + toggleStatusContentProperty (id, name) { + this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name]) }, - leastVisibleAncestor(id) { + leastVisibleAncestor (id) { let cur = id let parent = this.parentOf(cur) while (cur) { @@ -539,20 +472,18 @@ const conversation = { // nothing found, fall back to toplevel return this.topLevel[0] ? this.topLevel[0].id : undefined }, - diveIntoStatus(id) { + diveIntoStatus (id) { this.tryScrollTo(id) }, - diveToTopLevel() { - this.tryScrollTo( - this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id, - ) + diveToTopLevel () { + this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id) }, // only used when we are not on a page - undive() { + undive () { this.inlineDivePosition = null this.setHighlight(this.statusId) }, - tryScrollTo(id) { + tryScrollTo (id) { if (!id) { return } @@ -581,13 +512,13 @@ const conversation = { this.setHighlight(id) }) }, - goToCurrent() { + goToCurrent () { this.tryScrollTo(this.diveRoot || this.topLevel[0].id) }, - statusById(id) { + statusById (id) { return this.statusMap[id] }, - parentOf(id) { + parentOf (id) { const status = this.statusById(id) if (!status) { return undefined @@ -598,11 +529,11 @@ const conversation = { } return parentId }, - parentOrSelf(id) { + parentOrSelf (id) { return this.parentOf(id) || id }, // Ancestors of some status, from top to bottom - ancestorsOf(id) { + ancestorsOf (id) { const ancestors = [] let cur = this.parentOf(id) while (cur) { @@ -611,7 +542,7 @@ const conversation = { } return ancestors }, - topLevelAncestorOrSelfId(id) { + topLevelAncestorOrSelfId (id) { let cur = id let parent = this.parentOf(id) while (parent) { @@ -620,11 +551,11 @@ const conversation = { } return cur }, - resetDisplayState() { + resetDisplayState () { this.undive() this.threadDisplayStatusObject = {} - }, - }, + } + } } export default conversation diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 8d4734083..2f3de3a86 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -88,34 +88,38 @@ class="thread-ancestor" :class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}" > - ({ searchBarHidden: true, - supportsMask: - window.CSS && - window.CSS.supports && - (window.CSS.supports('mask-size', 'contain') || + supportsMask: window.CSS && window.CSS.supports && ( + window.CSS.supports('mask-size', 'contain') || window.CSS.supports('-webkit-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') || - window.CSS.supports('-o-mask-size', 'contain')), - showingConfirmLogout: false, + window.CSS.supports('-o-mask-size', 'contain') + ), + showingConfirmLogout: false }), computed: { - enableMask() { - return this.supportsMask && this.logoMask - }, - logoStyle() { + enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, + logoStyle () { return { - visibility: this.enableMask ? 'hidden' : 'visible', + visibility: this.enableMask ? 'hidden' : 'visible' } }, - logoMaskStyle() { + logoMaskStyle () { return this.enableMask ? { - 'mask-image': `url(${this.logo})`, + 'mask-image': `url(${this.$store.state.instance.logo})` } : { - 'background-color': this.enableMask ? '' : 'transparent', + 'background-color': this.enableMask ? '' : 'transparent' } }, - logoBgStyle() { - return Object.assign( - { - margin: `${this.logoMargin} 0`, - opacity: this.searchBarHidden ? 1 : 0, - }, - this.enableMask - ? {} - : { - 'background-color': this.enableMask ? '' : 'transparent', - }, - ) - }, - ...mapState(useInstanceStore, ['privateMode']), - ...mapState(useInstanceStore, { - logoMask: (store) => store.instanceIdentity.logoMask, - logo: (store) => store.instanceIdentity.logo, - logoLeft: (store) => store.instanceIdentity.logoLeft, - logoMargin: (store) => store.instanceIdentity.logoMargin, - sitename: (store) => store.instanceIdentity.name, - hideSitename: (store) => store.instanceIdentity.hideSitename, - }), - currentUser() { - return this.$store.state.users.currentUser - }, - shouldConfirmLogout() { - return useMergedConfigStore().mergedConfig.modalOnLogout + logoBgStyle () { + return Object.assign({ + margin: `${this.$store.state.instance.logoMargin} 0`, + opacity: this.searchBarHidden ? 1 : 0 + }, this.enableMask + ? {} + : { + 'background-color': this.enableMask ? '' : 'transparent' + }) }, + logo () { return this.$store.state.instance.logo }, + sitename () { return this.$store.state.instance.name }, + hideSitename () { return this.$store.state.instance.hideSitename }, + logoLeft () { return this.$store.state.instance.logoLeft }, + currentUser () { return this.$store.state.users.currentUser }, + privateMode () { return this.$store.state.instance.private }, + shouldConfirmLogout () { + return this.$store.getters.mergedConfig.modalOnLogout + } }, methods: { - scrollToTop() { + scrollToTop () { window.scrollTo(0, 0) }, - showConfirmLogout() { + showConfirmLogout () { this.showingConfirmLogout = true }, - hideConfirmLogout() { + hideConfirmLogout () { this.showingConfirmLogout = false }, - logout() { + logout () { if (!this.shouldConfirmLogout) { this.doLogout() } else { this.showConfirmLogout() } }, - doLogout() { + doLogout () { this.$router.replace('/main/public') this.$store.dispatch('logout') this.hideConfirmLogout() }, - onSearchBarToggled(hidden) { + onSearchBarToggled (hidden) { this.searchBarHidden = hidden }, - ...mapActions(useInterfaceStore, ['openSettingsModal']), - }, + openSettingsModal () { + useInterfaceStore().openSettingsModal('user') + }, + openAdminModal () { + useInterfaceStore().openSettingsModal('admin') + } + } } diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index da427f2a1..49382f8ee 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -40,7 +40,7 @@