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/.eslintignore b/.eslintignore new file mode 100644 index 000000000..34af3774f --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +build/*.js +config/*.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..361cff5f2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,27 @@ +module.exports = { + root: true, + parserOptions: { + parser: '@babel/eslint-parser', + sourceType: 'module' + }, + // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style + extends: [ + 'standard', + 'plugin:vue/recommended' + ], + // required to lint *.vue files + plugins: [ + 'vue' + ], + // add your custom rules here + rules: { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + 'vue/require-prop-types': 0, + 'vue/multi-word-component-names': 0 + } +} 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/.gitattributes b/.gitattributes index 1bea4dc8f..c5b9ea10e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -/build/commit_hash.js export-subst +/build/webpack.prod.conf.js export-subst diff --git a/.gitignore b/.gitignore index c4a96ee1e..0d5befd28 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +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 +static/emoji.json logs/ -__screenshots__/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e2946b162..f4c5cf43a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ # This file is a template, and might need editing before it works on your project. # Official framework image. Look for the different tagged releases at: # https://hub.docker.com/r/library/node/tags/ -image: node:20 +image: node:16 stages: - check-changelog @@ -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 + - npm run lint + - npm run stylelint test: stage: test @@ -61,144 +50,10 @@ test: APT_CACHE_DIR: apt-cache script: - mkdir -pv $APT_CACHE_DIR && apt-get -qq update + - apt install firefox-esr -y --no-install-recommends + - firefox --version - yarn - - yarn playwright install firefox - - yarn playwright install-deps - - yarn unit-ci - artifacts: - # When the test fails, upload screenshots for better context on why it fails - paths: - - test/**/__screenshots__ - when: on_failure - -e2e-pleroma: - stage: test - image: mcr.microsoft.com/playwright:v1.61.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/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:8099 - 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 + - yarn unit build: stage: build @@ -207,7 +62,7 @@ build: - himem script: - yarn - - yarn build + - npm run build artifacts: paths: - dist/ 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/.node-version b/.node-version index 5bd681170..5397c87fa 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.19.0 +16.18.1 diff --git a/.stylelintrc.json b/.stylelintrc.json index afdfd5f5b..d6689cc01 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,5 +1,6 @@ { "extends": [ + "stylelint-rscss/config", "stylelint-config-standard", "stylelint-config-recommended-scss", "stylelint-config-html", @@ -7,13 +8,20 @@ ], "rules": { "declaration-no-important": true, + "rscss/no-descendant-combinator": false, + "rscss/class-format": [ + false, + { + "component": "pascal-case", + "variant": "^-[a-z]\\w+", + "element": "^[a-z]\\w+" + } + ], "selector-class-pattern": null, "import-notation": null, "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/.woodpecker/build.yaml b/.woodpecker/build.yaml deleted file mode 100644 index af0bb98e3..000000000 --- a/.woodpecker/build.yaml +++ /dev/null @@ -1,43 +0,0 @@ -when: - - event: pull_request - evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")' - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - - event: manual - branch: ${CI_REPO_DEFAULT_BRANCH} - -depends_on: - - test - - test-e2e - -labels: - platform: linux/amd64 - memory: 'high' - -steps: - build: - image: docker.io/node:20-alpine - commands: - - apk add --no-cache zip git - - yarn --frozen-lockfile - - yarn build - - if [ "${CI_PIPELINE_EVENT}" = "push" ] || [ "${CI_PIPELINE_EVENT}" = "manual" ]; then zip -9qr ${CI_REPO_DEFAULT_BRANCH}.zip dist/; fi - - upload-artifacts: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - - event: manual - branch: ${CI_REPO_DEFAULT_BRANCH} - settings: - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - update: true - owner: 'pleroma' - package_name: 'pleroma-fe-builds' - package_version: ${CI_REPO_DEFAULT_BRANCH} - file_source: ./${CI_REPO_DEFAULT_BRANCH}.zip - file_name: latest.zip diff --git a/.woodpecker/changelog.yaml b/.woodpecker/changelog.yaml deleted file mode 100644 index b6c35afbb..000000000 --- a/.woodpecker/changelog.yaml +++ /dev/null @@ -1,10 +0,0 @@ -when: - - event: pull_request - evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")' - -steps: - check-changelog: - image: docker.io/alpine:3.23 - commands: - - apk add --no-cache git - - sh ./tools/check-changelog diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml deleted file mode 100644 index 257887338..000000000 --- a/.woodpecker/lint.yaml +++ /dev/null @@ -1,32 +0,0 @@ -when: - - event: pull_request - evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")' - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - - event: manual - branch: ${CI_REPO_DEFAULT_BRANCH} - -steps: - install-depends: - image: &node-image - docker.io/node:20-alpine - commands: - - yarn --frozen-lockfile - - eslint: - image: *node-image - depends_on: install-depends - commands: - - yarn ci-eslint - - biome: - image: *node-image - depends_on: install-depends - commands: - - yarn ci-biome - - stylelint: - image: *node-image - depends_on: install-depends - commands: - - yarn ci-stylelint diff --git a/.woodpecker/test-e2e.yaml b/.woodpecker/test-e2e.yaml deleted file mode 100644 index 2a7d1511d..000000000 --- a/.woodpecker/test-e2e.yaml +++ /dev/null @@ -1,101 +0,0 @@ -when: - - event: pull_request - evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")' - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - - event: manual - branch: ${CI_REPO_DEFAULT_BRANCH} - -labels: - platform: linux/amd64 - memory: 'high' - -variables: - artifacts_uploader_settings: &artifacts_uploader_settings - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' - package_name: 'pleroma-fe-test-artifacts' - script_file_entrypoint: &script_file_entrypoint - - /bin/sh - - -c - - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' - -steps: - test: - image: mcr.microsoft.com/playwright:v1.61.0-jammy - entrypoint: *script_file_entrypoint - environment: - APT_CACHE_DIR: apt-cache - DEBIAN_FRONTEND: noninteractive - E2E_BASE_URL: http://localhost:8099 - FF_NETWORK_PER_BUILD: "true" - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" - VITE_PROXY_ORIGIN: "http://pleroma:4000" - VITE_PROXY_TARGET: "http://pleroma:4000" - commands: - - | - if [ "${CI_PIPELINE_EVENT}" != "pull_request" ]; then - mkdir -pv $APT_CACHE_DIR && apt-get -qq update - apt-get install -y zip - fi - - 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 - - | - if ! yarn e2e:pw; then - [ "${CI_PIPELINE_EVENT}" = "pull_request" ] || zip -9qr ${CI_COMMIT_SHA:0:8}-e2e.zip ./test/e2e-playwright/test-results ./test/e2e-playwright/playwright-report - exit 1 - fi - - upload-artifacts: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - status: [failure] - - event: manual - branch: ${CI_REPO_DEFAULT_BRANCH} - status: [failure] - settings: - <<: *artifacts_uploader_settings - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8} - file_source: ./${CI_COMMIT_SHA:0:8}-e2e.zip - file_name: ${CI_COMMIT_SHA:0:8}-e2e.zip - -services: - postgres: - image: docker.io/postgres:13-alpine - environment: - POSTGRES_DB: pleroma_test - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - - pleroma: - image: git.pleroma.social/pleroma/pleroma:stable-e2e - environment: - ADMIN_EMAIL: "admin@example.com" - NOTIFY_EMAIL: "admin@example.com" - DB_USER: postgres - DB_PASS: postgres - DB_NAME: pleroma_test - DB_HOST: postgres - INSTANCE_NAME: Pleroma E2E - E2E_ADMIN_USERNAME: admin - E2E_ADMIN_PASSWORD: adminadmin - E2E_ADMIN_EMAIL: "admin@example.com" diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml deleted file mode 100644 index 89f6f2f93..000000000 --- a/.woodpecker/test.yaml +++ /dev/null @@ -1,61 +0,0 @@ -when: - - event: pull_request - evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")' - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - - event: manual - branch: ${CI_REPO_DEFAULT_BRANCH} - -labels: - platform: linux/amd64 - memory: 'high' - -variables: - artifacts_uploader_settings: &artifacts_uploader_settings - user: - from_secret: pleroma-ci-user - password: - from_secret: pleroma-ci-password - owner: 'pleroma' - package_name: 'pleroma-fe-test-artifacts' - script_file_entrypoint: &script_file_entrypoint - - /bin/sh - - -c - - 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh' - -steps: - test: - image: mcr.microsoft.com/playwright:v1.61.0-jammy - environment: - APT_CACHE_DIR: apt-cache - DEBIAN_FRONTEND: noninteractive - FF_NETWORK_PER_BUILD: "true" - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" - entrypoint: *script_file_entrypoint - commands: - - | - if [ "${CI_PIPELINE_EVENT}" != "pull_request" ]; then - mkdir -pv $APT_CACHE_DIR && apt-get -qq update - apt-get -y install zip - fi - - yarn --frozen-lockfile - - | - if ! yarn unit-ci; then - [ "${CI_PIPELINE_EVENT}" = "pull_request" ] || zip -9qr ${CI_COMMIT_SHA:0:8}-screenshots.zip $(find . -type d -name __screenshots__) - exit 1 - fi - - upload-artifacts: - image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0 - when: - - event: push - branch: ${CI_REPO_DEFAULT_BRANCH} - status: [failure] - - event: manual - branch: ${CI_REPO_DEFAULT_BRANCH} - status: [failure] - settings: - <<: *artifacts_uploader_settings - package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8} - file_source: ./${CI_COMMIT_SHA:0:8}-screenshots.zip - file_name: ${CI_COMMIT_SHA:0:8}-screenshots.zip diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb5a9cb4..9844319e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,144 +3,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 -- BREAKING: static/emoji.json is replaced with a properly hashed path under static/js in the production build, meaning server admins cannot provide their own set of unicode emojis by overriding this file (custom (image-based) emojis not affected) -- Speed up initial boot. -- Updated our build system to support browsers: - Safari >= 15 - Firefox >= 115 - Android > 4 - no Opera Mini support - no IE support - no "dead" (unmaintained) browsers support - -This does not guarantee that browsers will or will not work. - -- Use /api/v1/accounts/:id/follow for account subscriptions instead of the deprecated routes -- Modal layout for mobile has new layout to make it easy to use -- Better display of mute reason on posts -- Simplify the OAuth client_name to 'PleromaFE' -- Partially migrated from vuex to pinia -- Authenticate and subscribe to streaming after connection -- Tabs now have indentation for better visibility of which tab is currently active -- Upgraded Vue to version 3.5 - -### Added -- Support bookmark folders -- Some new default color schemes -- Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree -- Post actions customization -- 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. -- 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 -- Make UserLink wrappable - -### Fixed -- Fixed occasional overflows in emoji picker and made header scrollable -- Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name. -- Checkbox vertical alignment -- Check for canvas extract permission when initializing favicon service -- Fix some of the color manipulation functions -- Fix draft saving when auto-save is off -- Switch from class hack to normalButton attribute for emoji count popover -- Fix emoji inconsistencies in notifications, -- Fix some emoji not scaling with interface -- Make sure hover style is also applied to :focus-visible -- Improved ToS and registration -- Fix small markup inconsistencies -- Fixed modals buttons overflow -- Fix whitespaces for multiple status mute reasons, display bot status reason -- Create an OAuth app only when needed -- Fix CSS compatibility issues in style_setter.js for older browsers like Palemoon -- Proper sticky header for conversations on user page -- Add text label for more actions button in post status form -- Reply-or-quote buttons now take less space -- Allow repeats of own posts with private scopes -- Bookmarks visible again on mobile -- Remove focusability on hidden popover in subject input -- Show only month and day instead of weird "day, hour" format. - -### Removed -- BREAKING: drop support for browsers that do not support ` +
diff --git a/package.json b/package.json index f594d7a99..61db87d20 100644 --- a/package.json +++ b/package.json @@ -1,130 +1,136 @@ { "name": "pleroma_fe", - "version": "2.10.1", + "version": "2.7.1", "description": "Pleroma frontend, the default frontend of Pleroma social network server", - "author": "Pleroma contributors ", + "author": "Pleroma contributors ", "private": false, "scripts": { - "dev": "node build/update-emoji.js && vite dev", - "build": "node build/update-emoji.js && vite build", - "unit": "node build/update-emoji.js && vitest --run", - "unit-ci": "node build/update-emoji.js && vitest --run --browser.headless", - "unit:watch": "node build/update-emoji.js && vitest", - "e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs", - "e2e": "sh ./tools/e2e/run.sh", - "test": "yarn run unit && yarn run e2e", - "ci-biome": "yarn exec biome check", - "ci-eslint": "yarn exec eslint", - "ci-stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'", - "lint": "yarn ci-biome; yarn ci-eslint; yarn ci-stylelint", - "lint-fix": "yarn exec eslint -- --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write" + "dev": "node build/dev-server.js", + "build": "node build/build.js", + "unit": "karma start test/unit/karma.conf.js --single-run", + "unit:watch": "karma start test/unit/karma.conf.js --single-run=false", + "e2e": "node test/e2e/runner.js", + "test": "npm run unit && npm run e2e", + "stylelint": "npx stylelint '**/*.scss' '**/*.vue'", + "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs", + "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { - "@babel/runtime": "7.28.4", + "@babel/runtime": "7.21.5", "@chenfengyuan/vue-qrcode": "2.0.0", - "@fortawesome/fontawesome-svg-core": "7.1.0", - "@fortawesome/free-regular-svg-icons": "7.1.0", - "@fortawesome/free-solid-svg-icons": "7.1.0", - "@fortawesome/vue-fontawesome": "3.1.2", - "@kazvmoe-infra/pinch-zoom-element": "1.3.0", + "@fortawesome/fontawesome-svg-core": "6.4.0", + "@fortawesome/free-regular-svg-icons": "6.4.0", + "@fortawesome/free-solid-svg-icons": "6.4.0", + "@fortawesome/vue-fontawesome": "3.0.3", + "@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@kazvmoe-infra/unicode-emoji-json": "0.4.0", - "@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22", + "@ruffle-rs/ruffle": "0.1.0-nightly.2024.8.21", "@vuelidate/core": "2.0.3", "@vuelidate/validators": "2.0.4", - "@web3-storage/parse-link-header": "^3.1.0", "body-scroll-lock": "3.1.5", "chromatism": "3.0.0", "click-outside-vue3": "4.0.1", - "cropperjs": "2.0.1", + "cropperjs": "1.5.13", "escape-html": "1.0.3", - "globals": "^16.0.0", "hash-sum": "^2.0.0", "js-cookie": "3.0.5", "localforage": "1.10.0", "parse-link-header": "2.0.0", - "phoenix": "1.8.1", - "pinia": "^3.0.4", - "punycode.js": "2.3.1", - "qrcode": "1.5.4", + "phoenix": "1.7.7", + "punycode.js": "2.3.0", + "qrcode": "1.5.3", "querystring-es3": "0.2.1", - "url": "0.11.4", + "url": "0.11.0", "utf8": "3.0.0", - "uuid": "11.1.0", - "vue": "3.5.22", - "vue-i18n": "11", - "vue-router": "4.6.4", + "vue": "3.2.45", + "vue-i18n": "9.2.2", + "vue-router": "4.1.6", + "vue-template-compiler": "2.7.14", "vue-virtual-scroller": "^2.0.0-beta.7", "vuex": "4.1.0" }, "devDependencies": { - "@babel/core": "7.28.5", - "@babel/eslint-parser": "7.28.5", - "@babel/plugin-transform-runtime": "7.28.5", - "@babel/preset-env": "7.28.5", - "@babel/register": "7.28.3", - "@biomejs/biome": "2.3.11", - "@pinia/testing": "1.0.3", + "@babel/core": "7.21.8", + "@babel/eslint-parser": "7.21.8", + "@babel/plugin-transform-runtime": "7.21.4", + "@babel/preset-env": "7.21.5", + "@babel/register": "7.21.0", + "@intlify/vue-i18n-loader": "5.0.1", "@ungap/event-target": "0.2.4", - "@vitejs/devtools": "^0.3.1", - "@vitejs/plugin-vue": "^6.0.7", - "@vitejs/plugin-vue-jsx": "^5.1.5", - "@vitest/browser-playwright": "^4.1.7", - "@vitest/browser": "^4.1.7", - "@vitest/ui": "^4.1.7", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", - "@vue/babel-plugin-jsx": "1.5.0", - "@vue/compiler-sfc": "3.5.22", - "@vue/test-utils": "2.4.6", - "autoprefixer": "10.4.21", + "@vue/babel-plugin-jsx": "1.2.2", + "@vue/compiler-sfc": "3.2.45", + "@vue/test-utils": "2.2.8", + "autoprefixer": "10.4.19", + "babel-loader": "9.1.3", "babel-plugin-lodash": "3.3.4", - "chai": "5.3.3", - "chalk": "5.6.2", - "chromedriver": "135.0.4", + "chai": "4.3.7", + "chalk": "1.1.3", + "chromedriver": "108.0.0", "connect-history-api-fallback": "2.0.0", - "cross-spawn": "7.0.6", + "copy-webpack-plugin": "11.0.0", + "cross-spawn": "7.0.3", + "css-loader": "6.10.0", + "css-minimizer-webpack-plugin": "4.2.2", "custom-event-polyfill": "1.0.7", - "eslint": "9.39.2", - "eslint-config-standard": "17.1.0", + "eslint": "8.33.0", + "eslint-config-standard": "17.0.0", "eslint-formatter-friendly": "7.0.0", - "eslint-plugin-import": "2.32.0", - "eslint-plugin-n": "17.23.1", - "eslint-plugin-promise": "7.2.1", - "eslint-plugin-vue": "10.6.2", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-n": "15.6.1", + "eslint-plugin-promise": "6.1.1", + "eslint-plugin-vue": "9.9.0", + "eslint-webpack-plugin": "3.2.0", "eventsource-polyfill": "0.9.6", - "express": "5.1.0", - "function-bind": "1.1.2", - "http-proxy-middleware": "3.0.5", - "iso-639-1": "3.1.5", + "express": "4.18.2", + "function-bind": "1.1.1", + "html-webpack-plugin": "5.5.1", + "http-proxy-middleware": "2.0.6", + "iso-639-1": "2.1.15", + "json-loader": "0.5.7", + "karma": "6.4.4", + "karma-coverage": "2.2.0", + "karma-firefox-launcher": "2.1.3", + "karma-mocha": "2.0.1", + "karma-mocha-reporter": "2.2.5", + "karma-sinon-chai": "2.0.2", + "karma-sourcemap-loader": "0.3.8", + "karma-spec-reporter": "0.0.36", + "karma-webpack": "5.0.0", "lodash": "4.17.21", - "msw": "2.14.6", - "nightwatch": "3.12.2", - "oxc": "^1.0.1", - "playwright": "1.61.0", - "postcss": "8.5.6", + "mini-css-extract-plugin": "2.7.6", + "mocha": "10.2.0", + "nightwatch": "2.6.25", + "opn": "5.5.0", + "ora": "0.4.1", + "postcss": "8.4.23", "postcss-html": "^1.5.0", + "postcss-loader": "7.0.2", "postcss-scss": "^4.0.6", - "sass-embedded": "^1.100.0", - "selenium-server": "3.141.59", - "semver": "7.7.3", - "serve-static": "2.2.0", - "shelljs": "0.10.0", - "sinon": "20.0.0", - "sinon-chai": "4.0.1", - "stylelint": "16.25.0", + "sass": "1.60.0", + "sass-loader": "13.2.2", + "selenium-server": "2.53.1", + "semver": "7.3.8", + "serviceworker-webpack5-plugin": "2.0.0", + "shelljs": "0.8.5", + "sinon": "15.0.4", + "sinon-chai": "3.7.0", + "stylelint": "14.16.1", "stylelint-config-html": "^1.1.0", - "stylelint-config-recommended": "^16.0.0", - "stylelint-config-recommended-scss": "^14.0.0", - "stylelint-config-recommended-vue": "^1.6.0", - "stylelint-config-standard": "38.0.0", - "vite": "^8.0.0", - "vite-plugin-eslint2": "^5.1.0", - "vite-plugin-stylelint": "^6.1.0", - "vitest": "^4.1.7", - "vue-eslint-parser": "10.2.0" + "stylelint-config-recommended-scss": "^8.0.0", + "stylelint-config-recommended-vue": "^1.4.0", + "stylelint-config-standard": "29.0.0", + "stylelint-rscss": "0.4.0", + "stylelint-webpack-plugin": "^3.3.0", + "vue-loader": "17.0.1", + "vue-style-loader": "4.1.3", + "webpack": "5.75.0", + "webpack-dev-middleware": "3.7.3", + "webpack-hot-middleware": "2.25.3", + "webpack-merge": "0.20.0" }, - "type": "module", "engines": { - "node": ">= 16.0.0" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + "node": ">= 16.0.0", + "npm": ">= 3.0.0" + } } diff --git a/postcss.config.js b/postcss.config.js index b7fc12838..88752c6cb 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,5 +1,5 @@ -import autoprefixer from 'autoprefixer' - -export default { - plugins: [autoprefixer], +module.exports = { + plugins: [ + require('autoprefixer') + ] } diff --git a/public/static/.gitignore b/public/static/.gitignore deleted file mode 100644 index 3332292a1..000000000 --- a/public/static/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.custom.* diff --git a/public/static/palettes/index.json b/public/static/palettes/index.json deleted file mode 100644 index 3c4e37e44..000000000 --- a/public/static/palettes/index.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "pleroma-dark": [ - "Pleroma Dark", - "#121a24", - "#182230", - "#b9b9ba", - "#d8a070", - "#d31014", - "#0fa00f", - "#0095ff", - "#ffa500" - ], - "pleroma-light": [ - "Pleroma Light", - "#f2f4f6", - "#dbe0e8", - "#304055", - "#f86f0f", - "#d31014", - "#0fa00f", - "#0095ff", - "#ffa500" - ], - "classic-dark": { - "name": "Classic Dark", - "bg": "#161c20", - "fg": "#282e32", - "text": "#b9b9b9", - "link": "#baaa9c", - "cRed": "#d31014", - "cGreen": "#0fa00f", - "cBlue": "#0095ff", - "cOrange": "#ffa500" - }, - "bird": [ - "Bird", - "#f8fafd", - "#e6ecf0", - "#14171a", - "#0084b8", - "#e0245e", - "#17bf63", - "#1b95e0", - "#fab81e" - ], - "pleroma-amoled": [ - "Pleroma Dark AMOLED", - "#000000", - "#111111", - "#b0b0b1", - "#d8a070", - "#aa0000", - "#0fa00f", - "#0095ff", - "#d59500" - ], - "tomorrow-night": { - "name": "Tomorrow Night", - "bg": "#1d1f21", - "fg": "#373b41", - "link": "#81a2be", - "text": "#c5c8c6", - "cRed": "#cc6666", - "cBlue": "#8abeb7", - "cGreen": "#b5bd68", - "cOrange": "#de935f" - }, - "dracula": { - "name": "Dracula", - "bg": "#282A36", - "fg": "#44475A", - "link": "#BC92F9", - "text": "#f8f8f2", - "cRed": "#FF5555", - "cBlue": "#8BE9FD", - "cGreen": "#50FA7B", - "cOrange": "#FFB86C" - }, - "ir-black": [ - "Ir Black", - "#000000", - "#242422", - "#b5b3aa", - "#ff6c60", - "#FF6C60", - "#A8FF60", - "#96CBFE", - "#FFFFB6" - ], - "monokai": [ - "Monokai", - "#272822", - "#383830", - "#f8f8f2", - "#f92672", - "#F92672", - "#a6e22e", - "#66d9ef", - "#f4bf75" - ], - "purple-stream": { - "name": "Purple stream", - "bg": "#17171A", - "fg": "#450F92", - "link": "#8769B4", - "text": "#C0C0C5", - "cRed": "#EB0300", - "cBlue": "#4656FF", - "cGreen": "#B0E020", - "cOrange": "#FF9046" - }, - "feud": { - "name": "Feud", - "bg": "#323337", - "fg": "#1D1E21", - "link": "#18A0E3", - "accent": "#6671E2", - "text": "#DBDDE0", - "cRed": "#E05053", - "cBlue": "#6671E2", - "cGreen": "#3A8D5D", - "cOrange": "#DCAA45" - }, - "constabulary": { - "name": "Constabulary", - "bg": "#FFFFFF", - "fg": "#3B5897", - "link": "#28487C", - "text": "#333333", - "cRed": "#FA3C4C", - "cBlue": "#0083FF", - "cGreen": "#44BDC6", - "cOrange": "#FFC200" - } -} diff --git a/public/static/pleromatan_apology_fox_small.webp b/public/static/pleromatan_apology_fox_small.webp deleted file mode 100644 index eacbf3cbf..000000000 Binary files a/public/static/pleromatan_apology_fox_small.webp and /dev/null differ diff --git a/public/static/pleromatan_apology_small.webp b/public/static/pleromatan_apology_small.webp deleted file mode 100644 index e27e06d06..000000000 Binary files a/public/static/pleromatan_apology_small.webp and /dev/null differ diff --git a/public/static/pleromatan_orz.png b/public/static/pleromatan_orz.png deleted file mode 100644 index aa54411f1..000000000 Binary files a/public/static/pleromatan_orz.png and /dev/null differ diff --git a/public/static/pleromatan_orz_fox.png b/public/static/pleromatan_orz_fox.png deleted file mode 100644 index 936e42f11..000000000 Binary files a/public/static/pleromatan_orz_fox.png and /dev/null differ diff --git a/public/static/splash.css b/public/static/splash.css deleted file mode 100644 index caf57dacb..000000000 --- a/public/static/splash.css +++ /dev/null @@ -1,133 +0,0 @@ -body { - margin: 0; - padding: 0; -} - -#splash { - --scale: 1; - - width: 100vw; - height: 100vh; - display: grid; - grid-template-rows: auto; - grid-template-columns: auto; - align-content: center; - place-items: center; - flex-direction: column; - background: #0f161e; - font-family: sans-serif; - color: #b9b9ba; - position: absolute; - z-index: 9999; - font-size: calc(1vw + 1vh + 1vmin); - opacity: 1; - transition: opacity 500ms ease-out 2s; -} - -#splash.hidden, -#splash.initial-hidden { - opacity: 0; -} - -#splash-credit { - position: absolute; - font-size: 1em; - bottom: 1em; - right: 1em; -} - -#splash-container { - align-items: center; -} - -#mascot-container { - display: flex; - align-items: flex-end; - justify-content: center; - perspective: 60em; - perspective-origin: 0 -15em; - transform-style: preserve-3d; -} - -#mascot { - width: calc(10em * var(--scale)); - height: calc(10em * var(--scale)); - object-fit: contain; - object-position: bottom; - transform: translateZ(-2em); -} - -#throbber { - display: grid; - width: calc(5em * 0.5 * var(--scale)); - height: calc(8em * 0.5 * var(--scale)); - margin-left: 4.1em; - z-index: 2; - grid-template-rows: repeat(8, 1fr); - grid-template-columns: repeat(5, 1fr); - grid-template-areas: - "P P . L L" - "P P . L L" - "P P . L L" - "P P . L L" - "P P . . ." - "P P . . ." - "P P . E E" - "P P . E E"; - - --logoChunkSize: calc(2em * 0.5 * var(--scale)); -} - -.chunk { - background-color: #e2b188; - box-shadow: 0.01em 0.01em 0.1em 0 #e2b188; -} - -#chunk-P { - grid-area: P; - border-top-left-radius: calc(var(--logoChunkSize) / 2); -} - -#chunk-L { - grid-area: L; - border-bottom-right-radius: calc(var(--logoChunkSize) / 2); -} - -#chunk-E { - grid-area: E; - border-bottom-right-radius: calc(var(--logoChunkSize) / 2); -} - -#status { - margin-top: 1em; - line-height: 2; - width: 100%; - text-align: center; -} - -#statusError { - display: none; - margin-top: 1em; - font-size: calc(1vw + 1vh + 1vmin); - line-height: 2; - width: 100%; - text-align: center; -} - -#statusStack { - display: none; - margin-top: 1em; - font-size: calc((1vw + 1vh + 1vmin) / 2.5); - width: calc(100vw - 5em); - padding: 1em; - text-overflow: ellipsis; - overflow-x: hidden; - text-align: left; - line-height: 2; -} - -@media (prefers-reduced-motion) { - #throbber { - animation: none !important; - } -} diff --git a/public/static/styles.json b/public/static/styles.json deleted file mode 100644 index 2f836a47a..000000000 --- a/public/static/styles.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "pleroma-dark": "/static/themes/pleroma-dark.json", - "pleroma-light": "/static/themes/pleroma-light.json", - "redmond-xx": "/static/themes/redmond-xx.json", - "redmond-xx-se": "/static/themes/redmond-xx-se.json", - "redmond-xxi": "/static/themes/redmond-xxi.json", - "breezy-dark": "/static/themes/breezy-dark.json", - "breezy-light": "/static/themes/breezy-light.json", - "mammal": "/static/themes/mammal.json", - "paper": "/static/themes/paper.json" -} diff --git a/public/static/styles/Breezy DX.iss b/public/static/styles/Breezy DX.iss deleted file mode 100644 index 69cc3befe..000000000 --- a/public/static/styles/Breezy DX.iss +++ /dev/null @@ -1,102 +0,0 @@ -@meta { - name: Breezy DX; - author: HJ; - license: WTFPL; - website: ebin.club; -} - -@palette.Dark { - bg: #292C32; - fg: #292C32; - text: #ffffff; - link: #1CA4F3; - accent: #1CA4F3; - cRed: #f41a51; - cBlue: #1CA4F3; - cGreen: #1af46e; - cOrange: #f4af1a; -} - -@palette.Light { - bg: #EFF0F2; - fg: #EFF0F2; - text: #1B1F22; - underlay: #5d6086; - accent: #1CA4F3; - cBlue: #1CA4F3; - cRed: #f41a51; - cGreen: #0b6a30; - cOrange: #f4af1a; - border: #d8e6f9; - link: #1CA4F3; -} - -@palette.Panda { - bg: #EFF0F2; - fg: #292C32; - text: #1B1F22; - link: #1CA4F3; - accent: #1CA4F3; - cRed: #f41a51; - cBlue: #1CA4F3; - cGreen: #0b6a30; - cOrange: #f4af1a; -} - -Root { - --badgeNotification: color | --cRed; - --buttonDefaultHoverGlow: shadow | inset 0 0 0 1 --accent / 1; - --buttonDefaultFocusGlow: shadow | inset 0 0 0 1 --accent / 1; - --buttonDefaultShadow: shadow | inset 0 0 0 1 --text / 0.35, 0 5 5 -5 #000000 / 0.35; - --buttonDefaultBevel: shadow | inset 0 14 14 -14 #FFFFFF / 0.1; - --buttonPressedBevel: shadow | inset 0 -20 20 -20 #000000 / 0.05; - --defaultInputBevel: shadow | inset 0 0 0 1 --text / 0.35; - --defaultInputHoverGlow: shadow | 0 0 0 1 --accent / 1; - --defaultInputFocusGlow: shadow | 0 0 0 1 --link / 1; -} - -Button { - background: --parent; -} - -Button:disabled { - shadow: --buttonDefaultBevel, --buttonDefaultShadow -} - -Button:hover { - background: --inheritedBackground; - shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow -} - -Button:toggled { - background: $blend(--inheritedBackground 0.3 --accent) -} - -Button:pressed { - background: $blend(--inheritedBackground 0.8 --accent) -} - -Button:pressed:toggled { - background: $blend(--inheritedBackground 0.2 --accent) -} - -Button:toggled:hover { - background: $blend(--inheritedBackground 0.3 --accent) -} - -Input { - shadow: --defaultInputBevel; - background: $mod(--bg -10); -} - -PanelHeader { - shadow: inset 0 30 30 -30 #ffffff / 0.25 -} - -Tab:hover { - shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow -} - -Tab { - background: --bg; -} diff --git a/public/static/styles/Redmond DX.iss b/public/static/styles/Redmond DX.iss deleted file mode 100644 index 919be056b..000000000 --- a/public/static/styles/Redmond DX.iss +++ /dev/null @@ -1,201 +0,0 @@ -@meta { - name: Redmond DX; - author: HJ; - license: WTFPL; - website: ebin.club; -} - -@palette.Modern { - bg: #D3CFC7; - fg: #092369; - text: #000000; - link: #0000FF; - accent: #A5C9F0; - cRed: #FF3000; - cBlue: #009EFF; - cGreen: #309E00; - cOrange: #FFCE00; -} - -@palette.Classic { - bg: #BFBFBF; - fg: #000180; - text: #000000; - link: #0000FF; - accent: #A5C9F0; - cRed: #FF0000; - cBlue: #2E2ECE; - cGreen: #007E00; - cOrange: #CE8F5F; -} - -@palette.Vapor { - bg: #F0ADCD; - fg: #bca4ee; - text: #602040; - link: #064745; - accent: #9DF7C8; - cRed: #86004a; - cBlue: #0e5663; - cGreen: #0a8b51; - cOrange: #787424; -} - -Root { - --gradientColor: color | --accent; - --inputColor: color | #FFFFFF; - --bevelLight: color | $brightness(--bg 50); - --bevelDark: color | $brightness(--bg -20); - --bevelExtraDark: color | #404040; - --buttonDefaultBevel: shadow | $borderSide(--bevelExtraDark bottom-right 1 1), $borderSide(--bevelLight top-left 1 1), $borderSide(--bevelDark bottom-right 1 2); - --buttonPressedFocusedBevel: shadow | inset 0 0 0 1 #000000 / 1 #Outer , inset 0 0 0 2 --bevelExtraDark / 1 #inner; - --buttonPressedBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2); - --defaultInputBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2), $borderSide(--bg bottom-right 1 2); -} - -Button:toggled { - background: --bg; - shadow: --buttonPressedBevel -} - -Button:focused { - shadow: --buttonDefaultBevel, 0 0 0 1 #000000 / 1 -} - -Button:pressed { - shadow: --buttonPressedBevel -} - -Button:hover { - shadow: --buttonDefaultBevel; - background: --bg -} - -Button { - shadow: --buttonDefaultBevel; - background: --bg; - roundness: 0 -} - -Button:pressed:hover { - shadow: --buttonPressedBevel -} - -Button:hover:pressed:focused { - shadow: --buttonPressedFocusedBevel -} - -Button:pressed:focused { - shadow: --buttonPressedFocusedBevel -} - -Button:toggled:pressed { - shadow: --buttonPressedFocusedBevel -} - -Input { - background: $boost(--bg 20); - shadow: --defaultInputBevel; - roundness: 0 -} - -Input:focused { - shadow: inset 0 0 0 1 #000000 / 1, --defaultInputBevel -} - -Input:focused:hover { - shadow: --defaultInputBevel -} - -Input:focused:hover:disabled { - shadow: --defaultInputBevel -} - -Input:hover { - shadow: --defaultInputBevel -} - -Input:disabled { - shadow: --defaultInputBevel -} - -Panel { - shadow: --buttonDefaultBevel; - roundness: 0 -} - -PanelHeader { - shadow: inset -1100 0 1000 -1000 --gradientColor / 1 #Gradient ; - background: --fg -} - -PanelHeader ButtonUnstyled Icon { - textColor: --text; - textAuto: 'no-preserve' -} - -PanelHeader Button Icon { - textColor: --text; - textAuto: 'no-preserve' -} - -PanelHeader Button Text { - textColor: --text; - textAuto: 'no-preserve' -} - -Tab:hover { - background: --bg; - shadow: --buttonDefaultBevel -} - -Tab:active { - background: --bg -} - -Tab:active:hover { - background: --bg; - shadow: --defaultButtonBevel -} - -Tab:active:hover:disabled { - background: --bg -} - -Tab:hover:disabled { - background: --bg -} - -Tab:disabled { - background: --bg -} - -Tab { - background: --bg; - shadow: --buttonDefaultBevel -} - -Tab:hover:active { - shadow: --buttonDefaultBevel -} - -TopBar Link { - textColor: #ffffff -} - -MenuItem:hover { - background: --fg -} - -MenuItem:active { - background: --fg -} - -MenuItem:active:hover { - background: --fg -} - -Popover { - shadow: --buttonDefaultBevel, 5 5 0 0 #000000 / 0.2; - roundness: 0 -} diff --git a/public/static/styles/index.json b/public/static/styles/index.json deleted file mode 100644 index 6c1dd1676..000000000 --- a/public/static/styles/index.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "RedmondDX": "/static/styles/Redmond DX.iss", - "BreezyDX": "/static/styles/Breezy DX.iss" -} diff --git a/renovate.json b/renovate.json index 4bd832f5f..39a2b6e9a 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base"] + "extends": [ + "config:base" + ] } diff --git a/src/App.js b/src/App.js index 934b96713..b7eb2f72e 100644 --- a/src/App.js +++ b/src/App.js @@ -1,306 +1,135 @@ -import { throttle } from 'lodash' -import { mapState } from 'pinia' +import UserPanel from './components/user_panel/user_panel.vue' +import NavPanel from './components/nav_panel/nav_panel.vue' +import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue' +import FeaturesPanel from './components/features_panel/features_panel.vue' +import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' +import ShoutPanel from './components/shout_panel/shout_panel.vue' +import MediaModal from './components/media_modal/media_modal.vue' +import SideDrawer from './components/side_drawer/side_drawer.vue' +import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' +import MobileNav from './components/mobile_nav/mobile_nav.vue' +import DesktopNav from './components/desktop_nav/desktop_nav.vue' +import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' +import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue' +import PostStatusModal from './components/post_status_modal/post_status_modal.vue' +import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue' +import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' +import { windowWidth, windowHeight } from './services/window_utils/window_utils' +import { mapGetters } from 'vuex' import { defineAsyncComponent } from 'vue' -import DesktopNav from 'src/components/desktop_nav/desktop_nav.vue' -import FeaturesPanel from 'src/components/features_panel/features_panel.vue' -import GlobalError from 'src/components/global_error/global_error.vue' -import GlobalNoticeList from 'src/components/global_notice_list/global_notice_list.vue' -import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue' -import MobileNav from 'src/components/mobile_nav/mobile_nav.vue' -import MobilePostStatusButton from 'src/components/mobile_post_status_button/mobile_post_status_button.vue' -import NavPanel from 'src/components/nav_panel/nav_panel.vue' -import UserPanel from 'src/components/user_panel/user_panel.vue' -import { getOrCreateServiceWorker } from './services/sw/sw' -import { windowHeight, windowWidth } from './services/window_utils/window_utils' - -import { useEmojiStore } from 'src/stores/emoji.js' -import { useI18nStore } from 'src/stores/i18n.js' -import { useInstanceStore } from 'src/stores/instance.js' -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' -import { useInterfaceStore } from 'src/stores/interface.js' -import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useShoutStore } from 'src/stores/shout.js' - -// Helper to unwrap reactive proxies -window.toValue = (x) => JSON.parse(JSON.stringify(x)) - export default { name: 'app', components: { UserPanel, NavPanel, - Notifications: defineAsyncComponent( - () => import('src/components/notifications/notifications.vue'), - ), + Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')), InstanceSpecificPanel, FeaturesPanel, - WhoToFollowPanel: defineAsyncComponent( - () => - import('src/components/who_to_follow_panel/who_to_follow_panel.vue'), - ), - ShoutPanel: defineAsyncComponent( - () => import('src/components/shout_panel/shout_panel.vue'), - ), - MediaModal: defineAsyncComponent( - () => import('src/components/media_modal/media_modal.vue'), - ), + WhoToFollowPanel, + ShoutPanel, + MediaModal, + SideDrawer, MobilePostStatusButton, MobileNav, DesktopNav, - SettingsModal: defineAsyncComponent( - () => import('src/components/settings_modal/settings_modal.vue'), - ), - UpdateNotification: defineAsyncComponent( - () => - import('src/components/update_notification/update_notification.vue'), - ), - PostStatusModal: defineAsyncComponent( - () => import('src/components/post_status_modal/post_status_modal.vue'), - ), - UserReportingModal: defineAsyncComponent( - () => - import('src/components/user_reporting_modal/user_reporting_modal.vue'), - ), - EditStatusModal: defineAsyncComponent( - () => import('src/components/edit_status_modal/edit_status_modal.vue'), - ), - StatusHistoryModal: defineAsyncComponent( - () => - import('src/components/status_history_modal/status_history_modal.vue'), - ), - GlobalError, - GlobalNoticeList, + SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')), + UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')), + UserReportingModal, + PostStatusModal, + EditStatusModal, + StatusHistoryModal, + GlobalNoticeList }, data: () => ({ - mobileActivePanel: 'timeline', + mobileActivePanel: 'timeline' }), - provide() { - return { - allowNonSquareEmoji: useMergedConfigStore().mergedConfig.nonSquareEmoji, - } - }, - watch: { - themeApplied() { - this.removeSplash() - }, - currentTheme() { - this.setThemeBodyClass() - }, - layoutType() { - document.getElementById('modal').classList = ['-' + this.layoutType] - }, - }, - created() { + created () { // Load the locale from the storage - const value = useMergedConfigStore().mergedConfig.interfaceLanguage - useI18nStore().setLanguage(value) - useEmojiStore().loadUnicodeEmojiData(value) - - document.getElementById('modal').classList = ['-' + this.layoutType] - - // Create bound handlers - this.updateScrollState = throttle(this.scrollHandler, 200) - this.updateMobileState = throttle(this.resizeHandler, 200) - }, - mounted() { + const val = this.$store.getters.mergedConfig.interfaceLanguage + this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) window.addEventListener('resize', this.updateMobileState) - this.scrollParent.addEventListener('scroll', this.updateScrollState) - - if (this.themeApplied) { - this.setThemeBodyClass() - this.removeSplash() - } - getOrCreateServiceWorker() }, - unmounted() { + unmounted () { window.removeEventListener('resize', this.updateMobileState) - this.scrollParent.removeEventListener('scroll', this.updateScrollState) }, computed: { - currentTheme() { - if (this.styleDataUsed) { - const styleMeta = this.styleDataUsed.find( - (x) => x.component === '@meta', - ) - - if (styleMeta !== undefined) { - return styleMeta.directives.name.replaceAll(' ', '-').toLowerCase() - } - } - - return 'stock' - }, - layoutModalClass() { - return '-' + this.layoutType - }, - classes() { + classes () { return [ { '-reverse': this.reverseLayout, '-no-sticky-headers': this.noSticky, - '-has-new-post-button': this.newPostButtonShown, + '-has-new-post-button': this.newPostButtonShown }, - '-' + this.layoutType, + '-' + this.layoutType ] }, - navClasses() { - const { navbarColumnStretch } = useMergedConfigStore().mergedConfig + navClasses () { + const { navbarColumnStretch } = this.$store.getters.mergedConfig return [ '-' + this.layoutType, - ...(navbarColumnStretch ? ['-column-stretch'] : []), + ...(navbarColumnStretch ? ['-column-stretch'] : []) ] }, - currentUser() { - return this.$store.state.users.currentUser - }, - userBackground() { - return this.currentUser.background_image - }, - foreignProfileBackground() { - return ( - useMergedConfigStore().mergedConfig.allowForeignUserBackground && - useInterfaceStore().foreignProfileBackground - ) - }, - instanceBackground() { - return useMergedConfigStore().mergedConfig.hideInstanceWallpaper + currentUser () { return this.$store.state.users.currentUser }, + userBackground () { return this.currentUser.background_image }, + instanceBackground () { + return this.mergedConfig.hideInstanceWallpaper ? null - : this.instanceBackgroundUrl + : this.$store.state.instance.background }, - background() { - return ( - this.foreignProfileBackground || - this.userBackground || - this.instanceBackground - ) - }, - bgStyle() { + background () { return this.userBackground || this.instanceBackground }, + bgStyle () { if (this.background) { return { - '--body-background-image': `url(${this.background})`, + '--body-background-image': `url(${this.background})` } } }, - shoutJoined() { - return useShoutStore().joined + shout () { return this.$store.state.shout.joined }, + suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, + showInstanceSpecificPanel () { + return this.$store.state.instance.showInstanceSpecificPanel && + !this.$store.getters.mergedConfig.hideISP && + this.$store.state.instance.instanceSpecificPanelContent }, - isChats() { + isChats () { return this.$route.name === 'chat' || this.$route.name === 'chats' }, - isListEdit() { + isListEdit () { return this.$route.name === 'lists-edit' }, - newPostButtonShown() { + newPostButtonShown () { if (this.isChats) return false if (this.isListEdit) return false - return ( - useMergedConfigStore().mergedConfig.alwaysShowNewPostButton || - this.layoutType === 'mobile' - ) + return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' }, - shoutboxPosition() { - return ( - useMergedConfigStore().mergedConfig.alwaysShowNewPostButton || false - ) + showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, + editingAvailable () { return this.$store.state.instance.editingAvailable }, + shoutboxPosition () { + return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false }, - hideShoutbox() { - return useMergedConfigStore().mergedConfig.hideShoutbox + hideShoutbox () { + return this.$store.getters.mergedConfig.hideShoutbox }, - reverseLayout() { - const { thirdColumnMode, sidebarRight: reverseSetting } = - useMergedConfigStore().mergedConfig + layoutType () { return this.$store.state.interface.layoutType }, + privateMode () { return this.$store.state.instance.private }, + reverseLayout () { + const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig if (this.layoutType !== 'wide') { return reverseSetting } else { - return thirdColumnMode === 'notifications' - ? reverseSetting - : !reverseSetting + return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting } }, - noSticky() { - return useMergedConfigStore().mergedConfig.disableStickyHeaders - }, - showScrollbars() { - return useMergedConfigStore().mergedConfig.showScrollbars - }, - scrollParent() { - return window /* this.$refs.appContentRef */ - }, - showInstanceSpecificPanel() { - return ( - this.instanceSpecificPanelPresent && - !useMergedConfigStore().mergedConfig.hideISP - ) - }, - ...mapState(useMergedConfigStore, ['mergedConfig']), - ...mapState(useInterfaceStore, [ - 'themeApplied', - 'styleDataUsed', - 'layoutType', - ]), - ...mapState(useInstanceStore, ['styleDataUsed']), - ...mapState(useInstanceCapabilitiesStore, [ - 'suggestionsEnabled', - 'editingAvailable', - ]), - ...mapState(useInstanceStore, { - instanceBackgroundUrl: (store) => store.instanceIdentity.background, - showFeaturesPanel: (store) => store.instanceIdentity.showFeaturesPanel, - instanceSpecificPanelPresent: (store) => - store.instanceIdentity.showInstanceSpecificPanel && - store.instanceIdentity.instanceSpecificPanelContent, - }), + noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, + showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars }, + ...mapGetters(['mergedConfig']) }, methods: { - resizeHandler() { - useInterfaceStore().setLayoutWidth(windowWidth()) - useInterfaceStore().setLayoutHeight(windowHeight()) - }, - scrollHandler() { - const scrollPosition = - this.scrollParent === window - ? window.scrollY - : this.scrollParent.scrollTop - - if (scrollPosition != 0) { - this.$refs.appContentRef.classList.add(['-scrolled']) - } else { - this.$refs.appContentRef.classList.remove(['-scrolled']) - } - }, - setThemeBodyClass() { - const themeName = this.currentTheme - const classList = Array.from(document.body.classList) - const oldTheme = classList.filter((c) => c.startsWith('theme-')) - - if (themeName !== null && themeName !== '') { - const newTheme = `theme-${themeName.toLowerCase()}` - - // remove old theme reference if there are any - if (oldTheme.length) { - document.body.classList.replace(oldTheme[0], newTheme) - } else { - document.body.classList.add(newTheme) - } - } else { - // remove theme reference if non-V3 theme is used - document.body.classList.remove(...oldTheme) - } - }, - removeSplash() { - document.querySelector('#status').textContent = this.$t( - 'splash.fun_' + Math.ceil(Math.random() * 4), - ) - const splashscreenRoot = document.querySelector('#splash') - splashscreenRoot.addEventListener('transitionend', () => { - splashscreenRoot.remove() - }) - setTimeout(() => { - splashscreenRoot.remove() // forcibly remove it, should fix my plasma browser widget t. HJ - }, 600) - splashscreenRoot.classList.add('hidden') - document.querySelector('#app').classList.remove('hidden') - }, - }, + updateMobileState () { + this.$store.dispatch('setLayoutWidth', windowWidth()) + this.$store.dispatch('setLayoutHeight', windowHeight()) + } + } } diff --git a/src/App.scss b/src/App.scss index 0de5a3577..9d1ce77a6 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,9 +1,6 @@ // stylelint-disable rscss/class-format /* stylelint-disable no-descending-specificity */ -@use "panel"; - -@import '@fortawesome/fontawesome-svg-core/styles.css'; -@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css'; +@import "./panel"; :root { --status-margin: 0.75em; @@ -21,7 +18,7 @@ } html { - font-size: var(--textSize, 1rem); + font-size: var(--textSize, 14px); --navbar-height: var(--navbarSize, 3.5rem); --emoji-size: var(--emojiSize, 32px); @@ -33,12 +30,12 @@ body { font-family: sans-serif; font-family: var(--font); margin: 0; - padding: 0; color: var(--text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overscroll-behavior-y: none; - overflow: clip scroll; + overflow-x: clip; + overflow-y: scroll; &.hidden { display: none; @@ -50,7 +47,7 @@ body { // have a cursor/pointer to operate them @media (any-pointer: fine) { * { - scrollbar-color: var(--icon) transparent; + scrollbar-color: var(--fg) transparent; &::-webkit-scrollbar { background: transparent; @@ -130,7 +127,7 @@ body { } // Body should have background to scrollbar otherwise it will use white (body color?) html { - scrollbar-color: var(--icon) var(--wallpaper); + scrollbar-color: var(--fg) var(--wallpaper); background: var(--wallpaper); } } @@ -144,25 +141,6 @@ h4 { margin: 0; } -code { - background: var(--background); - border: 1px solid var(--border); - border-radius: var(--roundness); - padding: 0 0.2em; - - & pre, - pre & { - display: block; - overflow-x: auto; - padding: 0.2em; - } - - &.pre { - white-space: pre; - display: block; - } -} - .iconLetter { display: inline-block; text-align: center; @@ -219,7 +197,6 @@ nav { background-color: var(--wallpaper); background-image: var(--body-background-image); background-position: 50%; - transition: background-image 1s; } .underlay { @@ -247,8 +224,9 @@ nav { grid-template-rows: 1fr; box-sizing: border-box; margin: 0 auto; - place-content: flex-start center; + align-content: flex-start; flex-wrap: wrap; + justify-content: center; min-height: 100vh; overflow-x: clip; @@ -284,7 +262,8 @@ nav { position: sticky; top: var(--navbar-height); max-height: calc(100vh - var(--navbar-height)); - overflow: hidden auto; + overflow-y: auto; + overflow-x: hidden; margin-left: calc(var(--___paddingIncrease) * -1); padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); @@ -402,10 +381,6 @@ nav { font-family: sans-serif; font-family: var(--font); - &.-transparent { - backdrop-filter: blur(0.125em) contrast(60%); - } - &::-moz-focus-inner { border: none; } @@ -413,36 +388,6 @@ nav { &:disabled { cursor: not-allowed; } - - &:active { - transform: translate(1px, 1px); - } -} - -.menu-item { - line-height: var(--__line-height); - font-family: inherit; - font-weight: 400; - font-size: 100%; - cursor: pointer; - - a, - button:not(.button-default) { - color: var(--text); - font-size: 100%; - text-align: initial; - padding: 0; - background: none; - border: none; - outline: none; - display: inline; - font-family: inherit; - line-height: unset; - } - - &.disabled { - cursor: not-allowed; - } } .menu-item, @@ -452,17 +397,76 @@ nav { border: none; outline: none; text-align: initial; + font-size: inherit; + font-family: inherit; + font-weight: 400; + cursor: pointer; color: inherit; clear: both; position: relative; white-space: nowrap; + border-color: var(--border); + border-style: solid; + border-width: 0; + border-top-width: 1px; width: 100%; + line-height: var(--__line-height); padding: var(--__vertical-gap) var(--__horizontal-gap); background: transparent; --__line-height: 1.5em; --__horizontal-gap: 0.75em; --__vertical-gap: 0.5em; + + &.-non-interactive { + cursor: auto; + } + + &.-active, + &:hover { + border-top-width: 1px; + border-bottom-width: 1px; + } + + &.-active + &, + &:hover + & { + border-top-width: 0; + } + + &:hover + .menu-item-collapsible:not(.-expanded) + &, + &.-active + .menu-item-collapsible:not(.-expanded) + & { + border-top-width: 0; + } + + &[aria-expanded="true"] { + border-bottom-width: 1px; + } + + a, + button:not(.button-default) { + text-align: initial; + padding: 0; + background: none; + border: none; + outline: none; + display: inline; + font-size: 100%; + font-family: inherit; + line-height: unset; + color: var(--text); + } + + &:first-child { + border-top-right-radius: var(--roundness); + border-top-left-radius: var(--roundness); + border-top-width: 0; + } + + &:last-child { + border-bottom-right-radius: var(--roundness); + border-bottom-left-radius: var(--roundness); + border-bottom-width: 0; + } } .button-unstyled { @@ -486,12 +490,6 @@ nav { } } -label { - &.-disabled { - color: var(--textFaint); - } -} - input, textarea { border: none; @@ -508,10 +506,6 @@ textarea { height: unset; } - &::placeholder { - color: var(--textFaint) - } - --_padding: 0.5em; border: none; @@ -532,10 +526,6 @@ textarea { &[disabled="disabled"], &.disabled { cursor: not-allowed; - color: var(--textFaint); - - /* stylelint-disable-next-line declaration-no-important */ - background-color: transparent !important; } &[type="range"] { @@ -561,8 +551,6 @@ textarea { & + label::before { opacity: 0.5; } - - background-color: var(--background); } + label::before { @@ -662,8 +650,7 @@ option { list-style: none; display: grid; grid-auto-flow: row dense; - grid-template-columns: repeat(auto-fit, minmax(20em, 1fr)); - grid-gap: 0.5em; + grid-template-columns: 1fr 1fr; li { border: 1px solid var(--border); @@ -673,6 +660,11 @@ option { } } +.btn-block { + display: block; + width: 100%; +} + .btn-group { position: relative; display: inline-flex; @@ -684,6 +676,7 @@ option { --_roundness-right: 0; position: relative; + flex: 1 1 auto; } > *:first-child, @@ -730,15 +723,17 @@ option { } &.-dot { - min-height: 0.6em; - max-height: 0.6em; - min-width: 0.6em; - max-width: 0.6em; - left: calc(50% + 0.5em); - top: calc(50% - 1em); - line-height: 0; + min-height: 8px; + max-height: 8px; + min-width: 8px; + max-width: 8px; padding: 0; - margin: 0; + line-height: 0; + font-size: 0; + left: calc(50% - 4px); + top: calc(50% - 4px); + margin-left: 6px; + margin-top: -6px; } &.-counter { @@ -760,19 +755,6 @@ option { padding: 0 0.25em; border-radius: var(--roundness); border: 1px solid var(--border); - - &.-dismissible { - display: flex; - padding-left: 0.5em; - margin: 0; - align-items: baseline; - line-height: 2; - - span { - display: block; - flex: 1 0 auto; - } - } } .faint { @@ -782,20 +764,21 @@ option { color: var(--text); } -.notice-dismissible { - display: flex; - padding: 0.75em 1em; - align-items: baseline; - line-height: 1.5; +.visibility-notice { + padding: 0.5em; + border: 1px solid var(--textFaint); + border-radius: var(--roundness); +} - p, - span { - display: block; - flex: 1 1 auto; - margin: 0; - } +.notice-dismissible { + padding-right: 4rem; + position: relative; .dismiss { + position: absolute; + top: 0; + right: 0; + padding: 0.5em; color: inherit; } } @@ -831,7 +814,7 @@ option { .login-hint { text-align: center; - @media all and (width >= 801px) { + @media all and (min-width: 801px) { display: none; } @@ -853,7 +836,7 @@ option { flex: 1; } -@media all and (width <= 800px) { +@media all and (max-width: 800px) { .mobile-hidden { display: none; } @@ -931,169 +914,3 @@ option { color: var(--selectionText); background-color: var(--selectionBackground); } - -#splash { - pointer-events: none; - // transition: opacity 0.5s; - - #status { - &.css-ok { - &::before { - display: inline-block; - content: "CSS OK"; - } - } - - .initial-text { - display: none; - } - } - - #throbber { - animation-duration: 3s; - animation-name: bounce; - animation-iteration-count: infinite; - animation-direction: normal; - transform-origin: bottom center; - - &.dead { - animation-name: dead; - animation-duration: 0.5s; - animation-iteration-count: 1; - transform: rotateX(90deg) rotateY(0) rotateZ(-45deg); - } - - @keyframes dead { - 0% { - transform: rotateX(0) rotateY(0) rotateZ(0); - } - - 5% { - transform: rotateX(0) rotateY(0) rotateZ(1deg); - } - - 10% { - transform: rotateX(0) rotateY(0) rotateZ(-2deg); - } - - 15% { - transform: rotateX(0) rotateY(0) rotateZ(3deg); - } - - 20% { - transform: rotateX(0) rotateY(0) rotateZ(0); - } - - 25% { - transform: rotateX(0) rotateY(0) rotateZ(0); - } - - 30% { - transform: rotateX(10deg) rotateY(0) rotateZ(0); - } - - 35% { - transform: rotateX(-10deg) rotateY(0) rotateZ(0); - } - - 40% { - transform: rotateX(10deg) rotateY(0) rotateZ(0); - } - - 45% { - transform: rotateX(-10deg) rotateY(0) rotateZ(0); - } - - 50% { - transform: rotateX(10deg) rotateY(0) rotateZ(0); - } - - 100% { - transform: rotateX(90deg) rotateY(0) rotateZ(-45deg); - transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); /* easeInQuint */ - } - } - - @keyframes bounce { - 0% { - scale: 1 1; - translate: 0 0; - animation-timing-function: ease-out; - } - - 10% { - scale: 1.2 0.8; - translate: 0 0; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-out; - } - - 30% { - scale: 0.9 1.1; - translate: 0 -40%; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-in; - } - - 40% { - scale: 1.1 0.9; - translate: 0 -50%; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-in; - } - - 45% { - scale: 0.9 1.1; - translate: 0 -45%; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-in; - } - - 50% { - scale: 1.05 0.95; - translate: 0 -40%; - animation-timing-function: ease-in; - } - - 55% { - scale: 0.985 1.025; - translate: 0 -35%; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-in; - } - - 60% { - scale: 1.0125 0.9985; - translate: 0 -30%; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-in; - } - - 80% { - scale: 1.0063 0.9938; - translate: 0 -10%; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-in-out; - } - - 90% { - scale: 1.2 0.8; - translate: 0 0; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-out; - } - - 100% { - scale: 1 1; - translate: 0 0; - transform: rotateZ(var(--defaultZ)); - animation-timing-function: ease-out; - } - } - } -} - -@property --shadow { - syntax: "*"; - inherits: false; -} diff --git a/src/App.vue b/src/App.vue index 84a6f7d3d..9d7ad9129 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,6 @@ diff --git a/src/api/admin.js b/src/api/admin.js deleted file mode 100644 index c33f863a7..000000000 --- a/src/api/admin.js +++ /dev/null @@ -1,491 +0,0 @@ -import { promisedRequest } from './helpers.js' - -const REPORTS = '/api/v1/pleroma/admin/reports' -const CONFIG_URL = '/api/v1/pleroma/admin/config' -const DESCRIPTIONS_URL = '/api/v1/pleroma/admin/config/descriptions' - -const ANNOUNCEMENTS_URL = (id = '') => - `/api/v1/pleroma/admin/announcements/${id}` - -const FRONTENDS_URL = '/api/v1/pleroma/admin/frontends' -const FRONTENDS_INSTALL_URL = '/api/v1/pleroma/admin/frontends/install' - -const USERS_URL = (nickname = '') => `/api/v1/pleroma/admin/users/${nickname}` -const USERS_URL_LIST = ({ - page, - pageSize, - filters = {}, - query = '', - name = '', - email = '', -}) => { - const { - local = false, - external = false, - active = false, - needApproval = false, - unconfirmed = false, - deactivated = false, - isAdmin = true, - isModerator = true, - } = filters - const filters_str = [ - local && 'local', - external && 'external', - active && 'active', - needApproval && 'need_approval', - unconfirmed && 'unconfirmed', - deactivated && 'deactivated', - isAdmin && 'is_admin', - isModerator && 'is_moderator', - ] - .filter((x) => x) - .join(',') - return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}` -} - -const TAG_USER_URL = '/api/pleroma/admin/users/tag' - -const PERMISSION_GROUP_URL = (right) => - `/api/pleroma/admin/users/permission_group/${right}` -const ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate' -const DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate' -const SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest' -const UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest' -const APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve' -const CONFIRM_USERS_URL = '/api/v1/pleroma/admin/users/confirm_email' -const RESEND_CONFIRMATION_EMAIL_URL = - '/api/v1/pleroma/admin/users/resend_confirmation_email' -const LIST_STATUSES_URL = ({ id, page, pageSize, godmode, withReblogs }) => - `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&page=${page}&godmode=${godmode}&with_reblogs=${withReblogs}` -const CHANGE_STATUS_SCOPE_URL = (id) => `/api/v1/pleroma/admin/statuses/${id}` -const REQUIRE_PASSWORD_CHANGE_URL = - '/api/v1/pleroma/admin/users/force_password_reset' - -const DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa' -const EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' -const EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' -const EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}` -const EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download' -const EMOJI_PACKS_DL_REMOTE_ZIP_URL = '/api/v1/pleroma/emoji/packs/download_zip' -const EMOJI_PACKS_LS_REMOTE_URL = (url, page, pageSize) => - `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}` -const EMOJI_UPDATE_FILE_URL = (name) => - `/api/v1/pleroma/emoji/packs/files?name=${name}` - -// - -export const setUsersTags = ({ - tags, - credentials, - value, - screen_names: nicknames, -}) => - promisedRequest({ - url: TAG_USER_URL, - method: value ? 'PUT' : 'DELETE', - credentials, - payload: { - nicknames, - tags, - }, - }) - -export const setUsersRight = ({ - right, - credentials, - value, - screen_names: nicknames, -}) => - promisedRequest({ - url: PERMISSION_GROUP_URL(right), - method: value ? 'POST' : 'DELETE', - credentials, - payload: { - nicknames, - }, - }) - -export const setUsersActivationStatus = ({ - credentials, - screen_names: nicknames, - value, -}) => - promisedRequest({ - url: value ? ACTIVATE_USERS_URL : DEACTIVATE_USERS_URL, - method: 'PATCH', - credentials, - payload: { - nicknames, - }, - }).then((response) => response.users) - -export const setUsersApprovalStatus = ({ - credentials, - screen_names: nicknames, -}) => - promisedRequest({ - url: APPROVE_USERS_URL, - method: 'PATCH', - credentials, - payload: { - nicknames, - }, - }).then((response) => response.users) - -export const setUsersConfirmationStatus = ({ - credentials, - screen_names: nicknames, -}) => - promisedRequest({ - url: CONFIRM_USERS_URL, - method: 'PATCH', - credentials, - payload: { - nicknames, - }, - }).then((response) => response.users) - -export const setUsersSuggestionStatus = ({ - credentials, - screen_names: nicknames, - value, -}) => - promisedRequest({ - url: value ? SUGGEST_USERS_URL : UNSUGGEST_USERS_URL, - method: 'PATCH', - credentials, - payload: { - nicknames, - }, - }).then((response) => response.users) - -export const getUserData = ({ credentials, screen_name: nickname }) => - promisedRequest({ - url: USERS_URL(nickname), - method: 'GET', - credentials, - }) - -export const deleteAccounts = ({ credentials, screen_names: nicknames }) => - promisedRequest({ - url: USERS_URL(), - method: 'DELETE', - credentials, - payload: { - nicknames, - }, - }) - -export const getAnnouncements = ({ id, credentials }) => - promisedRequest({ url: ANNOUNCEMENTS_URL(id), credentials }) - -// the reported list is hardly useful because standards are for dating i guess, -// so make sure to fetchIfMissing right afterward using this call -export const listUsers = ({ opts, credentials }) => - promisedRequest({ - url: USERS_URL_LIST(opts), - credentials, - method: 'GET', - }) - -export const resendConfirmationEmail = ({ - screen_names: nicknames, - credentials, -}) => - promisedRequest({ - url: RESEND_CONFIRMATION_EMAIL_URL, - credentials, - method: 'PATCH', - payload: { - nicknames, - }, - }) - -export const requirePasswordChange = ({ - screen_names: nicknames, - credentials, -}) => - promisedRequest({ - url: REQUIRE_PASSWORD_CHANGE_URL, - credentials, - method: 'PATCH', - payload: { - nicknames, - }, - }) - -export const disableMFA = ({ screen_name: nickname, credentials }) => - promisedRequest({ - url: DISABLE_MFA_URL, - credentials, - method: 'PUT', - payload: { - nickname, - }, - }) - -export const listStatuses = ({ opts, credentials }) => - promisedRequest({ - url: LIST_STATUSES_URL(opts), - credentials, - method: 'GET', - }) - -export const changeStatusScope = ({ - opts: { id, sensitive, visibility }, - credentials, -}) => { - var payload = {} - if (typeof sensitive !== 'undefined') { - payload['sensitive'] = sensitive - } - if (typeof visibility !== 'undefined') { - payload['visibility'] = visibility - } - - return promisedRequest({ - url: CHANGE_STATUS_SCOPE_URL(id), - credentials, - method: 'PUT', - payload, - }) -} - -export const announcementToPayload = ({ - content, - startsAt, - endsAt, - allDay, -}) => { - const payload = { content } - - if (typeof startsAt !== 'undefined') { - payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null - } - - if (typeof endsAt !== 'undefined') { - payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null - } - - if (typeof allDay !== 'undefined') { - payload.all_day = allDay - } - - return payload -} - -export const postAnnouncement = ({ - credentials, - content, - startsAt, - endsAt, - allDay, -}) => - promisedRequest({ - url: ANNOUNCEMENTS_URL(), - credentials, - method: 'POST', - payload: announcementToPayload({ content, startsAt, endsAt, allDay }), - }) - -export const editAnnouncement = ({ - id, - credentials, - content, - startsAt, - endsAt, - allDay, -}) => - promisedRequest({ - url: ANNOUNCEMENTS_URL(id), - credentials, - method: 'PATCH', - payload: announcementToPayload({ content, startsAt, endsAt, allDay }), - }) - -export const deleteAnnouncement = ({ id, credentials }) => - promisedRequest({ - url: ANNOUNCEMENTS_URL(id), - credentials, - method: 'DELETE', - }) - -export const setReportState = ({ id, state, credentials }) => { - return promisedRequest({ - url: REPORTS, - credentials, - method: 'PATCH', - payload: { - reports: [ - { - id, - state, - }, - ], - }, - }) -} - -export const getInstanceDBConfig = ({ credentials }) => - promisedRequest({ - url: CONFIG_URL, - credentials, - }) - -export const getInstanceConfigDescriptions = ({ credentials }) => - promisedRequest({ - url: DESCRIPTIONS_URL, - credentials, - }) - -export const getAvailableFrontends = ({ credentials }) => - promisedRequest({ - url: FRONTENDS_URL, - credentials, - }) - -export const pushInstanceDBConfig = ({ credentials, payload }) => - promisedRequest({ - url: CONFIG_URL, - method: 'POST', - credentials, - payload, - }) - -export const installFrontend = ({ credentials, payload }) => - promisedRequest({ - url: FRONTENDS_INSTALL_URL, - credentials, - method: 'POST', - payload, - }) - -// Emoji packs -export const deleteEmojiPack = ({ name }) => - promisedRequest({ - url: EMOJI_PACK_URL(name), - method: 'DELETE', - }) - -export const reloadEmoji = ({ credentials }) => - promisedRequest({ - url: EMOJI_RELOAD_URL, - method: 'POST', - credentials, - }) - -export const importEmojiFromFS = ({ credentials }) => - promisedRequest({ - url: EMOJI_IMPORT_FS_URL, - credentials, - }) - -export const createEmojiPack = ({ name, credentials }) => - promisedRequest({ - url: EMOJI_PACK_URL(name), - method: 'POST', - credentials, - }) - -export const listRemoteEmojiPacks = ({ - instance, - page, - pageSize, - credentials, -}) => { - if (!instance.startsWith('http')) { - instance = 'https://' + instance - } - - return promisedRequest({ - url: EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize), - credentials, - }) -} - -export const downloadRemoteEmojiPack = ({ - instance, - packName, - as, - credentials, -}) => - promisedRequest({ - url: EMOJI_PACKS_DL_REMOTE_URL, - credentials, - method: 'POST', - payload: { - url: instance, - name: packName, - as, - }, - }) - -export const downloadRemoteEmojiPackZIP = ({ - url, - packName, - file, - credentials, -}) => { - const data = new FormData() - if (file) data.set('file', file) - if (url) data.set('url', url) - data.set('name', packName) - - return promisedRequest({ - url: EMOJI_PACKS_DL_REMOTE_ZIP_URL, - method: 'POST', - formData: data, - }) -} - -export const saveEmojiPackMetadata = ({ name, newData, credentials }) => - promisedRequest({ - url: EMOJI_PACK_URL(name), - credentials, - method: 'PATCH', - payload: { metadata: newData }, - }) - -export const addNewEmojiFile = ({ packName, file, shortcode, filename }) => { - const data = new FormData() - if (filename.trim() !== '') { - data.set('filename', filename) - } - if (shortcode.trim() !== '') { - data.set('shortcode', shortcode) - } - data.set('file', file) - - return promisedRequest({ - url: EMOJI_UPDATE_FILE_URL(packName), - method: 'POST', - formData: data, - }) -} - -export const updateEmojiFile = ({ - packName, - shortcode, - newShortcode, - newFilename, - credentials, - force, -}) => - promisedRequest({ - url: EMOJI_UPDATE_FILE_URL(packName), - credentials, - method: 'PATCH', - payload: { - shortcode, - new_shortcode: newShortcode, - new_filename: newFilename, - force, - }, - }) - -export const deleteEmojiFile = ({ packName, shortcode }) => - promisedRequest({ - url: `${EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, - method: 'DELETE', - }) diff --git a/src/api/chats.js b/src/api/chats.js deleted file mode 100644 index 114038e52..000000000 --- a/src/api/chats.js +++ /dev/null @@ -1,87 +0,0 @@ -import { paramsString, promisedRequest } from './helpers.js' - -import { parseChat } from 'src/services/entity_normalizer/entity_normalizer.service.js' - -const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats' -const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}` -const PLEROMA_CHAT_MESSAGES_URL = (id, { maxId, sinceId, limit } = {}) => - `/api/v1/pleroma/chats/${id}/messages${paramsString({ maxId, sinceId, limit })}` -const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read` -const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => - `/api/v1/pleroma/chats/${chatId}/messages/${messageId}` - -export const chats = ({ credentials }) => - promisedRequest({ - url: PLEROMA_CHATS_URL, - credentials, - }).then(({ data }) => ({ - chatList: data.map(parseChat).filter((c) => c), - })) - -export const getOrCreateChat = ({ accountId, credentials }) => - promisedRequest({ - url: PLEROMA_CHAT_URL(accountId), - method: 'POST', - credentials, - }) - -export const chatMessages = ({ - id, - credentials, - maxId, - sinceId, - limit = 20, -}) => { - return promisedRequest({ - url: PLEROMA_CHAT_MESSAGES_URL(id, { maxId, sinceId, limit }), - method: 'GET', - credentials, - }) -} - -export const sendChatMessage = ({ - id, - content, - mediaId = null, - idempotencyKey, - credentials, -}) => { - const payload = { - content, - } - - if (mediaId) { - payload.media_id = mediaId - } - - const headers = {} - - if (idempotencyKey) { - headers['idempotency-key'] = idempotencyKey - } - - return promisedRequest({ - url: PLEROMA_CHAT_MESSAGES_URL(id), - method: 'POST', - payload, - credentials, - headers, - }) -} - -export const readChat = ({ id, lastReadId, credentials }) => - promisedRequest({ - url: PLEROMA_CHAT_READ_URL(id), - method: 'POST', - payload: { - last_read_id: lastReadId, - }, - credentials, - }) - -export const deleteChatMessage = ({ chatId, messageId, credentials }) => - promisedRequest({ - url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId), - method: 'DELETE', - credentials, - }) diff --git a/src/api/helpers.js b/src/api/helpers.js deleted file mode 100644 index 4bf16e0cc..000000000 --- a/src/api/helpers.js +++ /dev/null @@ -1,140 +0,0 @@ -import { snakeCase } from 'lodash' - -import { StatusCodeError } from 'src/services/errors/errors' - -export const paramsString = (params = {}) => { - if (params == null || params === undefined) return '' - - if (typeof params !== 'object' || Array.isArray(params)) { - throw new Error('Params are not an object!') - } - - const entries = (() => { - if (params instanceof Map) { - return params.entries() - } else { - return Object.entries(params) - } - })() - - const arrays = [] - const nonArrays = [] - - entries.forEach(([k, v]) => { - if (v == null) return // Drop nulls - if ( - (typeof v === 'object' && !Array.isArray(v)) || - typeof v === 'function' - ) { - throw new Error('Param cannot be non-primitive!') - } - if (Array.isArray(v)) { - arrays.push([k, v]) - } else { - nonArrays.push([k, v]) - } - }) - - arrays.forEach(([k, array]) => { - array.forEach((v) => { - if ( - typeof v === 'object' || - typeof v === 'function' || - typeof v === 'undefined' - ) - throw new Error('Array param cannot contain non-primitives!') - }) - }) - - if (nonArrays.length + arrays.length === 0) return '' - - return ( - '?' + - [ - ...nonArrays.map(([k, v]) => [snakeCase(k), v]), - // turning [a,[1,2,3]] into [[a[],1],[a[],2],[a[],3]] - ...arrays.reduce( - (acc, [k, arrayValue]) => [ - ...acc, - ...arrayValue.map((v) => [snakeCase(k) + '[]', v]), - ], - [], - ), - ] - .map(([k, v]) => `${k}=${window.encodeURIComponent(v)}`) - .join('&') - ) -} - -export const promisedRequest = async ({ - method, - url, - payload, - formData, - cache, - credentials, - headers = {}, -}) => { - const options = { - method, - credentials: 'same-origin', - headers: { - Accept: 'application/json', - ...headers, - }, - } - - if (!formData) { - options.headers['Content-Type'] = 'application/json' - } - - if (cache) { - options.cache = cache - } - - if (formData || payload) { - options.body = formData || JSON.stringify(payload) - } - - if (credentials) { - options.headers = { - ...options.headers, - ...authHeaders(credentials), - } - } - - const response = await fetch(url, options) - const data = await (async () => { - const [contentType] = response.headers - .get('content-type') - .split(';') - .map((x) => x.toLowerCase().trim()) - const contentLength = parseInt(response.headers.get('content-length')) - if (contentLength === 0) return null - - switch (contentType) { - case 'text/plain': - return await response.text() - case 'application/json': - return await response.json() - default: - return await response.bytes() - } - })() - - const { ok, status } = response - - if (ok) { - return { response, status, data } - } else { - throw new StatusCodeError(response.status, data, { url, options }, response) - } -} - -const authHeaders = (accessToken) => { - if (accessToken) { - return { Authorization: `Bearer ${accessToken}` } - } else { - return {} - } -} diff --git a/src/api/mfa.js b/src/api/mfa.js deleted file mode 100644 index 8c1677fbe..000000000 --- a/src/api/mfa.js +++ /dev/null @@ -1,45 +0,0 @@ -import { promisedRequest } from './helpers.js' - -export const verifyOTPCode = ({ - clientId, - clientSecret, - instance, - mfaToken, - code, -}) => { - const formData = new window.FormData() - - formData.append('client_id', clientId) - formData.append('client_secret', clientSecret) - formData.append('mfa_token', mfaToken) - formData.append('code', code) - formData.append('challenge_type', 'totp') - - return promisedRequest({ - url: '/oauth/mfa/challenge', - method: 'POST', - formData, - }) -} - -export const verifyRecoveryCode = ({ - clientId, - clientSecret, - instance, - mfaToken, - code, -}) => { - const formData = new window.FormData() - - formData.append('client_id', clientId) - formData.append('client_secret', clientSecret) - formData.append('mfa_token', mfaToken) - formData.append('code', code) - formData.append('challenge_type', 'recovery') - - return promisedRequest({ - url: `${instance}/oauth/mfa/challenge`, - method: 'POST', - formData, - }) -} diff --git a/src/api/oauth.js b/src/api/oauth.js deleted file mode 100644 index f0fd2950d..000000000 --- a/src/api/oauth.js +++ /dev/null @@ -1,146 +0,0 @@ -import { paramsString, promisedRequest } from './helpers.js' - -const REDIRECT_URI = `${window.location.origin}/oauth-callback` - -export const MASTODON_APP_VERIFY_URL = '/api/v1/apps/verify_credentials' -export const MASTODON_APP_URL = '/api/v1/apps' -export const OAUTH_TOKEN_URL = '/oauth/token' -export const OAUTH_MFA_CHALLENGE_URL = '/oauth/mfa/challenge' -export const OAUTH_REVOKE_URL = '/oauth/revoke' - -export const createApp = () => { - const formData = new window.FormData() - - formData.append('client_name', 'PleromaFE') - formData.append('website', 'https://pleroma.social') - formData.append('redirect_uris', REDIRECT_URI) - formData.append('scopes', 'read write follow push admin') - - return promisedRequest({ - method: 'POST', - url: MASTODON_APP_URL, - formData, - }).then(({ data, ...rest }) => ({ - ...rest, - data: { - ...data, - clientId: data.client_id, - clientSecret: data.client_secret, - }, - })) -} - -export const verifyAppToken = ({ credentials }) => - promisedRequest({ - url: MASTODON_APP_VERIFY_URL, - credentials, - }) - -export const getLoginUrl = ({ instance, clientId }) => { - const data = { - responseType: 'code', - clientId, - redirectUri: REDIRECT_URI, - scope: 'read write follow push admin', - } - - return `${instance}/oauth/authorize${paramsString(data)}` -} - -export const getTokenWithCredentials = ({ - clientId, - clientSecret, - username, - password, -}) => { - const formData = new window.FormData() - - formData.append('client_id', clientId) - formData.append('client_secret', clientSecret) - formData.append('grant_type', 'password') - formData.append('username', username) - formData.append('password', password) - - return promisedRequest({ - url: OAUTH_TOKEN_URL, - method: 'POST', - formData, - }) -} - -export const getToken = ({ clientId, clientSecret, code }) => { - const formData = new window.FormData() - - formData.append('client_id', clientId) - formData.append('client_secret', clientSecret) - formData.append('grant_type', 'authorization_code') - formData.append('code', code) - formData.append('redirect_uri', `${window.location.origin}/oauth-callback`) - - return promisedRequest({ - url: OAUTH_TOKEN_URL, - method: 'POST', - formData, - }) -} - -export const getClientToken = ({ clientId, clientSecret }) => { - const formData = new window.FormData() - - formData.append('client_id', clientId) - formData.append('client_secret', clientSecret) - formData.append('grant_type', 'client_credentials') - formData.append('redirect_uri', `${window.location.origin}/oauth-callback`) - - return promisedRequest({ - url: OAUTH_TOKEN_URL, - method: 'POST', - formData, - }) -} - -export const verifyOTPCode = ({ app, mfaToken, code }) => { - const formData = new window.FormData() - - formData.append('client_id', app.client_id) - formData.append('client_secret', app.client_secret) - formData.append('mfa_token', mfaToken) - formData.append('code', code) - formData.append('challenge_type', 'totp') - - return promisedRequest({ - url: OAUTH_MFA_CHALLENGE_URL, - method: 'POST', - formData, - }) -} - -export const verifyRecoveryCode = ({ app, mfaToken, code }) => { - const formData = new window.FormData() - - formData.append('client_id', app.client_id) - formData.append('client_secret', app.client_secret) - formData.append('mfa_token', mfaToken) - formData.append('code', code) - formData.append('challenge_type', 'recovery') - - return promisedRequest({ - url: OAUTH_MFA_CHALLENGE_URL, - method: 'POST', - formData, - }) -} - -export const revokeToken = ({ app, token }) => { - const formData = new window.FormData() - - formData.append('client_id', app.clientId) - formData.append('client_secret', app.clientSecret) - formData.append('token', token) - - return promisedRequest({ - url: OAUTH_REVOKE_URL, - method: 'POST', - formData, - }) -} diff --git a/src/api/public.js b/src/api/public.js deleted file mode 100644 index e001ba749..000000000 --- a/src/api/public.js +++ /dev/null @@ -1,279 +0,0 @@ -import { paramsString, promisedRequest } from './helpers.js' -import { MASTODON_USER_TIMELINE_URL } from './timelines.js' - -import { - parseSource, - parseStatus, - parseUser, -} from 'src/services/entity_normalizer/entity_normalizer.service.js' - -const MASTODON_SUGGESTIONS_URL = '/api/v1/suggestions' -const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' -const MASTODON_REGISTRATION_URL = '/api/v1/accounts' -const MASTODON_PASSWORD_RESET_URL = ({ email }) => - `/auth/password${paramsString({ email })}` - -const MASTODON_FOLLOWING_URL = ( - id, - { minId, maxId, sinceId, limit, withRelationships }, -) => - `/api/v1/accounts/${id}/following${paramsString({ minId, maxId, sinceId, limit, withRelationships })}` -const MASTODON_FOLLOWERS_URL = ( - id, - { minId, maxId, sinceId, limit, withRelationships }, -) => - `/api/v1/accounts/${id}/followers${paramsString({ minId, maxId, sinceId, limit, withRelationships })}` - -export const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}` -const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context` -const MASTODON_STATUS_SOURCE_URL = (id) => `/api/v1/statuses/${id}/source` -const MASTODON_STATUS_HISTORY_URL = (id) => `/api/v1/statuses/${id}/history` -const MASTODON_USER_URL = '/api/v1/accounts' -const MASTODON_USER_LOOKUP_URL = ({ acct }) => - `/api/v1/accounts/lookup${paramsString({ acct })}` -const MASTODON_POLL_URL = (id = '') => `/api/v1/polls/${id}` -const MASTODON_STATUS_FAVORITEDBY_URL = (id) => - `/api/v1/statuses/${id}/favourited_by` -const MASTODON_STATUS_REBLOGGEDBY_URL = (id) => - `/api/v1/statuses/${id}/reblogged_by` -const MASTODON_SEARCH_2 = ({ - q, - resolve, - limit, - offset, - following, - type, - withRelationships, - accountId, - excludeUnreviewed, -}) => - `/api/v2/search${paramsString({ q, resolve, limit, offset, following, type, withRelationships, accountId, excludeUnreviewed })}` -const MASTODON_USER_SEARCH_URL = ({ q, resolve }) => - `/api/v1/accounts/search${paramsString({ q, resolve })}` -const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' -const PLEROMA_EMOJI_REACTIONS_URL = (id) => - `/api/v1/pleroma/statuses/${id}/reactions` -const PLEROMA_SCROBBLES_URL = (id, { maxId, sinceId, minId, limit, offset }) => - `/api/v1/pleroma/accounts/${id}/scrobbles${paramsString({ maxId, sinceId, minId, limit, offset })}` - -const EMOJI_PACKS_URL = (page, pageSize) => - `/api/v1/pleroma/emoji/packs${paramsString({ page, pageSize })}` - -// Params needed: -// nickname -// email -// fullname -// password -// password_confirm -// -// Optional -// bio -// homepage -// location -// token -// language -export const register = ({ params, credentials }) => { - const { nickname, ...rest } = params - return promisedRequest({ - url: MASTODON_REGISTRATION_URL, - method: 'POST', - credentials, - payload: { - nickname, - locale: 'en_US', - agreement: true, - ...rest, - }, - }) -} - -export const getCaptcha = () => - promisedRequest({ - url: '/api/pleroma/captcha', - }) - -export const fetchUser = ({ id, credentials }) => - promisedRequest({ - url: `${MASTODON_USER_URL}/${id}`, - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) - -export const fetchUserByName = ({ name, credentials }) => - promisedRequest({ - url: MASTODON_USER_LOOKUP_URL({ acct: name }), - credentials, - }) - .then(({ data }) => data.id) - .catch((error) => { - if (error && error.statusCode === 404) { - // Either the backend does not support lookup endpoint, - // or there is no user with such name. Fallback and treat name as id. - return name - } else { - throw error - } - }) - .then((id) => fetchUser({ id, credentials })) - -export const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) => - promisedRequest({ - url: MASTODON_FOLLOWING_URL(id, { maxId, sinceId, limit }), - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) - -export const fetchFollowers = ({ - id, - maxId, - sinceId, - limit = 20, - credentials, -}) => - promisedRequest({ - url: MASTODON_FOLLOWERS_URL(id, { - maxId, - sinceId, - limit, - withRelationships: true, - }), - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) - -export const fetchConversation = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_STATUS_CONTEXT_URL(id), - credentials, - }).then((result) => ({ - ...result, - data: { - ...result.data, - ancestors: result.data.ancestors.map(parseStatus), - descendants: result.data.descendants.map(parseStatus), - }, - })) - -export const fetchStatus = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_STATUS_URL(id), - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const fetchStatusSource = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_STATUS_SOURCE_URL(id), - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseSource(data) })) - -export const fetchStatusHistory = ({ status, credentials }) => - promisedRequest({ - url: MASTODON_STATUS_HISTORY_URL(status.id), - credentials, - }).then(({ data, ...rest }) => { - return [...data].reverse().map((item) => { - item.originalStatus = status - return { ...rest, data: parseStatus(item) } - }) - }) - -export const listEmojiPacks = ({ page, pageSize, credentials }) => - promisedRequest({ - url: EMOJI_PACKS_URL(page, pageSize), - }) - -export const fetchPinnedStatuses = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_USER_TIMELINE_URL(id, { pinned: true }), - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseStatus) })) - -export const verifyCredentials = ({ credentials }) => - promisedRequest({ - url: MASTODON_LOGIN_URL, - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) - -export const resetPassword = ({ email }) => { - return promisedRequest({ - url: MASTODON_PASSWORD_RESET_URL({ email }), - method: 'POST', - }) -} - -export const suggestions = ({ credentials }) => - promisedRequest({ - url: MASTODON_SUGGESTIONS_URL, - credentials, - }) - -export const fetchPoll = ({ pollId, credentials }) => - promisedRequest({ - url: MASTODON_POLL_URL(encodeURIComponent(pollId)), - method: 'GET', - credentials, - }) - -export const fetchFavoritedByUsers = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_STATUS_FAVORITEDBY_URL(id), - method: 'GET', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) - -export const fetchRebloggedByUsers = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_STATUS_REBLOGGEDBY_URL(id), - method: 'GET', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) - -export const fetchEmojiReactions = ({ id, credentials }) => - promisedRequest({ - url: PLEROMA_EMOJI_REACTIONS_URL(id), - credentials, - }).then(({ data, ...rest }) => ({ - ...rest, - data: data.map((r) => { - r.accounts = r.accounts.map(parseUser) - return r - }), - })) - -export const searchUsers = ({ credentials, query }) => - promisedRequest({ - url: MASTODON_USER_SEARCH_URL({ q: query, resolve: true }), - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) - -export const search2 = ({ - credentials, - q, - resolve, - limit, - offset, - following, - type, -}) => { - return promisedRequest({ - url: MASTODON_SEARCH_2({ - q, - resolve, - limit, - offset, - following, - type, - withRelationships: true, - }), - credentials, - }).then(({ data, ...rest }) => { - data.accounts = data.accounts.slice(0, limit).map((u) => parseUser(u)) - data.statuses = data.statuses.slice(0, limit).map((s) => parseStatus(s)) - return { ...rest, data } - }) -} - -export const fetchKnownDomains = ({ credentials }) => - promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) - -export const fetchScrobbles = ({ accountId, limit = 1 }) => - promisedRequest({ - url: PLEROMA_SCROBBLES_URL(accountId, { limit }), - }) diff --git a/src/api/timelines.js b/src/api/timelines.js deleted file mode 100644 index 0679b1eec..000000000 --- a/src/api/timelines.js +++ /dev/null @@ -1,198 +0,0 @@ -import { paramsString, promisedRequest } from './helpers.js' - -import { - parseLinkHeaderPagination, - parseNotification, - parseStatus, -} from 'src/services/entity_normalizer/entity_normalizer.service.js' - -const MASTODON_USER_HOME_TIMELINE_URL = ({ - minId, - sinceId, - maxId, - limit, - replyVisibility, -}) => - `/api/v1/timelines/home${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` -const MASTODON_LIST_TIMELINE_URL = ( - id, - { minId, sinceId, maxId, limit, replyVisibility }, -) => - `/api/v1/timelines/list/${id}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` -const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = ({ - minId, - sinceId, - maxId, - limit, - replyVisibility, -}) => - `/api/v1/timelines/direct${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` -const MASTODON_PUBLIC_TIMELINE = ({ - minId, - sinceId, - maxId, - limit, - replyVisibility, - local, - remote, - onlyMedia, -}) => - `/api/v1/timelines/public${paramsString({ minId, sinceId, maxId, limit, replyVisibility, local, remote, onlyMedia })}` -const MASTODON_TAG_TIMELINE_URL = ( - tag, - { minId, sinceId, maxId, limit, replyVisibility }, -) => - `/api/v1/timelines/tag/${tag}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` -export const MASTODON_USER_TIMELINE_URL = ( - id, - { minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia }, -) => - `/api/v1/accounts/${id}/statuses${paramsString({ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia })}` -const MASTODON_USER_FAVORITES_TIMELINE_URL = ({ - minId, - sinceId, - maxId, - limit, - replyVisibility, -}) => - `/api/v1/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` -const MASTODON_BOOKMARK_TIMELINE_URL = ({ - minId, - sinceId, - maxId, - limit, - replyVisibility, - folderId, -}) => - `/api/v1/bookmarks${paramsString({ minId, sinceId, maxId, limit, replyVisibility, folderId })}` -const PLEROMA_STATUS_QUOTES_URL = ( - id, - { minId, sinceId, maxId, limit, replyVisibility }, -) => - `/api/v1/pleroma/statuses/${id}/quotes${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` -const PLEROMA_USER_FAVORITES_TIMELINE_URL = ( - id, - { minId, sinceId, maxId, limit, replyVisibility }, -) => - `/api/v1/pleroma/accounts/${id}/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` -const AKKOMA_BUBBLE_TIMELINE_URL = ({ - minId, - sinceId, - maxId, - limit, - replyVisibility, -}) => - `/api/v1/timelines/bubble${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}` - -const MASTODON_USER_NOTIFICATIONS_URL = ({ - minId, - sinceId, - maxId, - limit, - includeTypes, - replyVisibility, -}) => - `/api/v1/notifications${paramsString({ minId, sinceId, maxId, limit, includeTypes, replyVisibility })}` - -export const fetchTimeline = ({ - timeline, - credentials, - sinceId, - minId, - maxId, - userId, - listId, - statusId, - tag, - withMuted, - replyVisibility = 'all', - includeTypes = [], - bookmarkFolderId, -}) => { - const timelineUrls = { - friends: MASTODON_USER_HOME_TIMELINE_URL, - public: MASTODON_PUBLIC_TIMELINE, - publicAndExternal: MASTODON_PUBLIC_TIMELINE, - dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, - user: MASTODON_USER_TIMELINE_URL, - media: MASTODON_USER_TIMELINE_URL, - list: MASTODON_LIST_TIMELINE_URL, - favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, - publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL, - bookmarks: MASTODON_BOOKMARK_TIMELINE_URL, - bubble: AKKOMA_BUBBLE_TIMELINE_URL, - tag: MASTODON_TAG_TIMELINE_URL, - quotes: PLEROMA_STATUS_QUOTES_URL, - - notifications: MASTODON_USER_NOTIFICATIONS_URL, - } - const urlFunc = timelineUrls[timeline] - - const twoArgs = new Set([ - 'user', - 'media', - 'list', - 'publicFavorites', - 'tag', - 'quotes', - ]) - - const params = { - minId, - sinceId, - maxId, - limit: 20, - } - - const id = (() => { - switch (timeline) { - case 'user': - case 'media': - return userId - case 'list': - return listId - case 'quotes': - return statusId - case 'tag': - return tag - } - })() - - const isNotifications = timeline === 'notifications' - - if (timeline === 'media') { - params.onlyMedia = true - } - if (timeline === 'public') { - params.local = true - } - if (timeline !== 'favorites' && timeline !== 'bookmarks') { - params.withMuted = withMuted - } - if (replyVisibility !== 'all') { - params.replyVisibility = replyVisibility - } - if (timeline === 'bookmarks' && bookmarkFolderId) { - params.folderId = bookmarkFolderId - } - - if (isNotifications && includeTypes.length > 0) { - params.includeTypes = includeTypes - } - - const url = twoArgs.has(timeline) ? urlFunc(id, params) : urlFunc(params) - return promisedRequest({ url, credentials }).then((result) => { - const pagination = parseLinkHeaderPagination( - result.response.headers.get('Link'), - { - flakeId: timeline !== 'bookmarks' && timeline !== 'notifications', - }, - ) - - return { - ...result, - data: result.data.map(isNotifications ? parseNotification : parseStatus), - pagination, - } - }) -} diff --git a/src/api/user.js b/src/api/user.js deleted file mode 100644 index 17e13195d..000000000 --- a/src/api/user.js +++ /dev/null @@ -1,921 +0,0 @@ -import { concat, last } from 'lodash' - -import { paramsString, promisedRequest } from './helpers.js' -import { fetchFriends, MASTODON_STATUS_URL } from './public.js' - -import { - parseAttachment, - parseStatus, - parseUser, -} from 'src/services/entity_normalizer/entity_normalizer.service.js' - -const MUTES_IMPORT_URL = '/api/pleroma/mutes_import' -const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' -const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' -const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' -const CHANGE_EMAIL_URL = '/api/pleroma/change_email' -const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' -const MOVE_ACCOUNT_URL = '/api/pleroma/move_account' -const ALIASES_URL = '/api/pleroma/aliases' -const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' -const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' - -const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' -const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' - -const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp' -const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp' -const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp' - -const MASTODON_DISMISS_NOTIFICATION_URL = (id) => - `/api/v1/notifications/${id}/dismiss` -const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite` -const MASTODON_UNFAVORITE_URL = (id) => `/api/v1/statuses/${id}/unfavourite` -const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog` -const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog` -const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}` -const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow` -const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow` - -const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests' -const MASTODON_APPROVE_USER_URL = (id) => - `/api/v1/follow_requests/${id}/authorize` -const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject` -const MASTODON_USER_RELATIONSHIPS_URL = ({ id, withSuspended }) => - `/api/v1/accounts/relationships/${paramsString({ id, withSuspended })}` -const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists` -export const MASTODON_LIST_URL = (id = '') => `/api/v1/lists/${id}` -export const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts` -const MASTODON_USER_BLOCKS_URL = ({ - maxId, - sinceId, - limit, - withRelationships, -}) => - `/api/v1/blocks/${paramsString({ maxId, sinceId, limit, withRelationships })}` -const MASTODON_USER_MUTES_URL = ({ - maxId, - sinceId, - limit, - withRelationships, -}) => - `/api/v1/mutes/${paramsString({ maxId, sinceId, limit, withRelationships })}` -const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block` -const MASTODON_UNBLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/unblock` -const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute` -const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute` -const MASTODON_REMOVE_USER_FROM_FOLLOWERS = (id) => - `/api/v1/accounts/${id}/remove_from_followers` -const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note` -const MASTODON_BOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark` -const MASTODON_UNBOOKMARK_STATUS_URL = (id) => - `/api/v1/statuses/${id}/unbookmark` -const MASTODON_POST_STATUS_URL = '/api/v1/statuses' -const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' -const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes` -const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' -const MASTODON_REPORT_USER_URL = '/api/v1/reports' -const MASTODON_PIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/pin` -const MASTODON_UNPIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/unpin` -const MASTODON_MUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/mute` -const MASTODON_UNMUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/unmute` -const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' -const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements' -const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) => - `/api/v1/announcements/${id}/dismiss` -const PLEROMA_EMOJI_REACT_URL = (id, emoji) => - `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` -const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => - `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` -const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups' -const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders' -const PLEROMA_BOOKMARK_FOLDER_URL = (id) => - `/api/v1/pleroma/bookmark_folders/${id}` - -// #Posts -export const favorite = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_FAVORITE_URL(id), - method: 'POST', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const unfavorite = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_UNFAVORITE_URL(id), - method: 'POST', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const retweet = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_RETWEET_URL(id), - method: 'POST', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const unretweet = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_UNRETWEET_URL(id), - method: 'POST', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const reactWithEmoji = ({ id, emoji, credentials }) => - promisedRequest({ - url: PLEROMA_EMOJI_REACT_URL(id, emoji), - method: 'PUT', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const unreactWithEmoji = ({ id, emoji, credentials }) => - promisedRequest({ - url: PLEROMA_EMOJI_UNREACT_URL(id, emoji), - method: 'DELETE', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const bookmarkStatus = ({ id, credentials, ...options }) => - promisedRequest({ - url: MASTODON_BOOKMARK_STATUS_URL(id), - credentials, - method: 'POST', - payload: { - folder_id: options.folder_id, - }, - }) - -export const unbookmarkStatus = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_UNBOOKMARK_STATUS_URL(id), - credentials, - method: 'POST', - }) - -export const pinOwnStatus = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_PIN_OWN_STATUS(id), - credentials, - method: 'POST', - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const unpinOwnStatus = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_UNPIN_OWN_STATUS(id), - credentials, - method: 'POST', - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const muteConversation = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_MUTE_CONVERSATION(id), - credentials, - method: 'POST', - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const unmuteConversation = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_UNMUTE_CONVERSATION(id), - credentials, - method: 'POST', - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) - -export const vote = ({ pollId, choices, credentials }) => { - return promisedRequest({ - url: MASTODON_VOTE_URL(encodeURIComponent(pollId)), - method: 'POST', - credentials, - payload: { - choices, - }, - }) -} - -// #Posting -export const postStatus = ({ - credentials, - status, - spoilerText, - visibility, - sensitive, - poll, - mediaIds = [], - inReplyToStatusId, - quoteId, - contentType, - preview, - idempotencyKey, -}) => { - const form = new FormData() - const pollOptions = poll.options || [] - - form.append('status', status) - form.append('source', 'Pleroma FE') - if (spoilerText) form.append('spoiler_text', spoilerText) - if (visibility) form.append('visibility', visibility) - if (sensitive) form.append('sensitive', sensitive) - if (contentType) form.append('content_type', contentType) - mediaIds.forEach((val) => { - form.append('media_ids[]', val) - }) - if (pollOptions.some((option) => option !== '')) { - const normalizedPoll = { - expires_in: parseInt(poll.expiresIn, 10), - multiple: poll.multiple, - } - Object.keys(normalizedPoll).forEach((key) => { - form.append(`poll[${key}]`, normalizedPoll[key]) - }) - - pollOptions.forEach((option) => { - form.append('poll[options][]', option) - }) - } - if (inReplyToStatusId) { - form.append('in_reply_to_id', inReplyToStatusId) - } - if (quoteId) { - form.append('quote_id', quoteId) - } - if (preview) { - form.append('preview', 'true') - } - - const headers = {} - if (idempotencyKey) { - headers['idempotency-key'] = idempotencyKey - } - - return promisedRequest({ - url: MASTODON_POST_STATUS_URL, - formData: form, - method: 'POST', - credentials, - headers, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) -} - -export const editStatus = ({ - id, - credentials, - status, - spoilerText, - sensitive, - poll, - mediaIds = [], - contentType, -}) => { - const form = new FormData() - const pollOptions = poll.options || [] - - form.append('status', status) - if (spoilerText) form.append('spoiler_text', spoilerText) - if (sensitive) form.append('sensitive', sensitive) - if (contentType) form.append('content_type', contentType) - mediaIds.forEach((val) => { - form.append('media_ids[]', val) - }) - - if (pollOptions.some((option) => option !== '')) { - const normalizedPoll = { - expires_in: parseInt(poll.expiresIn, 10), - multiple: poll.multiple, - } - Object.keys(normalizedPoll).forEach((key) => { - form.append(`poll[${key}]`, normalizedPoll[key]) - }) - - pollOptions.forEach((option) => { - form.append('poll[options][]', option) - }) - } - - return promisedRequest({ - url: MASTODON_STATUS_URL(id), - formData: form, - method: 'PUT', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) -} - -export const deleteStatus = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_DELETE_URL(id), - credentials, - method: 'DELETE', - }) - -export const uploadMedia = ({ formData, credentials }) => - promisedRequest({ - url: MASTODON_MEDIA_UPLOAD_URL, - formData, - method: 'POST', - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) })) - -export const setMediaDescription = ({ id, description, credentials }) => - promisedRequest({ - url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`, - method: 'PUT', - credentials, - payload: { - description, - }, - }).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) })) - -// #Notifications -export const dismissNotification = ({ credentials, id }) => - promisedRequest({ - url: MASTODON_DISMISS_NOTIFICATION_URL(id), - method: 'POST', - payload: { id }, - credentials, - }) - -export const markNotificationsAsSeen = ({ - id, - credentials, - single = false, -}) => { - const formData = new FormData() - - if (single) { - formData.append('id', id) - } else { - formData.append('max_id', id) - } - - return promisedRequest({ - url: NOTIFICATION_READ_URL, - formData, - credentials, - method: 'POST', - }) -} - -// #Announcements -export const getAnnouncements = ({ credentials }) => - promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials }) - -export const dismissAnnouncement = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id), - credentials, - method: 'POST', - }) - -// #Imports -export const importMutes = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return promisedRequest({ - url: MUTES_IMPORT_URL, - formData, - method: 'POST', - credentials, - }).then((response) => response.ok) -} - -export const importBlocks = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return promisedRequest({ - url: BLOCKS_IMPORT_URL, - formData, - method: 'POST', - credentials, - }).then((response) => response.ok) -} - -export const importFollows = ({ file, credentials }) => { - const formData = new FormData() - formData.append('list', file) - return promisedRequest({ - url: FOLLOW_IMPORT_URL, - formData, - method: 'POST', - credentials, - }).then((response) => response.ok) -} - -export const exportFriends = ({ id, credentials }) => { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO refactor this - return new Promise(async (resolve, reject) => { - try { - let friends = [] - let more = true - while (more) { - const maxId = friends.length > 0 ? last(friends).id : undefined - const users = await fetchFriends({ - id, - maxId, - credentials, - withRelationships: true, - }) - friends = concat(friends, users) - if (users.length === 0) { - more = false - } - } - resolve(friends) - } catch (err) { - reject(err) - } - }) -} - -// #Profile settings -export const updateNotificationSettings = ({ credentials, settings }) => { - return promisedRequest({ - url: NOTIFICATION_SETTINGS_URL, - credentials, - method: 'PUT', - payload: settings, - }) -} - -export const updateProfileImages = ({ - credentials, - avatar = null, - avatarName = null, - banner = null, - background = null, -}) => { - const form = new FormData() - if (avatar !== null) { - if (avatarName !== null) { - form.append('avatar', avatar, avatarName) - } else { - form.append('avatar', avatar) - } - } - if (banner !== null) form.append('header', banner) - if (background !== null) form.append('pleroma_background_image', background) - return promisedRequest({ - url: MASTODON_PROFILE_UPDATE_URL, - credentials, - method: 'PATCH', - formData: form, - }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) -} - -export const updateProfile = ({ credentials, params }) => { - const formData = new FormData() - - for (const name in params) { - if (name === 'fields_attributes') { - params[name].forEach((param, i) => { - formData.append(name + `[${i}][name]`, param.name) - formData.append(name + `[${i}][value]`, param.value) - }) - } else { - if (typeof params[name] === 'object') { - console.warn( - 'Object detected in updateProfile API call. This will not work, use updateProfileJSON instead.', - ) - console.warn('Object:\n' + JSON.stringify(params[name], null, 2)) - } - formData.append(name, params[name]) - } - } - - return promisedRequest({ - url: MASTODON_PROFILE_UPDATE_URL, - credentials, - method: 'PATCH', - formData, - }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) -} - -export const updateProfileJSON = ({ credentials, params }) => - promisedRequest({ - url: MASTODON_PROFILE_UPDATE_URL, - credentials, - payload: params, - method: 'PATCH', - }).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) })) - -export const changeEmail = ({ credentials, email, password }) => { - const form = new FormData() - - form.append('email', email) - form.append('password', password) - - return promisedRequest({ - url: CHANGE_EMAIL_URL, - formData: form, - method: 'POST', - credentials, - }) -} - -export const moveAccount = ({ credentials, password, targetAccount }) => { - const form = new FormData() - - form.append('password', password) - form.append('target_account', targetAccount) - - return promisedRequest({ - url: MOVE_ACCOUNT_URL, - formData: form, - method: 'POST', - credentials, - }) -} - -export const changePassword = ({ - credentials, - password, - newPassword, - newPasswordConfirmation, -}) => { - const form = new FormData() - - form.append('password', password) - form.append('new_password', newPassword) - form.append('new_password_confirmation', newPasswordConfirmation) - - return promisedRequest({ - url: CHANGE_PASSWORD_URL, - formData: form, - method: 'POST', - credentials, - }) -} - -// #MFA -export const settingsMFA = ({ credentials }) => - promisedRequest({ - url: MFA_SETTINGS_URL, - credentials, - method: 'GET', - }) - -export const mfaDisableOTP = ({ credentials, password }) => { - const form = new FormData() - - form.append('password', password) - - return promisedRequest({ - url: MFA_DISABLE_OTP_URL, - formData: form, - method: 'DELETE', - credentials, - }) -} - -export const mfaConfirmOTP = ({ credentials, password, token }) => { - const form = new FormData() - - form.append('password', password) - form.append('code', token) - - return promisedRequest({ - url: MFA_CONFIRM_OTP_URL, - formData: form, - credentials, - method: 'POST', - }) -} -export const mfaSetupOTP = ({ credentials }) => - promisedRequest({ - url: MFA_SETUP_OTP_URL, - credentials, - method: 'GET', - }) -export const generateMfaBackupCodes = ({ credentials }) => - promisedRequest({ - url: MFA_BACKUP_CODES_URL, - credentials, - method: 'GET', - }) - -// #Aliases -export const addAlias = ({ credentials, alias }) => - promisedRequest({ - url: ALIASES_URL, - method: 'PUT', - credentials, - payload: { alias }, - }) - -export const deleteAlias = ({ credentials, alias }) => - promisedRequest({ - url: ALIASES_URL, - method: 'DELETE', - credentials, - payload: { alias }, - }) - -export const listAliases = ({ credentials }) => - promisedRequest({ - url: ALIASES_URL, - method: 'GET', - credentials, - params: { - _cacheBooster: new Date().getTime(), - }, - }) - -// User manipulation -export const fetchUserRelationship = ({ id, withSuspended, credentials }) => - promisedRequest({ - url: MASTODON_USER_RELATIONSHIPS_URL({ id, withSuspended }), - credentials, - }) - -export const followUser = ({ id, credentials, ...options }) => { - const payload = {} - - if (options.reblogs !== undefined) { - payload.reblogs = options.reblogs - } - - if (options.notify !== undefined) { - payload.notify = options.notify - } - - return promisedRequest({ - url: MASTODON_FOLLOW_URL(id), - payload, - credentials, - method: 'POST', - }) -} - -export const unfollowUser = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_UNFOLLOW_URL(id), - credentials, - method: 'POST', - }) -export const fetchUserInLists = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_USER_IN_LISTS(id), - credentials, - }) - -export const removeUserFromFollowers = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), - credentials, - method: 'POST', - }) - -export const fetchFollowRequests = ({ credentials }) => - promisedRequest({ - url: MASTODON_FOLLOW_REQUESTS_URL, - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) - -export const approveUser = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_APPROVE_USER_URL(id), - credentials, - method: 'POST', - }) - -export const denyUser = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_DENY_USER_URL(id), - credentials, - method: 'POST', - }) - -export const editUserNote = ({ id, credentials, comment }) => - promisedRequest({ - url: MASTODON_USER_NOTE_URL(id), - credentials, - payload: { - comment, - }, - method: 'POST', - }) - -export const fetchMutes = ({ maxId, credentials }) => - promisedRequest({ - url: MASTODON_USER_MUTES_URL({ maxId, withRelationships: true }), - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) - -export const muteUser = ({ id, expiresIn, credentials }) => { - const payload = {} - if (expiresIn) { - payload.expires_in = expiresIn - } - - return promisedRequest({ - url: MASTODON_MUTE_USER_URL(id), - credentials, - method: 'POST', - payload, - }) -} - -export const unmuteUser = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_UNMUTE_USER_URL(id), - credentials, - method: 'POST', - }) - -export const fetchBlocks = ({ maxId, credentials }) => - promisedRequest({ - url: MASTODON_USER_BLOCKS_URL({ maxId, withRelationships: true }), - credentials, - }).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) })) - -export const blockUser = ({ id, expiresIn, credentials }) => { - const payload = {} - if (expiresIn) { - payload.duration = expiresIn - } - - return promisedRequest({ - url: MASTODON_BLOCK_USER_URL(id), - credentials, - method: 'POST', - payload, - }) -} - -export const unblockUser = ({ id, credentials }) => - promisedRequest({ - url: MASTODON_UNBLOCK_USER_URL(id), - credentials, - method: 'POST', - }) - -export const reportUser = ({ - credentials, - userId, - statusIds, - comment, - forward, -}) => - promisedRequest({ - url: MASTODON_REPORT_USER_URL, - method: 'POST', - payload: { - account_id: userId, - status_ids: statusIds, - comment, - forward, - }, - credentials, - }) - -// #Domain mutes -export const fetchDomainMutes = ({ credentials }) => - promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials }) - -export const muteDomain = ({ domain, credentials }) => - promisedRequest({ - url: MASTODON_DOMAIN_BLOCKS_URL, - method: 'POST', - payload: { domain }, - credentials, - }) - -export const unmuteDomain = ({ domain, credentials }) => - promisedRequest({ - url: MASTODON_DOMAIN_BLOCKS_URL, - method: 'DELETE', - payload: { domain }, - credentials, - }) - -// #Backups -export const addBackup = ({ credentials }) => - promisedRequest({ - url: PLEROMA_BACKUP_URL, - method: 'POST', - credentials, - }) - -export const listBackups = ({ credentials }) => - promisedRequest({ - url: PLEROMA_BACKUP_URL, - method: 'GET', - credentials, - params: { - _cacheBooster: new Date().getTime(), - }, - }) - -// #OAuth -export const fetchOAuthTokens = ({ credentials }) => - promisedRequest({ - url: '/api/oauth_tokens.json', - credentials, - }) - -export const revokeOAuthToken = ({ id, credentials }) => - promisedRequest({ - url: `/api/oauth_tokens/${id}`, - credentials, - method: 'DELETE', - }) - -// #Lists -export const fetchLists = ({ credentials }) => - promisedRequest({ - url: MASTODON_LIST_URL(), - credentials, - }) - -export const createList = ({ title, credentials }) => - promisedRequest({ - url: MASTODON_LIST_URL(), - credentials, - method: 'POST', - payload: { title }, - }) - -export const getList = ({ listId, credentials }) => - promisedRequest({ - url: MASTODON_LIST_URL(listId), - credentials, - }) - -export const updateList = ({ listId, title, credentials }) => - promisedRequest({ - url: MASTODON_LIST_URL(listId), - - credentials, - method: 'PUT', - payload: { title }, - }) - -export const getListAccounts = ({ listId, credentials }) => - promisedRequest({ - url: MASTODON_LIST_ACCOUNTS_URL(listId), - credentials, - }).then((data) => data.map(({ id }) => id)) - -export const addAccountsToList = ({ listId, accountIds, credentials }) => - promisedRequest({ - url: MASTODON_LIST_ACCOUNTS_URL(listId), - credentials, - method: 'POST', - payload: { account_ids: accountIds }, - }) - -export const removeAccountsFromList = ({ listId, accountIds, credentials }) => - promisedRequest({ - url: MASTODON_LIST_ACCOUNTS_URL(listId), - credentials, - method: 'DELETE', - payload: { account_ids: accountIds }, - }) - -export const deleteList = ({ listId, credentials }) => - promisedRequest({ - url: MASTODON_LIST_URL(listId), - method: 'DELETE', - credentials, - }) - -// #Bookmarks -export const fetchBookmarkFolders = ({ credentials }) => - promisedRequest({ - url: PLEROMA_BOOKMARK_FOLDERS_URL, - credentials, - }) - -export const createBookmarkFolder = ({ name, emoji, credentials }) => - promisedRequest({ - url: PLEROMA_BOOKMARK_FOLDERS_URL, - credentials, - method: 'POST', - payload: { name, emoji }, - }) - -export const updateBookmarkFolder = ({ folderId, name, emoji, credentials }) => - promisedRequest({ - url: PLEROMA_BOOKMARK_FOLDER_URL(folderId), - credentials, - method: 'PATCH', - payload: { name, emoji }, - }) - -export const deleteBookmarkFolder = ({ folderId, credentials }) => - promisedRequest({ - url: PLEROMA_BOOKMARK_FOLDER_URL(folderId), - method: 'DELETE', - credentials, - }) - -// #So long and thanks for all the fish -export const deleteAccount = ({ credentials, password }) => { - const formData = new FormData() - - formData.append('password', password) - - return promisedRequest({ - url: DELETE_ACCOUNT_URL, - formData, - method: 'POST', - credentials, - }) -} diff --git a/src/api/websocket.js b/src/api/websocket.js deleted file mode 100644 index d952372e5..000000000 --- a/src/api/websocket.js +++ /dev/null @@ -1,176 +0,0 @@ -import { paramsString } from './helpers.js' - -import { - parseChat, - parseNotification, - parseStatus, -} from 'src/services/entity_normalizer/entity_normalizer.service.js' - -const MASTODON_STREAMING = ({ accessToken, stream }) => - `/api/v1/streaming${paramsString({ accessToken, stream })}` - -export const getMastodonSocketURI = ({ credentials, stream }) => { - return MASTODON_STREAMING({ accessToken: credentials, stream }) -} - -const MASTODON_STREAMING_EVENTS = new Set([ - 'update', - 'notification', - 'delete', - 'filters_changed', - 'status.update', -]) - -const PLEROMA_STREAMING_EVENTS = new Set([ - 'pleroma:chat_update', - 'pleroma:respond', -]) - -// A thin wrapper around WebSocket API that allows adding a pre-processor to it -// Uses EventTarget and a CustomEvent to proxy events -export const ProcessedWS = ({ - url, - preprocessor = handleMastoWS, - id = 'Unknown', - credentials, -}) => { - const eventTarget = new EventTarget() - const socket = new WebSocket(url) - if (!socket) throw new Error(`Failed to create socket ${id}`) - const proxy = (original, eventName, processor = (a) => a) => { - original.addEventListener(eventName, (eventData) => { - eventTarget.dispatchEvent( - new CustomEvent(eventName, { detail: processor(eventData) }), - ) - }) - } - socket.addEventListener('open', (wsEvent) => { - console.debug(`[WS][${id}] Socket connected`, wsEvent) - if (credentials) { - socket.send( - JSON.stringify({ - type: 'pleroma:authenticate', - token: credentials, - }), - ) - } - }) - socket.addEventListener('error', (wsEvent) => { - console.debug(`[WS][${id}] Socket errored`, wsEvent) - }) - socket.addEventListener('close', (wsEvent) => { - console.debug( - `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, - wsEvent, - ) - }) - // Commented code reason: very spammy, uncomment to enable message debug logging - /* - socket.addEventListener('message', (wsEvent) => { - console.debug( - `[WS][${id}] Message received`, - wsEvent - ) - }) - /**/ - - const onAuthenticated = () => { - eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated')) - } - - proxy(socket, 'open') - proxy(socket, 'close') - proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated })) - proxy(socket, 'error') - - // 1000 = Normal Closure - eventTarget.close = () => { - socket.close(1000, 'Shutting down socket') - } - eventTarget.getState = () => socket.readyState - eventTarget.subscribe = (stream, args = {}) => { - console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args) - socket.send( - JSON.stringify({ - type: 'subscribe', - stream, - ...args, - }), - ) - } - eventTarget.unsubscribe = (stream, args = {}) => { - console.debug( - `[WS][${id}] Unsubscribing from stream ${stream} with args`, - args, - ) - socket.send( - JSON.stringify({ - type: 'unsubscribe', - stream, - ...args, - }), - ) - } - - return eventTarget -} - -export const handleMastoWS = ( - wsEvent, - { - onAuthenticated = () => { - /* no-op */ - }, - } = {}, -) => { - const { data } = wsEvent - if (!data) return - const parsedEvent = JSON.parse(data) - const { event, payload } = parsedEvent - if ( - MASTODON_STREAMING_EVENTS.has(event) || - PLEROMA_STREAMING_EVENTS.has(event) - ) { - // MastoBE and PleromaBE both send payload for delete as a PLAIN string - if (event === 'delete') { - return { event, id: payload } - } - const data = payload ? JSON.parse(payload) : null - if (event === 'pleroma:respond') { - if (data.type === 'pleroma:authenticate') { - if (data.result === 'success') { - console.debug('[WS] Successfully authenticated') - onAuthenticated() - } else { - if (data.error === 'already_authenticated') { - onAuthenticated() - } else { - console.error('[WS] Unable to authenticate:', data.error) - wsEvent.target.close() - } - } - } - return null - } else if (event === 'update') { - return { event, status: parseStatus(data) } - } else if (event === 'status.update') { - return { event, status: parseStatus(data) } - } else if (event === 'notification') { - return { event, notification: parseNotification(data) } - } else if (event === 'pleroma:chat_update') { - return { event, chatUpdate: parseChat(data) } - } - } else { - console.warn('Unknown event', wsEvent) - return null - } -} - -export const WSConnectionStatus = Object.freeze({ - JOINED: 1, - CLOSED: 2, - ERROR: 3, - DISABLED: 4, - STARTING: 5, - STARTING_INITIAL: 6, -}) diff --git a/public/static/pleromatan_apology.png b/src/assets/pleromatan_apology.png similarity index 100% rename from public/static/pleromatan_apology.png rename to src/assets/pleromatan_apology.png diff --git a/public/static/pleromatan_apology_fox.png b/src/assets/pleromatan_apology_fox.png similarity index 100% rename from public/static/pleromatan_apology_fox.png rename to src/assets/pleromatan_apology_fox.png diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 4ccfa4d41..6cad05f62 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -1,51 +1,21 @@ -/* global process */ - -import vClickOutside from 'click-outside-vue3' import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' +import vClickOutside from 'click-outside-vue3' import VueVirtualScroller from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' -import RichContent from 'src/components/rich_content/rich_content.jsx' -import Status from 'src/components/status/status.vue' -import StillImage from 'src/components/still-image/still-image.vue' - -import { config } from '@fortawesome/fontawesome-svg-core' -import { - FontAwesomeIcon, - FontAwesomeLayers, -} from '@fortawesome/vue-fontawesome' - -config.autoAddCss = false +import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome' import App from '../App.vue' -import FaviconService from '../services/favicon_service/favicon_service.js' -import { applyStyleConfig } from '../services/style_setter/style_setter.js' -import { initServiceWorker, updateFocus } from '../services/sw/sw.js' -import { - windowHeight, - windowWidth, -} from '../services/window_utils/window_utils' import routes from './routes' - -import { useAuthFlowStore } from 'src/stores/auth_flow' -import { useEmojiStore } from 'src/stores/emoji.js' -import { useI18nStore } from 'src/stores/i18n' -import { useInstanceStore } from 'src/stores/instance.js' -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' -import { useInterfaceStore } from 'src/stores/interface.js' -import { useLocalConfigStore } from 'src/stores/local_config.js' -import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useOAuthStore } from 'src/stores/oauth.js' -import { useSyncConfigStore } from 'src/stores/sync_config.js' -import { useUserHighlightStore } from 'src/stores/user_highlight.js' - import VBodyScrollLock from 'src/directives/body_scroll_lock' -import { - INSTANCE_DEFAULT_CONFIG_DEFINITIONS, - INSTANCE_IDENTITY_DEFAULT_DEFINITIONS, - INSTANCE_IDENTIY_EXTERNAL, -} from 'src/modules/default_config_state.js' + +import { windowWidth, windowHeight } from '../services/window_utils/window_utils' +import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' +import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' +import { applyConfig } from '../services/style_setter/style_setter.js' +import FaviconService from '../services/favicon_service/favicon_service.js' +import { initServiceWorker, updateFocus } from '../services/sw/sw.js' let staticInitialResults = null @@ -54,9 +24,7 @@ const parsedInitialResults = () => { return null } if (!staticInitialResults) { - staticInitialResults = JSON.parse( - document.getElementById('initial-results').textContent, - ) + staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent) } return staticInitialResults } @@ -78,7 +46,7 @@ const preloadFetch = async (request) => { return { ok: true, json: () => requestData, - text: () => requestData, + text: () => requestData } } @@ -87,60 +55,37 @@ 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: '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') console.error(error) } - // We should check for scrobbles support here but it requires userId - // so instead we check for it where it's fetched (statuses.js) } -const getBackendProvidedConfig = async () => { +const getBackendProvidedConfig = async ({ store }) => { try { const res = await window.fetch('/api/pleroma/frontend_configurations') if (res.ok) { 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) } } @@ -151,13 +96,11 @@ 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.', - error, - ) + console.warn('Failed to load static/config.json, continuing without it.') + console.warn(error) return {} } } @@ -175,25 +118,48 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { config = Object.assign({}, staticConfig, apiConfig) } - Object.keys(INSTANCE_IDENTITY_DEFAULT_DEFINITIONS).forEach((source) => { - if (source === 'name') return - if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return - useInstanceStore().set({ - value: - config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default, - path: `instanceIdentity.${source}`, - }) + const copyInstanceOption = (name) => { + store.dispatch('setInstanceOption', { name, value: config[name] }) + } + + copyInstanceOption('theme') + 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 }) - Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) => - useInstanceStore().set({ - value: - config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default, - path: `prefsStorage.${source}`, - }), - ) + store.dispatch('setInstanceOption', { + name: 'logoMargin', + value: typeof config.logoMargin === 'undefined' + ? 0 + : config.logoMargin + }) + copyInstanceOption('logoLeft') + store.commit('authFlow/setInitialStrategy', config.loginMethod) - 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 }) => { @@ -201,12 +167,13 @@ 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) + console.warn("Can't load TOS") + console.warn(e) } } @@ -215,15 +182,13 @@ 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) + console.warn("Can't load instance panel") + console.warn(e) } } @@ -232,313 +197,148 @@ 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) + console.warn("Can't load stickers") + console.warn(e) } } +const getAppSecret = async ({ store }) => { + const { state, commit } = store + const { oauth, instance } = state + return getOrCreateApp({ ...oauth, instance: instance.server, commit }) + .then((app) => getClientToken({ ...app, instance: instance.server })) + .then((token) => { + commit('setAppToken', token.access_token) + commit('setBackendInteractor', backendInteractorService(store.getters.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 }) => { try { - let res = await preloadFetch('/nodeinfo/2.1.json') - if (!res.ok) res = await preloadFetch('/nodeinfo/2.0.json') + const res = await preloadFetch('/nodeinfo/2.0.json') if (res.ok) { const data = await res.json() const metadata = data.metadata const features = metadata.features - useInstanceStore().set({ - path: 'instanceIdentity.name', - value: metadata.nodeName, - }) - 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: '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') }) + 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') }) - 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: 'pleromaBackend', value: software.name === 'pleroma' }) 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', e) + console.warn('Could not load nodeinfo') + console.warn(e) } } 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] - await setSettings({ store, apiConfig, staticConfig }) + await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store })) } const checkOAuthToken = async ({ store }) => { - const oauth = useOAuthStore() - if (oauth.userToken) { - return store.dispatch('loginUser', oauth.userToken) + if (store.getters.getUserToken()) { + try { + await store.dispatch('loginUser', store.getters.getUserToken()) + } catch (e) { + console.error(e) + } } return Promise.resolve() } -const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { - const app = createApp(App) - // Must have app use pinia before we do anything that touches the store - // https://pinia.vuejs.org/core-concepts/plugins.html#Introduction - // "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied." - app.use(pinia) - - app.config.errorHandler = (error, instance, info) => { - console.error( - 'Global Vue Error Handler caught an error:', - error, - instance, - info, - ) - useInterfaceStore().setGlobalError({ error, instance, info }) - } - - const waitForAllStoresToLoad = async () => { - // the stores that do not persist technically do not need to be awaited here, - // but that involves either hard-coding the stores in some place (prone to errors) - // or writing another vite plugin to analyze which stores needs persisting (++load time) - const allStores = import.meta.glob('../stores/*.js', { eager: true }) - 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', - ) - } - } - 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/', - ) - } - } - 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`, - ) - } - }), - ) - } - - try { - await waitForAllStoresToLoad() - } catch (e) { - console.error('Cannot load stores:', e) - storageError = e - } - - if (storageError) { - 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() +const afterStoreSetup = async ({ store, i18n }) => { + store.dispatch('setLayoutWidth', windowWidth()) + store.dispatch('setLayoutHeight', windowHeight()) FaviconService.initFaviconService() initServiceWorker(store) @@ -546,25 +346,13 @@ 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) - }) - } catch (e) { - window.splashError(e) - return Promise.reject(e) - } + await store.dispatch('setTheme') - applyStyleConfig(useMergedConfigStore().mergedConfig, i18n.global) + applyConfig(store.state.config) // Now we can try getting the server settings and logging in // Most of these are preloaded into the index.html so blocking is minimized @@ -572,9 +360,12 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { checkOAuthToken({ store }), getInstancePanel({ store }), getNodeInfo({ store }), - getInstanceConfig({ store }), - ]).catch((e) => Promise.reject(e)) + getInstanceConfig({ store }) + ]) + // Start fetching things that don't need to block the UI + store.dispatch('fetchMutes') + store.dispatch('startFetchingAnnouncements') getTOS({ store }) getStickers({ store }) @@ -582,40 +373,31 @@ 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) + const app = createApp(App) app.use(router) app.use(store) app.use(i18n) - // Little thing to get out of invalid theme state - window.resetThemes = () => { - useInterfaceStore().resetThemeV3() - useInterfaceStore().resetThemeV3Palette() - useInterfaceStore().resetThemeV2() - } - app.use(vClickOutside) app.use(VBodyScrollLock) app.use(VueVirtualScroller) app.component('FAIcon', FontAwesomeIcon) app.component('FALayers', FontAwesomeLayers) - app.component('Status', Status) - app.component('RichContent', RichContent) - app.component('StillImage', StillImage) // remove after vue 3.3 app.config.unwrapInjectedRef = true app.mount('#app') + return app } diff --git a/src/boot/routes.js b/src/boot/routes.js index d50baab04..31e3dbb07 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -1,29 +1,38 @@ -import { defineAsyncComponent } from 'vue' - -import AuthForm from 'src/components/auth_form/auth_form.js' -import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue' -import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue' -import ConversationPage from 'src/components/conversation-page/conversation-page.vue' -import DMs from 'src/components/dm_timeline/dm_timeline.vue' -import FriendsTimeline from 'src/components/friends_timeline/friends_timeline.vue' +import PublicTimeline from 'components/public_timeline/public_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 PublicAndExternalTimeline from 'src/components/public_and_external_timeline/public_and_external_timeline.vue' -import PublicTimeline from 'src/components/public_timeline/public_timeline.vue' -import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue' -import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue' -import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue' - -import { useInstanceStore } from 'src/stores/instance.js' -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' +import AnnouncementsPage from 'components/announcements_page/announcements_page.vue' +import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.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') } } @@ -31,282 +40,59 @@ 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, + redirect: _to => { + 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: defineAsyncComponent( - () => import('src/components/user_profile/user_profile.vue'), - ), - }, - { - name: 'user-profile-admin-view', - path: '/users/$:id/admin_view', - component: defineAsyncComponent( - () => import('src/components/user_profile/user_profile_admin_view.vue'), - ), - }, - { - name: 'interactions', - path: '/users/:username/interactions', - component: defineAsyncComponent( - () => import('src/components/interactions/interactions.vue'), - ), - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'dms', - path: '/users/:username/dms', - component: DMs, - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'registration', - path: '/registration', - component: defineAsyncComponent( - () => import('src/components/registration/registration.vue'), - ), - }, - { - name: 'password-reset', - path: '/password-reset', - component: defineAsyncComponent( - () => import('src/components/password_reset/password_reset.vue'), - ), - props: true, - }, - { - name: 'registration-token', - path: '/registration/:token', - component: defineAsyncComponent( - () => import('src/components/registration/registration.vue'), - ), - }, - { - name: 'friend-requests', - path: '/friend-requests', - component: defineAsyncComponent( - () => import('src/components/follow_requests/follow_requests.vue'), - ), - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'notifications', - path: '/:username/notifications', - component: defineAsyncComponent( - () => import('src/components/notifications/notifications.vue'), - ), - props: () => ({ disableTeleport: true }), - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'login', - path: '/login', - component: AuthForm, - }, - { - name: 'shout-panel', - path: '/shout-panel', - component: defineAsyncComponent( - () => import('src/components/shout_panel/shout_panel.vue'), - ), - props: () => ({ floating: false }), - }, - { - name: 'oauth-callback', - path: '/oauth-callback', - component: defineAsyncComponent( - () => import('src/components/oauth_callback/oauth_callback.vue'), - ), - props: (route) => ({ code: route.query.code }), - }, - { - name: 'search', - path: '/search', - component: defineAsyncComponent( - () => import('src/components/search/search.vue'), - ), - props: (route) => ({ query: route.query.query }), - }, - { - name: 'who-to-follow', - path: '/who-to-follow', - component: defineAsyncComponent( - () => import('src/components/who_to_follow/who_to_follow.vue'), - ), - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'about', - path: '/about', - component: defineAsyncComponent( - () => import('src/components/about/about.vue'), - ), - }, - { - name: 'announcements', - path: '/announcements', - component: defineAsyncComponent( - () => - import('src/components/announcements_page/announcements_page.vue'), - ), - }, - { - name: 'drafts', - path: '/drafts', - component: defineAsyncComponent( - () => import('src/components/drafts/drafts.vue'), - ), - }, - { - name: 'user-profile', - path: '/users/:name', - component: defineAsyncComponent( - () => import('src/components/user_profile/user_profile.vue'), - ), - }, - { - name: 'legacy-user-profile', - path: '/:name', - component: defineAsyncComponent( - () => import('src/components/user_profile/user_profile.vue'), - ), - }, - { - name: 'lists', - path: '/lists', - component: defineAsyncComponent( - () => import('src/components/lists/lists.vue'), - ), - }, - { - name: 'lists-timeline', - path: '/lists/:id', - component: defineAsyncComponent( - () => import('src/components/lists_timeline/lists_timeline.vue'), - ), - }, - { - name: 'lists-edit', - path: '/lists/:id/edit', - component: defineAsyncComponent( - () => import('src/components/lists_edit/lists_edit.vue'), - ), - }, - { - name: 'lists-new', - path: '/lists/new', - component: defineAsyncComponent( - () => import('src/components/lists_edit/lists_edit.vue'), - ), - }, - { - name: 'edit-navigation', - path: '/nav-edit', - component: NavPanel, - props: () => ({ forceExpand: true, forceEditMode: true }), - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'bookmark-folders', - path: '/bookmark_folders', - component: defineAsyncComponent( - () => import('src/components/bookmark_folders/bookmark_folders.vue'), - ), - }, - { - name: 'bookmark-folder-new', - path: '/bookmarks/new-folder', - component: defineAsyncComponent( - () => - import( - 'src/components/bookmark_folder_edit/bookmark_folder_edit.vue' - ), - ), - }, - { - name: 'bookmark-folder', - path: '/bookmarks/:id', - component: BookmarkTimeline, - }, - { - name: 'bookmark-folder-edit', - path: '/bookmarks/:id/edit', - component: defineAsyncComponent( - () => - import( - 'src/components/bookmark_folder_edit/bookmark_folder_edit.vue' - ), - ), + 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: '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: 'about', path: '/about', component: About }, + { name: 'announcements', path: '/announcements', component: AnnouncementsPage }, + { name: 'user-profile', path: '/users/:name', component: UserProfile }, + { name: 'legacy-user-profile', path: '/:name', component: UserProfile }, + { name: 'lists', path: '/lists', component: Lists }, + { name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline }, + { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }, + { name: 'lists-new', path: '/lists/new', component: ListsEdit }, + { name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute } ] - if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) { + if (store.state.instance.pleromaChatMessagesAvailable) { routes = routes.concat([ - { - name: 'chat', - path: '/users/:username/chats/:recipient_id', - component: defineAsyncComponent( - () => import('src/components/chat/chat.vue'), - ), - meta: { dontScroll: false }, - beforeEnter: validateAuthenticatedRoute, - }, - { - name: 'chats', - path: '/users/:username/chats', - component: defineAsyncComponent( - () => import('src/components/chat_list/chat_list.vue'), - ), - 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 e293895d5..1df258450 100644 --- a/src/components/about/about.js +++ b/src/components/about/about.js @@ -1,16 +1,8 @@ -import { mapState } from 'pinia' - -import FeaturesPanel from 'src/components/features_panel/features_panel.vue' -import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue' -import MRFTransparencyPanel from 'src/components/mrf_transparency_panel/mrf_transparency_panel.vue' -import StaffPanel from 'src/components/staff_panel/staff_panel.vue' -import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue' - -import { useInstanceStore } from 'src/stores/instance.js' -import { useMergedConfigStore } from 'src/stores/merged_config.js' - -const pleromaFeCommitUrl = - 'https://git.pleroma.social/pleroma/pleroma-fe/commit/' +import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue' +import FeaturesPanel from '../features_panel/features_panel.vue' +import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue' +import StaffPanel from '../staff_panel/staff_panel.vue' +import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue' const About = { components: { @@ -18,28 +10,16 @@ const About = { FeaturesPanel, TermsOfServicePanel, StaffPanel, - MRFTransparencyPanel, + MRFTransparencyPanel }, computed: { - showFeaturesPanel() { - return useInstanceStore().instanceIdentity.showFeaturesPanel - }, - frontendVersionLink() { - return pleromaFeCommitUrl + this.frontendVersion - }, - ...mapState(useInstanceStore, [ - 'backendVersion', - 'backendRepository', - 'frontendVersion', - ]), - 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/about/about.vue b/src/components/about/about.vue index f7bc0d00d..8a551485f 100644 --- a/src/components/about/about.vue +++ b/src/components/about/about.vue @@ -1,47 +1,11 @@ - diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 9b4775b97..acd93e065 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -1,111 +1,98 @@ -import { mapState } from 'pinia' -import { defineAsyncComponent } from 'vue' - -import Popover from 'src/components/popover/popover.vue' -import ProgressButton from 'src/components/progress_button/progress_button.vue' +import { mapState } from 'vuex' +import ProgressButton from '../progress_button/progress_button.vue' +import Popover from '../popover/popover.vue' import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' - -import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' -import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useReportsStore } from 'src/stores/reports' - +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' -import { faEllipsisV } from '@fortawesome/free-solid-svg-icons' +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: defineAsyncComponent( - () => import('src/components/confirm_modal/confirm_modal.vue'), - ), - UserTimedFilterModal: defineAsyncComponent( - () => - import( - 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue' - ), - ), + 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() { - useReportsStore().openUserReportingModal({ userId: this.user.id }) + reportUser () { + this.$store.dispatch('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 0c93872de..9a7c6824a 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -3,85 +3,66 @@ - - + - - - +
diff --git a/src/components/alert.style.js b/src/components/alert.style.js index 6876faca8..abbeb5baa 100644 --- a/src/components/alert.style.js +++ b/src/components/alert.style.js @@ -1,58 +1,53 @@ export default { name: 'Alert', selector: '.alert', - validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'], + validInnerComponents: [ + 'Text', + 'Icon', + 'Link', + 'Border', + 'ButtonUnstyled' + ], variants: { normal: '.neutral', - info: '.info', error: '.error', warning: '.warning', - success: '.success', - }, - editor: { - border: 1, - aspect: '3 / 1', + success: '.success' }, 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', - }, - }, - { - variant: 'info', - directives: { - background: '--cBlue', - }, - }, - ], + background: '--cGreen' + } + } + ] } diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js index ee427533f..30254926a 100644 --- a/src/components/announcement/announcement.js +++ b/src/components/announcement/announcement.js @@ -1,126 +1,108 @@ import { mapState } from 'vuex' - -import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue' +import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' +import RichContent from '../rich_content/rich_content.jsx' import localeService from '../../services/locale/locale.service.js' -import { useAnnouncementsStore } from 'src/stores/announcements.js' - const Announcement = { components: { AnnouncementEditor, + 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.has('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 this.$store.dispatch('markAnnouncementAsRead', this.announcement.id) } }, - deleteAnnouncement() { - return useAnnouncementsStore().deleteAnnouncement(this.announcement.id) + deleteAnnouncement () { + return this.$store.dispatch('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 () { + this.$store.dispatch('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 c816083a4..79a03afe1 100644 --- a/src/components/announcement_editor/announcement_editor.js +++ b/src/components/announcement_editor/announcement_editor.js @@ -1,13 +1,13 @@ -import Checkbox from 'src/components/checkbox/checkbox.vue' +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/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue index f1b016183..c0a3c30ac 100644 --- a/src/components/announcement_editor/announcement_editor.vue +++ b/src/components/announcement_editor/announcement_editor.vue @@ -34,9 +34,8 @@ id="announcement-all-day" v-model="announcement.allDay" :disabled="disabled" - > - {{ $t('announcements.all_day_prompt') }} - + /> + @@ -56,7 +55,7 @@ .post-textarea { resize: vertical; height: 10em; - overflow: visible; + overflow: none; box-sizing: content-box; } } diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js index 0f7933e5f..8d1204d4c 100644 --- a/src/components/announcements_page/announcements_page.js +++ b/src/components/announcements_page/announcements_page.js @@ -1,65 +1,58 @@ import { mapState } from 'vuex' - -import Announcement from 'src/components/announcement/announcement.vue' -import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue' - -import { useAnnouncementsStore } from 'src/stores/announcements.js' +import Announcement from '../announcement/announcement.vue' +import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' 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() { - useAnnouncementsStore().fetchAnnouncements() + mounted () { + this.$store.dispatch('fetchAnnouncements') }, computed: { ...mapState({ - currentUser: (state) => state.users.currentUser, + currentUser: state => state.users.currentUser }), - announcements() { - return useAnnouncementsStore().announcements - }, - canPostAnnouncement() { - return ( - this.currentUser && - this.currentUser.privileges.has('announcements_manage_announcements') - ) + announcements () { + return this.$store.state.announcements.announcements }, + canPostAnnouncement () { + return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements') + } }, methods: { - postAnnouncement() { + postAnnouncement () { this.posting = true - useAnnouncementsStore() - .postAnnouncement(this.newAnnouncement) + this.$store.dispatch('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/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue index 44e2cf46a..25f830ffb 100644 --- a/src/components/announcements_page/announcements_page.vue +++ b/src/components/announcements_page/announcements_page.vue @@ -1,9 +1,9 @@