diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..3de57a360
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+node_modules/
+dist/
+logs/
+.DS_Store
+.git/
+config/local.json
+pleroma-backend/
+test/e2e/reports/
+test/e2e-playwright/test-results/
+test/e2e-playwright/playwright-report/
+__screenshots__/
+
diff --git a/.forgejo/issue_template/bug.yaml b/.forgejo/issue_template/bug.yaml
new file mode 100644
index 000000000..082ee496e
--- /dev/null
+++ b/.forgejo/issue_template/bug.yaml
@@ -0,0 +1,87 @@
+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
new file mode 100644
index 000000000..c1531d8e3
--- /dev/null
+++ b/.forgejo/issue_template/suggestion.yaml
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 000000000..d2d7689bd
--- /dev/null
+++ b/.forgejo/pull_request_template.md
@@ -0,0 +1,12 @@
+### Checklist
+- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `.`.
+
+
diff --git a/.gitignore b/.gitignore
index 01ffda9a8..c4a96ee1e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,11 @@ dist/
npm-debug.log
test/unit/coverage
test/e2e/reports
+test/e2e-playwright/test-results
+test/e2e-playwright/playwright-report
selenium-debug.log
.idea/
+.gitlab-ci-local/
config/local.json
src/assets/emoji.json
logs/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 99c85dd36..247218091 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
# This file is a template, and might need editing before it works on your project.
# Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/
-image: node:18
+image: node:20
stages:
- check-changelog
@@ -34,12 +34,23 @@ check-changelog:
- apk add git
- sh ./tools/check-changelog
-lint:
+lint-eslint:
stage: lint
script:
- yarn
- - yarn lint
- - yarn stylelint
+ - yarn ci-eslint
+
+lint-biome:
+ stage: lint
+ script:
+ - yarn
+ - yarn ci-biome
+
+lint-stylelint:
+ stage: lint
+ script:
+ - yarn
+ - yarn ci-stylelint
test:
stage: test
@@ -60,6 +71,135 @@ test:
- test/**/__screenshots__
when: on_failure
+e2e-pleroma:
+ stage: test
+ image: mcr.microsoft.com/playwright:v1.57.0-jammy
+ services:
+ - name: postgres:15-alpine
+ alias: db
+ - name: $PLEROMA_IMAGE
+ alias: pleroma
+ entrypoint: ["/bin/ash", "-c"]
+ command:
+ - |
+ set -eu
+
+ SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded
+ CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs
+
+ echo '-- Waiting for database...'
+ while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do
+ sleep 1s
+ done
+
+ echo '-- Writing E2E config overrides...'
+ cat > $CONFIG_OVERRIDE_PATH </dev/null; then
+ kill -TERM $PLEROMA_PID
+ wait $PLEROMA_PID || true
+ fi
+ }
+
+ trap cleanup INT TERM
+
+ echo '-- Waiting for API...'
+ api_ok=false
+ for _i in $(seq 1 120); do
+ if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then
+ api_ok=true
+ break
+ fi
+ sleep 1s
+ done
+
+ if [ $api_ok != true ]; then
+ echo 'Timed out waiting for Pleroma API to become available'
+ exit 1
+ fi
+
+ if [ ! -f $SEED_SENTINEL_PATH ]; then
+ if [ -n ${E2E_ADMIN_USERNAME:-} ] && [ -n ${E2E_ADMIN_PASSWORD:-} ] && [ -n ${E2E_ADMIN_EMAIL:-} ]; then
+ echo '-- Seeding admin user' $E2E_ADMIN_USERNAME '...'
+ if ! /opt/pleroma/bin/pleroma_ctl user new $E2E_ADMIN_USERNAME $E2E_ADMIN_EMAIL --admin --password $E2E_ADMIN_PASSWORD -y; then
+ echo '-- User already exists or creation failed, ensuring admin + confirmed...'
+ /opt/pleroma/bin/pleroma_ctl user set $E2E_ADMIN_USERNAME --admin --confirmed
+ fi
+ else
+ echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)'
+ fi
+
+ touch $SEED_SENTINEL_PATH
+ fi
+
+ wait $PLEROMA_PID
+ tags:
+ - amd64
+ - himem
+ variables:
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
+ FF_NETWORK_PER_BUILD: "true"
+ PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable
+ POSTGRES_USER: pleroma
+ POSTGRES_PASSWORD: pleroma
+ POSTGRES_DB: pleroma
+ DB_USER: pleroma
+ DB_PASS: pleroma
+ DB_NAME: pleroma
+ DB_HOST: db
+ DB_PORT: 5432
+ DOMAIN: localhost
+ INSTANCE_NAME: Pleroma E2E
+ E2E_ADMIN_USERNAME: admin
+ E2E_ADMIN_PASSWORD: adminadmin
+ E2E_ADMIN_EMAIL: admin@example.com
+ ADMIN_EMAIL: $E2E_ADMIN_EMAIL
+ NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
+ VITE_PROXY_TARGET: http://pleroma:4000
+ VITE_PROXY_ORIGIN: http://localhost:4000
+ E2E_BASE_URL: http://localhost:8080
+ script:
+ - npm install -g yarn@1.22.22
+ - yarn --frozen-lockfile
+ - |
+ echo "-- Waiting for Pleroma API..."
+ api_ok="false"
+ for _i in $(seq 1 120); do
+ if wget -qO- http://pleroma:4000/api/v1/instance >/dev/null 2>&1; then
+ api_ok="true"
+ break
+ fi
+ sleep 1s
+ done
+ if [ "$api_ok" != "true" ]; then
+ echo "Timed out waiting for Pleroma API to become available"
+ exit 1
+ fi
+ - yarn e2e:pw
+ artifacts:
+ when: on_failure
+ paths:
+ - test/e2e-playwright/test-results
+ - test/e2e-playwright/playwright-report
+
build:
stage: build
tags:
diff --git a/.node-version b/.node-version
index 08b7109d0..5bd681170 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-18.20.8
+20.19.0
diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml
new file mode 100644
index 000000000..af0bb98e3
--- /dev/null
+++ b/.woodpecker/build.yaml
@@ -0,0 +1,43 @@
+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
new file mode 100644
index 000000000..b6c35afbb
--- /dev/null
+++ b/.woodpecker/changelog.yaml
@@ -0,0 +1,10 @@
+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
new file mode 100644
index 000000000..257887338
--- /dev/null
+++ b/.woodpecker/lint.yaml
@@ -0,0 +1,32 @@
+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
new file mode 100644
index 000000000..c0bf103cd
--- /dev/null
+++ b/.woodpecker/test-e2e.yaml
@@ -0,0 +1,101 @@
+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.57.0-jammy
+ entrypoint: *script_file_entrypoint
+ environment:
+ APT_CACHE_DIR: apt-cache
+ DEBIAN_FRONTEND: noninteractive
+ E2E_BASE_URL: http://localhost:8080
+ 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
new file mode 100644
index 000000000..acc48aacc
--- /dev/null
+++ b/.woodpecker/test.yaml
@@ -0,0 +1,61 @@
+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.57.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 ded5c6e4d..1eb5a9cb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,40 @@ 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
@@ -70,8 +104,8 @@ This does not guarantee that browsers will or will not work.
- Support displaying time in absolute format
- Add draft management system
- Compress most kinds of images on upload.
-- Added option to always convert images to JPEG format instead of using WebP when compressing images.
-- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
+- Added option to always convert images to JPEG format instead of using WebP when compressing images.
+- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
- Inform users that Smithereen public polls are public
- Splash screen + loading indicator to make process of identifying initialization issues and load performance
- UI for making v3 themes and palettes, support for bundling v3 themes
diff --git a/README.md b/README.md
index 6a37195d5..16d32dcd2 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
# For Translators
-To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/languages.js).
+To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/languages.js).
Pleroma-FE will set your language by your browser locale, but you can change language in settings.
@@ -32,10 +32,10 @@ yarn unit
# For Contributors:
-You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
+You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/src/config/local.example.json)) to enable some convenience dev options:
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
-* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
+* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/pleroma/frontend_configurations`. Only works in dev mode.
FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE.
diff --git a/biome.json b/biome.json
new file mode 100644
index 000000000..6a464a0e5
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,149 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "includes": ["**", "!!**/dist", "!!tools/emojis.json"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space"
+ },
+ "linter": {
+ "enabled": true,
+ "domains": {
+ "vue": "recommended"
+ },
+ "rules": {
+ "recommended": false,
+ "complexity": {
+ "noAdjacentSpacesInRegex": "error",
+ "noExtraBooleanCast": "error",
+ "noUselessCatch": "error",
+ "noUselessEscapeInRegex": "error"
+ },
+ "correctness": {
+ "noConstAssign": "error",
+ "noConstantCondition": "error",
+ "noEmptyCharacterClassInRegex": "error",
+ "noEmptyPattern": "error",
+ "noGlobalObjectCalls": "error",
+ "noInvalidBuiltinInstantiation": "error",
+ "noInvalidConstructorSuper": "error",
+ "noNonoctalDecimalEscape": "error",
+ "noPrecisionLoss": "error",
+ "noSelfAssign": "error",
+ "noSetterReturn": "error",
+ "noSwitchDeclarations": "error",
+ "noUndeclaredVariables": "error",
+ "noUnreachable": "error",
+ "noUnreachableSuper": "error",
+ "noUnsafeFinally": "error",
+ "noUnsafeOptionalChaining": "error",
+ "noUnusedLabels": "error",
+ "noUnusedPrivateClassMembers": "error",
+ "noUnusedVariables": "error",
+ "useIsNan": "error",
+ "useValidForDirection": "error",
+ "useValidTypeof": "error",
+ "useYield": "error"
+ },
+ "suspicious": {
+ "noAsyncPromiseExecutor": "error",
+ "noCatchAssign": "error",
+ "noClassAssign": "error",
+ "noCompareNegZero": "error",
+ "noConstantBinaryExpressions": "error",
+ "noControlCharactersInRegex": "error",
+ "noDebugger": "error",
+ "noDuplicateCase": "error",
+ "noDuplicateClassMembers": "error",
+ "noDuplicateElseIf": "error",
+ "noDuplicateObjectKeys": "error",
+ "noDuplicateParameters": "error",
+ "noEmptyBlockStatements": "error",
+ "noFallthroughSwitchClause": "error",
+ "noFunctionAssign": "error",
+ "noGlobalAssign": "error",
+ "noImportAssign": "error",
+ "noIrregularWhitespace": "error",
+ "noMisleadingCharacterClass": "error",
+ "noPrototypeBuiltins": "error",
+ "noRedeclare": "error",
+ "noShadowRestrictedNames": "error",
+ "noSparseArray": "error",
+ "noUnsafeNegation": "error",
+ "noUselessRegexBackrefs": "error",
+ "noWith": "error",
+ "useGetterReturn": "error"
+ }
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "semicolons": "asNeeded"
+ },
+ "globals": []
+ },
+ "overrides": [
+ {
+ "includes": ["**/*.spec.js", "test/fixtures/*.js"],
+ "javascript": {
+ "globals": [
+ "vi",
+ "describe",
+ "it",
+ "test",
+ "expect",
+ "before",
+ "beforeEach",
+ "after",
+ "afterEach"
+ ]
+ }
+ },
+ {
+ "includes": ["**/*.vue"],
+ "linter": {
+ "rules": {
+ "style": {
+ "useConst": "off",
+ "useImportType": "off"
+ },
+ "correctness": {
+ "noUnusedVariables": "off",
+ "noUnusedImports": "off"
+ }
+ }
+ }
+ }
+ ],
+ "assist": {
+ "enabled": true,
+ "actions": {
+ "source": {
+ "organizeImports": {
+ "level": "on",
+ "options": {
+ "groups": [
+ [":NODE:", ":PACKAGE:", "!src/**", "!@fortawesome/**"],
+ ":BLANK_LINE:",
+ [":PATH:", "src/components/**"],
+ ":BLANK_LINE:",
+ [":PATH:", "src/stores/**"],
+ ":BLANK_LINE:",
+ [":PATH:", "src/**", "src/stores/**", "src/components/**"],
+ ":BLANK_LINE:",
+ "@fortawesome/fontawesome-svg-core",
+ "@fortawesome/*"
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/build/check-versions.mjs b/build/check-versions.mjs
index 73c1eeb15..c22004b00 100644
--- a/build/check-versions.mjs
+++ b/build/check-versions.mjs
@@ -1,5 +1,5 @@
-import semver from 'semver'
import chalk from 'chalk'
+import semver from 'semver'
import packageConfig from '../package.json' with { type: 'json' }
@@ -7,8 +7,8 @@ var versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
- versionRequirement: packageConfig.engines.node
- }
+ versionRequirement: packageConfig.engines.node,
+ },
]
export default function () {
@@ -16,20 +16,26 @@ export default function () {
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
- warnings.push(mod.name + ': ' +
- chalk.red(mod.currentVersion) + ' should be ' +
- chalk.green(mod.versionRequirement)
+ warnings.push(
+ mod.name +
+ ': ' +
+ chalk.red(mod.currentVersion) +
+ ' should be ' +
+ chalk.green(mod.versionRequirement),
)
}
}
if (warnings.length) {
- console.warn(chalk.yellow('\nTo use this template, you must update following to modules:\n'))
+ console.warn(
+ chalk.yellow(
+ '\nTo use this template, you must update following to modules:\n',
+ ),
+ )
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.warn(' ' + warning)
}
- console.warn()
process.exit(1)
}
}
diff --git a/build/commit_hash.js b/build/commit_hash.js
index c104af5d9..c60355804 100644
--- a/build/commit_hash.js
+++ b/build/commit_hash.js
@@ -1,8 +1,8 @@
import childProcess from 'child_process'
-export const getCommitHash = (() => {
- const subst = "$Format:%h$"
- if(!subst.match(/Format:/)) {
+export const getCommitHash = () => {
+ const subst = '$Format:%h$'
+ if (!subst.match(/Format:/)) {
return subst
} else {
try {
@@ -15,4 +15,4 @@ export const getCommitHash = (() => {
return 'UNKNOWN'
}
}
-})
+}
diff --git a/build/copy_plugin.js b/build/copy_plugin.js
index a783fe7ff..4f020f359 100644
--- a/build/copy_plugin.js
+++ b/build/copy_plugin.js
@@ -1,8 +1,8 @@
-import serveStatic from 'serve-static'
-import { resolve } from 'node:path'
import { cp } from 'node:fs/promises'
+import { resolve } from 'node:path'
+import serveStatic from 'serve-static'
-const getPrefix = s => {
+const getPrefix = (s) => {
const padEnd = s.endsWith('/') ? s : s + '/'
return padEnd.startsWith('/') ? padEnd : '/' + padEnd
}
@@ -13,28 +13,31 @@ const copyPlugin = ({ inUrl, inFs }) => {
let copyTarget
const handler = serveStatic(inFs)
- return [{
- name: 'copy-plugin-serve',
- apply: 'serve',
- configureServer (server) {
- server.middlewares.use(prefix, handler)
- }
- }, {
- name: 'copy-plugin-build',
- apply: 'build',
- configResolved (config) {
- copyTarget = resolve(config.root, config.build.outDir, subdir)
+ return [
+ {
+ name: 'copy-plugin-serve',
+ apply: 'serve',
+ configureServer(server) {
+ server.middlewares.use(prefix, handler)
+ },
},
- closeBundle: {
- order: 'post',
- sequential: true,
- async handler () {
- console.log(`Copying '${inFs}' to ${copyTarget}...`)
- await cp(inFs, copyTarget, { recursive: true })
- console.log('Done.')
- }
- }
- }]
+ {
+ name: 'copy-plugin-build',
+ apply: 'build',
+ configResolved(config) {
+ copyTarget = resolve(config.root, config.build.outDir, subdir)
+ },
+ closeBundle: {
+ order: 'post',
+ sequential: true,
+ async handler() {
+ console.info(`Copying '${inFs}' to ${copyTarget}...`)
+ await cp(inFs, copyTarget, { recursive: true })
+ console.info('Done.')
+ },
+ },
+ },
+ ]
}
export default copyPlugin
diff --git a/build/emojis_plugin.js b/build/emojis_plugin.js
index aed52066d..9872f5331 100644
--- a/build/emojis_plugin.js
+++ b/build/emojis_plugin.js
@@ -1,21 +1,23 @@
-import { resolve } from 'node:path'
import { access } from 'node:fs/promises'
-import { languages, langCodeToCldrName } from '../src/i18n/languages.js'
+import { resolve } from 'node:path'
+
+import { languages } from '../src/i18n/languages.js'
const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/'
const specialAnnotationsLocale = {
- ja_easy: 'ja'
+ ja_easy: 'ja',
}
-const internalToAnnotationsLocale = (internal) => specialAnnotationsLocale[internal] || internal
+const internalToAnnotationsLocale = (internal) =>
+ specialAnnotationsLocale[internal] || internal
// This gets all the annotations that are accessible (whose language
// can be chosen in the settings). Data for other languages are
// discarded because there is no way for it to be fetched.
const getAllAccessibleAnnotations = async (projectRoot) => {
- const imports = (await Promise.all(
- languages
- .map(async lang => {
+ const imports = (
+ await Promise.all(
+ languages.map(async (lang) => {
const destLang = internalToAnnotationsLocale(lang)
const importModule = `${annotationsImportPrefix}${destLang}.json`
const importFile = resolve(projectRoot, 'node_modules', importModule)
@@ -23,11 +25,18 @@ const getAllAccessibleAnnotations = async (projectRoot) => {
await access(importFile)
return `'${lang}': () => import('${importModule}')`
} catch (e) {
+ if (e.message.match(/ENOENT/)) {
+ console.warn(`Missing emoji annotations locale: ${destLang}`)
+ } else {
+ console.error('test', e.message)
+ }
return
}
- })))
- .filter(k => k)
- .join(',\n')
+ }),
+ )
+ )
+ .filter((k) => k)
+ .join(',\n')
return `
export const annotationsLoader = {
@@ -43,21 +52,21 @@ const emojisPlugin = () => {
let projectRoot
return {
name: 'emojis-plugin',
- configResolved (conf) {
+ configResolved(conf) {
projectRoot = conf.root
},
- resolveId (id) {
+ resolveId(id) {
if (id === emojiAnnotationsId) {
return emojiAnnotationsIdResolved
}
return null
},
- async load (id) {
+ async load(id) {
if (id === emojiAnnotationsIdResolved) {
return await getAllAccessibleAnnotations(projectRoot)
}
return null
- }
+ },
}
}
diff --git a/build/msw_plugin.js b/build/msw_plugin.js
index f544348fc..c4e9098c5 100644
--- a/build/msw_plugin.js
+++ b/build/msw_plugin.js
@@ -1,5 +1,5 @@
-import { resolve } from 'node:path'
import { readFile } from 'node:fs/promises'
+import { resolve } from 'node:path'
const target = 'node_modules/msw/lib/mockServiceWorker.js'
@@ -8,10 +8,10 @@ const mswPlugin = () => {
return {
name: 'msw-plugin',
apply: 'serve',
- configResolved (conf) {
+ configResolved(conf) {
projectRoot = conf.root
},
- configureServer (server) {
+ configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (req.path === '/mockServiceWorker.js') {
const file = await readFile(resolve(projectRoot, target))
@@ -21,7 +21,7 @@ const mswPlugin = () => {
next()
}
})
- }
+ },
}
}
diff --git a/build/service_worker_messages.js b/build/service_worker_messages.js
index c078e8563..0948aa919 100644
--- a/build/service_worker_messages.js
+++ b/build/service_worker_messages.js
@@ -1,11 +1,12 @@
-import { languages, langCodeToJsonName } from '../src/i18n/languages.js'
import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
+import { langCodeToJsonName, languages } from '../src/i18n/languages.js'
+
const i18nDir = resolve(
dirname(dirname(fileURLToPath(import.meta.url))),
- 'src/i18n'
+ 'src/i18n',
)
export const i18nFiles = languages.reduce((acc, lang) => {
@@ -16,13 +17,15 @@ export const i18nFiles = languages.reduce((acc, lang) => {
}, {})
export const generateServiceWorkerMessages = async () => {
- const msgArray = await Promise.all(Object.entries(i18nFiles).map(async ([lang, file]) => {
- const fileContent = await readFile(file, 'utf-8')
- const msg = {
- notifications: JSON.parse(fileContent).notifications || {}
- }
- return [lang, msg]
- }))
+ const msgArray = await Promise.all(
+ Object.entries(i18nFiles).map(async ([lang, file]) => {
+ const fileContent = await readFile(file, 'utf-8')
+ const msg = {
+ notifications: JSON.parse(fileContent).notifications || {},
+ }
+ return [lang, msg]
+ }),
+ )
return msgArray.reduce((acc, [lang, msg]) => {
acc[lang] = msg
return acc
diff --git a/build/sw_plugin.js b/build/sw_plugin.js
index a2c792b7d..066c8da12 100644
--- a/build/sw_plugin.js
+++ b/build/sw_plugin.js
@@ -1,9 +1,13 @@
-import { fileURLToPath } from 'node:url'
-import { dirname, resolve } from 'node:path'
import { readFile } from 'node:fs/promises'
+import { dirname, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { exactRegex } from '@rolldown/pluginutils'
import { build } from 'vite'
-import * as esbuild from 'esbuild'
-import { generateServiceWorkerMessages, i18nFiles } from './service_worker_messages.js'
+
+import {
+ generateServiceWorkerMessages,
+ i18nFiles,
+} from './service_worker_messages.js'
const getSWMessagesAsText = async () => {
const messages = await generateServiceWorkerMessages()
@@ -11,123 +15,24 @@ const getSWMessagesAsText = async () => {
}
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
-const swEnvName = 'virtual:pleroma-fe/service_worker_env'
-const swEnvNameResolved = '\0' + swEnvName
-const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };`
-const getProdSwEnv = ({ assets }) => `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
-
-export const devSwPlugin = ({
- swSrc,
- swDest,
- transformSW,
- alias
-}) => {
- const swFullSrc = resolve(projectRoot, swSrc)
- const esbuildAlias = {}
- Object.entries(alias).forEach(([source, dest]) => {
- esbuildAlias[source] = dest.startsWith('/') ? projectRoot + dest : dest
- })
-
- return {
- name: 'dev-sw-plugin',
- apply: 'serve',
- configResolved (conf) {
- },
- resolveId (id) {
- const name = id.startsWith('/') ? id.slice(1) : id
- if (name === swDest) {
- return swFullSrc
- } else if (name === swEnvName) {
- return swEnvNameResolved
- }
- return null
- },
- async load (id) {
- if (id === swFullSrc) {
- return readFile(swFullSrc, 'utf-8')
- } else if (id === swEnvNameResolved) {
- return getDevSwEnv()
- }
- return null
- },
- /**
- * vite does not bundle the service worker
- * during dev, and firefox does not support ESM as service worker
- * https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
- */
- async transform (code, id) {
- if (id === swFullSrc && transformSW) {
- const res = await esbuild.build({
- entryPoints: [swSrc],
- bundle: true,
- write: false,
- outfile: 'sw-pleroma.js',
- alias: esbuildAlias,
- plugins: [{
- name: 'vite-like-root-resolve',
- setup (b) {
- b.onResolve(
- { filter: new RegExp(/^\//) },
- args => ({
- path: resolve(projectRoot, args.path.slice(1))
- })
- )
- }
- }, {
- name: 'sw-messages',
- setup (b) {
- b.onResolve(
- { filter: new RegExp('^' + swMessagesName + '$') },
- args => ({
- path: args.path,
- namespace: 'sw-messages'
- }))
- b.onLoad(
- { filter: /.*/, namespace: 'sw-messages' },
- async () => ({
- contents: await getSWMessagesAsText()
- }))
- }
- }, {
- name: 'sw-env',
- setup (b) {
- b.onResolve(
- { filter: new RegExp('^' + swEnvName + '$') },
- args => ({
- path: args.path,
- namespace: 'sw-env'
- }))
- b.onLoad(
- { filter: /.*/, namespace: 'sw-env' },
- () => ({
- contents: getDevSwEnv()
- }))
- }
- }]
- })
- const text = res.outputFiles[0].text
- return text
- }
- }
- }
-}
-
// Idea taken from
// https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/plugins/build.ts
// rollup does not support compiling to iife if we want to code-split;
// however, we must compile the service worker to iife because of browser support.
// Run another vite build just for the service worker targeting iife at
// the end of the build.
-export const buildSwPlugin = ({
- swSrc,
- swDest,
-}) => {
+export const buildSwPlugin = ({ swSrc, swDest }) => {
+ const swFullSrc = resolve(projectRoot, swSrc)
+ const swEnvName = 'virtual:pleroma-fe/service_worker_env'
+ const swEnvNameResolved = '\0' + swEnvName
+
let config
+
return {
name: 'build-sw-plugin',
enforce: 'post',
- apply: 'build',
- configResolved (resolvedConfig) {
+ configResolved(resolvedConfig) {
+ resolvedConfig
config = {
define: resolvedConfig.define,
resolve: resolvedConfig.resolve,
@@ -135,53 +40,81 @@ export const buildSwPlugin = ({
publicDir: false,
build: {
...resolvedConfig.build,
- lib: {
- entry: swSrc,
- formats: ['iife'],
- name: 'sw_pleroma'
- },
emptyOutDir: false,
- rollupOptions: {
+ rolldownOptions: {
+ input: {
+ main: swSrc,
+ },
+ context: 'self',
output: {
- entryFileNames: swDest
- }
- }
+ entryFileNames: swDest,
+ codeSplitting: false,
+ format: 'iife',
+ },
+ },
},
- configFile: false
+ configFile: false,
}
},
generateBundle: {
order: 'post',
sequential: true,
- async handler (_, bundle) {
+ async handler(_, bundle) {
const assets = Object.keys(bundle)
- .filter(name => !/\.map$/.test(name))
- .map(name => '/' + name)
+ .filter((name) => !/\.map$/.test(name))
+ .map((name) => '/' + name)
+
config.plugins.push({
name: 'build-sw-env-plugin',
- resolveId (id) {
- if (id === swEnvName) {
- return swEnvNameResolved
- }
- return null
+ mode: 'production',
+ resolveId: {
+ filter: { id: exactRegex(swEnvName) },
+ handler: () => swEnvNameResolved,
+ },
+ load: {
+ filter: { id: exactRegex(swEnvNameResolved) },
+ handler() {
+ return `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
+ },
},
- load (id) {
- if (id === swEnvNameResolved) {
- return getProdSwEnv({ assets })
- }
- return null
- }
})
- }
+ },
+ },
+ resolveId: {
+ filter: { id: new RegExp(swDest) },
+ handler() {
+ return swFullSrc
+ },
+ },
+ load: {
+ filter: { id: new RegExp(swFullSrc) },
+ async handler() {
+ config.plugins.push({
+ name: 'dummy-sw-env',
+ mode: 'development',
+ resolveId: {
+ filter: { id: exactRegex(swEnvName) },
+ handler: () => swEnvNameResolved,
+ },
+ load: {
+ filter: { id: exactRegex(swEnvNameResolved) },
+ handler: () => 'self.serviceWorkerOption = { assets: [] }',
+ },
+ })
+
+ const swBundle = await build(config)
+ return swBundle.output[0]
+ },
},
closeBundle: {
order: 'post',
sequential: true,
- async handler () {
- console.log('Building service worker for production')
+ async handler() {
+ if (process.env.VITEST) return
+ console.info('Building service worker for production')
await build(config)
- }
- }
+ },
+ },
}
}
@@ -191,9 +124,9 @@ const swMessagesNameResolved = '\0' + swMessagesName
export const swMessagesPlugin = () => {
return {
name: 'sw-messages-plugin',
- resolveId (id) {
+ resolveId(id) {
if (id === swMessagesName) {
- Object.values(i18nFiles).forEach(f => {
+ Object.values(i18nFiles).forEach((f) => {
this.addWatchFile(f)
})
return swMessagesNameResolved
@@ -201,11 +134,11 @@ export const swMessagesPlugin = () => {
return null
}
},
- async load (id) {
+ async load(id) {
if (id === swMessagesNameResolved) {
return await getSWMessagesAsText()
}
return null
- }
+ },
}
}
diff --git a/build/update-emoji.js b/build/update-emoji.js
index 5d578ba61..4ff7e1de8 100644
--- a/build/update-emoji.js
+++ b/build/update-emoji.js
@@ -1,22 +1,21 @@
-
-import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' }
+import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with {
+ type: 'json',
+}
import fs from 'fs'
-Object.keys(emojis)
- .map(k => {
- emojis[k].map(e => {
- delete e.unicode_version
- delete e.emoji_version
- delete e.skin_tone_support_unicode_version
- })
+Object.keys(emojis).map((k) => {
+ emojis[k].map((e) => {
+ delete e.unicode_version
+ delete e.emoji_version
+ delete e.skin_tone_support_unicode_version
})
+})
const res = {}
-Object.keys(emojis)
- .map(k => {
- const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
- res[groupId] = emojis[k]
- })
+Object.keys(emojis).map((k) => {
+ const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
+ res[groupId] = emojis[k]
+})
console.info('Updating emojis...')
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res))
diff --git a/changelog.d/attrs-parsing.fix b/changelog.d/attrs-parsing.fix
new file mode 100644
index 000000000..e36e59a86
--- /dev/null
+++ b/changelog.d/attrs-parsing.fix
@@ -0,0 +1 @@
+Fix HTML attribute parsing for escaped quotes
\ No newline at end of file
diff --git a/changelog.d/finalfix-fr.skip b/changelog.d/ci-pr-uploads-removal.skip
similarity index 100%
rename from changelog.d/finalfix-fr.skip
rename to changelog.d/ci-pr-uploads-removal.skip
diff --git a/changelog.d/fast.change b/changelog.d/fast.change
new file mode 100644
index 000000000..1f0a89092
--- /dev/null
+++ b/changelog.d/fast.change
@@ -0,0 +1 @@
+Migrated to Vite 8 and optimized our imports, more stuff is loaded on-demand, reducing the initial load time and transfer size
diff --git a/changelog.d/fix-emojis-breaking-bio.fix b/changelog.d/fix-emojis-breaking-bio.fix
new file mode 100644
index 000000000..62a607d8a
--- /dev/null
+++ b/changelog.d/fix-emojis-breaking-bio.fix
@@ -0,0 +1 @@
+Fix emojis breaking user bio/description editing
diff --git a/changelog.d/finalfix.skip b/changelog.d/instance-store-migration.skip
similarity index 100%
rename from changelog.d/finalfix.skip
rename to changelog.d/instance-store-migration.skip
diff --git a/changelog.d/minor.add b/changelog.d/minor.add
new file mode 100644
index 000000000..5f4934173
--- /dev/null
+++ b/changelog.d/minor.add
@@ -0,0 +1,6 @@
+button to remove all drafts
+option to remove forced aspect ratio for user profiles (requested)
+showing user tags (mrf policies for user + custom if present)
+version information now is also in about page
+mention autosuggest now sorts by recent activity
+non-square emoji support (toggleable by user)
diff --git a/changelog.d/minor.change b/changelog.d/minor.change
new file mode 100644
index 000000000..979d36955
--- /dev/null
+++ b/changelog.d/minor.change
@@ -0,0 +1,7 @@
+overall improved spacings in status action buttons and post form
+logout confirm button is now dangerous
+reply/quote now is a radio group and wraps, fixes overflow on languages where labels are too wide
+personal note input is now bigger
+moved "edit pinned" to the bottom for status action buttons.
+dots status action button drops down instead of up to avoid hiding the action buttons
+improved attachment description (alt text) input and display
diff --git a/changelog.d/minor.fix b/changelog.d/minor.fix
new file mode 100644
index 000000000..420836364
--- /dev/null
+++ b/changelog.d/minor.fix
@@ -0,0 +1,12 @@
+navbar wide logo cropping search input
+danger buttons being too bright
+user background upload failure no longer breaks new uploads + displays an error
+importing theme from old theme editor
+removed duplicate federationpolicy entry in admin tab
+repeater name overflowing content
+reply popover is now shown if replied-to status is muted
+second language input not having header
+post form's bottom left buttons not showing their toggled state
+some font overrides not working
+popovers opening outside of window's boundaries
+occasional blank page when showing new posts
diff --git a/changelog.d/mute-dropdown.fix b/changelog.d/mute-dropdown.fix
new file mode 100644
index 000000000..33f12a571
--- /dev/null
+++ b/changelog.d/mute-dropdown.fix
@@ -0,0 +1 @@
+Fixed status action mute hiding itself on click
diff --git a/changelog.d/profilebg.add b/changelog.d/profilebg.add
new file mode 100644
index 000000000..a2c79074a
--- /dev/null
+++ b/changelog.d/profilebg.add
@@ -0,0 +1 @@
+displaying other user's backgrounds (if supported by BE)
diff --git a/changelog.d/quote-by-url.add b/changelog.d/quote-by-url.add
new file mode 100644
index 000000000..ef401f93c
--- /dev/null
+++ b/changelog.d/quote-by-url.add
@@ -0,0 +1 @@
+Add quoting by URL and in replies
diff --git a/changelog.d/reply-quote-config.fix b/changelog.d/reply-quote-config.fix
new file mode 100644
index 000000000..b6ac4e5e9
--- /dev/null
+++ b/changelog.d/reply-quote-config.fix
@@ -0,0 +1 @@
+Fix reply form crash when quote-reply settings are unavailable
diff --git a/changelog.d/sync-config.add b/changelog.d/sync-config.add
new file mode 100644
index 000000000..76a5a1cca
--- /dev/null
+++ b/changelog.d/sync-config.add
@@ -0,0 +1,2 @@
+settings synchronization
+user highlight synchronization
diff --git a/changelog.d/woodpecker-pr-pipeline.skip b/changelog.d/woodpecker-pr-pipeline.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml
new file mode 100644
index 000000000..75a4979a1
--- /dev/null
+++ b/docker-compose.e2e.yml
@@ -0,0 +1,57 @@
+services:
+ db:
+ image: postgres:15-alpine
+ environment:
+ POSTGRES_USER: pleroma
+ POSTGRES_PASSWORD: pleroma
+ POSTGRES_DB: pleroma
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U pleroma -d pleroma"]
+ interval: 2s
+ timeout: 2s
+ retries: 30
+
+ pleroma:
+ image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable}
+ environment:
+ DB_USER: pleroma
+ DB_PASS: pleroma
+ DB_NAME: pleroma
+ DB_HOST: db
+ DB_PORT: 5432
+ DOMAIN: localhost
+ INSTANCE_NAME: Pleroma E2E
+ ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
+ NOTIFY_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
+ E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
+ E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
+ E2E_ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
+ depends_on:
+ db:
+ condition: service_healthy
+ volumes:
+ - ./docker/pleroma/entrypoint.e2e.sh:/opt/pleroma/entrypoint.e2e.sh:ro
+ entrypoint: ["/bin/ash", "/opt/pleroma/entrypoint.e2e.sh"]
+ healthcheck:
+ # NOTE: "localhost" may resolve to ::1 in some images (IPv6) while Pleroma only
+ # listens on IPv4 in this container. Use 127.0.0.1 to avoid false negatives.
+ test: ["CMD-SHELL", "test -f /var/lib/pleroma/.e2e_seeded && wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null || exit 1"]
+ interval: 5s
+ timeout: 3s
+ retries: 60
+
+ e2e:
+ build:
+ context: .
+ dockerfile: docker/e2e/Dockerfile.e2e
+ depends_on:
+ pleroma:
+ condition: service_healthy
+ environment:
+ CI: "1"
+ VITE_PROXY_TARGET: http://pleroma:4000
+ VITE_PROXY_ORIGIN: http://localhost:4000
+ E2E_BASE_URL: http://localhost:8080
+ E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
+ E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
+ command: ["yarn", "e2e:pw"]
diff --git a/docker/e2e/Dockerfile.e2e b/docker/e2e/Dockerfile.e2e
new file mode 100644
index 000000000..e84359ceb
--- /dev/null
+++ b/docker/e2e/Dockerfile.e2e
@@ -0,0 +1,16 @@
+FROM mcr.microsoft.com/playwright:v1.57.0-jammy
+
+WORKDIR /app
+
+ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
+
+RUN npm install -g yarn@1.22.22
+
+COPY package.json yarn.lock ./
+RUN yarn --frozen-lockfile
+
+COPY . .
+
+ENV CI=1
+
+CMD ["yarn", "e2e:pw"]
diff --git a/docker/pleroma/entrypoint.e2e.sh b/docker/pleroma/entrypoint.e2e.sh
new file mode 100644
index 000000000..96920eeae
--- /dev/null
+++ b/docker/pleroma/entrypoint.e2e.sh
@@ -0,0 +1,71 @@
+#!/bin/ash
+
+set -eu
+
+SEED_SENTINEL_PATH="/var/lib/pleroma/.e2e_seeded"
+CONFIG_OVERRIDE_PATH="/var/lib/pleroma/config.exs"
+
+echo "-- Waiting for database..."
+while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do
+ sleep 1s
+done
+
+echo "-- Writing E2E config overrides..."
+cat > "$CONFIG_OVERRIDE_PATH" <<'EOF'
+import Config
+
+config :pleroma, Pleroma.Captcha,
+ enabled: false
+
+config :pleroma, :instance,
+ registrations_open: true,
+ account_activation_required: false,
+ approval_required: false
+EOF
+
+echo "-- Running migrations..."
+/opt/pleroma/bin/pleroma_ctl migrate
+
+echo "-- Starting!"
+/opt/pleroma/bin/pleroma start &
+PLEROMA_PID="$!"
+
+cleanup() {
+ if [ -n "${PLEROMA_PID:-}" ] && kill -0 "$PLEROMA_PID" 2>/dev/null; then
+ kill -TERM "$PLEROMA_PID"
+ wait "$PLEROMA_PID" || true
+ fi
+}
+
+trap cleanup INT TERM
+
+echo "-- Waiting for API..."
+api_ok="false"
+for _i in $(seq 1 120); do
+ if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then
+ api_ok="true"
+ break
+ fi
+ sleep 1s
+done
+
+if [ "$api_ok" != "true" ]; then
+ echo "Timed out waiting for Pleroma API to become available"
+ exit 1
+fi
+
+if [ ! -f "$SEED_SENTINEL_PATH" ]; then
+ if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then
+ echo "-- Seeding admin user (${E2E_ADMIN_USERNAME})..."
+ if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then
+ echo "-- User already exists (or creation failed), ensuring admin + confirmed..."
+ /opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed
+ fi
+ else
+ echo "-- Skipping admin seeding (missing E2E_ADMIN_* env)"
+ fi
+
+ touch "$SEED_SENTINEL_PATH"
+fi
+
+wait "$PLEROMA_PID"
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index dfc5f9dc3..8ca076931 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -7,9 +7,9 @@
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
-1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
-2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json).
-3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) )
+1. `/api/pleroma/frontend_configurations` - this is generated by backend and includes FE/Client-specific data. PleromaFE uses the `pleroma_fe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
+2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/src/public/static/config.json).
+3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/src/src/stores/instance.js) )
## Instance-defaults
diff --git a/docs/HACKING.md b/docs/HACKING.md
index a5c491136..88760b77a 100644
--- a/docs/HACKING.md
+++ b/docs/HACKING.md
@@ -79,7 +79,7 @@ server {
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](../backend/API/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
-PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
+PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/src/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters.
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 01bdb2038..417ff8cf3 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,37 +1,34 @@
-import vue from "eslint-plugin-vue";
-import js from "@eslint/js";
-import globals from "globals";
+import js from '@eslint/js'
+import { defineConfig, globalIgnores } from 'eslint/config'
+import vue from 'eslint-plugin-vue'
+import globals from 'globals'
-
-export default [
+export default defineConfig([
...vue.configs['flat/recommended'],
- js.configs.recommended,
+ globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']),
{
- files: ["**/*.js", "**/*.mjs", "**/*.vue"],
- ignores: ["build/*.js", "config/*.js"],
-
+ files: ['src/**/*.vue'],
+ plugins: { js },
+ extends: ['js/recommended'],
languageOptions: {
ecmaVersion: 2024,
- sourceType: "module",
+ sourceType: 'module',
parserOptions: {
- parser: "@babel/eslint-parser",
+ parser: '@babel/eslint-parser',
},
globals: {
...globals.browser,
...globals.vitest,
...globals.chai,
...globals.commonjs,
- ...globals.serviceworker
- }
+ ...globals.serviceworker,
+ },
},
rules: {
- 'arrow-parens': 0,
- 'generator-star-spacing': 0,
- 'no-debugger': 0,
'vue/require-prop-types': 0,
'vue/multi-word-component-names': 0,
- }
- }
-]
+ },
+ },
+])
diff --git a/package.json b/package.json
index edc122760..c51d98b81 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "pleroma_fe",
- "version": "2.9.2",
+ "version": "2.10.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",
@@ -10,19 +10,22 @@
"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": "node test/e2e/runner.js",
+ "e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs",
+ "e2e": "sh ./tools/e2e/run.sh",
"test": "yarn run unit && yarn run e2e",
- "stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
- "lint": "eslint src test/unit/specs test/e2e/specs",
- "lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
+ "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"
},
"dependencies": {
- "@babel/runtime": "7.28.3",
+ "@babel/runtime": "7.28.4",
"@chenfengyuan/vue-qrcode": "2.0.0",
- "@fortawesome/fontawesome-svg-core": "6.7.2",
- "@fortawesome/free-regular-svg-icons": "6.7.2",
- "@fortawesome/free-solid-svg-icons": "6.7.2",
- "@fortawesome/vue-fontawesome": "3.1.1",
+ "@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",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
@@ -39,51 +42,53 @@
"js-cookie": "3.0.5",
"localforage": "1.10.0",
"parse-link-header": "2.0.0",
- "phoenix": "1.8.0",
- "pinia": "^3.0.0",
+ "phoenix": "1.8.1",
+ "pinia": "^3.0.4",
"punycode.js": "2.3.1",
"qrcode": "1.5.4",
"querystring-es3": "0.2.1",
"url": "0.11.4",
"utf8": "3.0.0",
"uuid": "11.1.0",
- "vue": "3.5.19",
+ "vue": "3.5.22",
"vue-i18n": "11",
- "vue-router": "4.5.1",
+ "vue-router": "4.6.4",
"vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.1.0"
},
"devDependencies": {
- "@babel/core": "7.28.3",
- "@babel/eslint-parser": "7.28.0",
- "@babel/plugin-transform-runtime": "7.28.3",
- "@babel/preset-env": "7.28.3",
+ "@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",
"@ungap/event-target": "0.2.4",
- "@vitejs/plugin-vue": "^5.2.1",
- "@vitejs/plugin-vue-jsx": "^4.1.1",
+ "@vitejs/devtools": "^0.3.1",
+ "@vitejs/plugin-vue": "^6.0.7",
+ "@vitejs/plugin-vue-jsx": "^5.1.5",
"@vitest/browser": "^3.0.7",
"@vitest/ui": "^3.0.7",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.5.0",
- "@vue/compiler-sfc": "3.5.19",
+ "@vue/compiler-sfc": "3.5.22",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.21",
"babel-plugin-lodash": "3.3.4",
- "chai": "5.3.2",
- "chalk": "5.6.0",
+ "chai": "5.3.3",
+ "chalk": "5.6.2",
"chromedriver": "135.0.4",
"connect-history-api-fallback": "2.0.0",
"cross-spawn": "7.0.6",
"custom-event-polyfill": "1.0.7",
- "eslint": "9.33.0",
- "vue-eslint-parser": "10.2.0",
+ "eslint": "9.39.2",
"eslint-config-standard": "17.1.0",
"eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.32.0",
- "eslint-plugin-n": "17.21.3",
+ "eslint-plugin-n": "17.23.1",
"eslint-plugin-promise": "7.2.1",
- "eslint-plugin-vue": "10.4.0",
+ "eslint-plugin-vue": "10.6.2",
"eventsource-polyfill": "0.9.6",
"express": "5.1.0",
"function-bind": "1.1.2",
@@ -92,27 +97,29 @@
"lodash": "4.17.21",
"msw": "2.10.5",
"nightwatch": "3.12.2",
- "playwright": "1.55.0",
+ "oxc": "^1.0.1",
+ "playwright": "1.57.0",
"postcss": "8.5.6",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
- "sass": "1.89.2",
+ "sass-embedded": "^1.100.0",
"selenium-server": "3.141.59",
- "semver": "7.7.2",
+ "semver": "7.7.3",
"serve-static": "2.2.0",
"shelljs": "0.10.0",
"sinon": "20.0.0",
- "sinon-chai": "4.0.0",
- "stylelint": "16.19.1",
+ "sinon-chai": "4.0.1",
+ "stylelint": "16.25.0",
"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": "^6.1.0",
- "vite-plugin-eslint2": "^5.0.3",
- "vite-plugin-stylelint": "^6.0.0",
- "vitest": "^3.0.7"
+ "vite": "^8.0.0",
+ "vite-plugin-eslint2": "^5.1.0",
+ "vite-plugin-stylelint": "^6.1.0",
+ "vitest": "^3.0.7",
+ "vue-eslint-parser": "10.2.0"
},
"type": "module",
"engines": {
diff --git a/postcss.config.js b/postcss.config.js
index 95ebbf2a6..b7fc12838 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,7 +1,5 @@
import autoprefixer from 'autoprefixer'
export default {
- plugins: [
- autoprefixer
- ]
+ plugins: [autoprefixer],
}
diff --git a/public/static/palettes/index.json b/public/static/palettes/index.json
index 2cd110d1e..3c4e37e44 100644
--- a/public/static/palettes/index.json
+++ b/public/static/palettes/index.json
@@ -1,6 +1,26 @@
{
- "pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
- "pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "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",
@@ -12,8 +32,28 @@
"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"],
+ "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",
@@ -36,8 +76,28 @@
"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" ],
+ "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",
diff --git a/public/static/splash.css b/public/static/splash.css
index f56f33d07..caf57dacb 100644
--- a/public/static/splash.css
+++ b/public/static/splash.css
@@ -65,16 +65,17 @@ body {
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";
+ 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))
+ --logoChunkSize: calc(2em * 0.5 * var(--scale));
}
.chunk {
@@ -84,7 +85,7 @@ body {
#chunk-P {
grid-area: P;
- border-top-left-radius: calc(var(--logoChunkSize) / 2);
+ border-top-left-radius: calc(var(--logoChunkSize) / 2);
}
#chunk-L {
diff --git a/renovate.json b/renovate.json
index 39a2b6e9a..4bd832f5f 100644
--- a/renovate.json
+++ b/renovate.json
@@ -1,6 +1,4 @@
{
"$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 0027d908a..ea2271a75 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,187 +1,268 @@
-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 { getOrCreateServiceWorker } from './services/sw/sw'
-import { windowWidth, windowHeight } from './services/window_utils/window_utils'
-import { mapGetters } from 'vuex'
-import { defineAsyncComponent } from 'vue'
-import { useShoutStore } from './stores/shout'
-import { useInterfaceStore } from './stores/interface'
-
import { throttle } from 'lodash'
+import { mapState } from 'pinia'
+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 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'
+
+import messages from 'src/i18n/messages'
+import localeService from 'src/services/locale/locale.service.js'
+
+// Helper to unwrap reactive proxies
+window.toValue = (x) => JSON.parse(JSON.stringify(x))
export default {
name: 'app',
components: {
UserPanel,
NavPanel,
- Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
+ Notifications: defineAsyncComponent(
+ () => import('src/components/notifications/notifications.vue'),
+ ),
InstanceSpecificPanel,
FeaturesPanel,
- WhoToFollowPanel,
- ShoutPanel,
- MediaModal,
- SideDrawer,
+ WhoToFollowPanel: defineAsyncComponent(
+ () =>
+ import('src/components/who_to_follow_panel/who_to_follow_panel.vue'),
+ ),
+ ShoutPanel: defineAsyncComponent(
+ () => import('src/components/shout_panel/shout_panel.vue'),
+ ),
+ MediaModal: defineAsyncComponent(
+ () => import('src/components/media_modal/media_modal.vue'),
+ ),
MobilePostStatusButton,
MobileNav,
DesktopNav,
- SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
- UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
- UserReportingModal,
- PostStatusModal,
- EditStatusModal,
- StatusHistoryModal,
- GlobalNoticeList
+ 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'),
+ ),
+ GlobalNoticeList,
},
data: () => ({
- mobileActivePanel: 'timeline'
+ mobileActivePanel: 'timeline',
}),
- watch: {
- themeApplied () {
- this.removeSplash()
- },
- currentTheme () {
- this.setThemeBodyClass()
- },
- layoutType () {
- document.getElementById('modal').classList = ['-' + this.layoutType]
+ provide() {
+ return {
+ allowNonSquareEmoji: useMergedConfigStore().mergedConfig.nonSquareEmoji,
}
},
- created () {
+ watch: {
+ themeApplied() {
+ this.removeSplash()
+ },
+ currentTheme() {
+ this.setThemeBodyClass()
+ },
+ layoutType() {
+ document.getElementById('modal').classList = ['-' + this.layoutType]
+ },
+ },
+ created() {
// Load the locale from the storage
- const val = this.$store.getters.mergedConfig.interfaceLanguage
- this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+ 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 () {
+ mounted() {
window.addEventListener('resize', this.updateMobileState)
this.scrollParent.addEventListener('scroll', this.updateScrollState)
- if (useInterfaceStore().themeApplied) {
+ if (this.themeApplied) {
this.setThemeBodyClass()
this.removeSplash()
}
getOrCreateServiceWorker()
},
- unmounted () {
+ unmounted() {
window.removeEventListener('resize', this.updateMobileState)
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
},
computed: {
- themeApplied () {
- return useInterfaceStore().themeApplied
- },
- currentTheme () {
- if (useInterfaceStore().styleDataUsed) {
- const styleMeta = useInterfaceStore().styleDataUsed.find(x => x.component === '@meta')
+ currentTheme() {
+ if (this.styleDataUsed) {
+ const styleMeta = this.styleDataUsed.find(
+ (x) => x.component === '@meta',
+ )
if (styleMeta !== undefined) {
- return styleMeta.directives.name.replaceAll(" ", "-").toLowerCase()
+ return styleMeta.directives.name.replaceAll(' ', '-').toLowerCase()
}
}
return 'stock'
},
- layoutModalClass () {
+ 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 } = this.$store.getters.mergedConfig
+ navClasses() {
+ const { navbarColumnStretch } = useMergedConfigStore().mergedConfig
return [
'-' + this.layoutType,
- ...(navbarColumnStretch ? ['-column-stretch'] : [])
+ ...(navbarColumnStretch ? ['-column-stretch'] : []),
]
},
- currentUser () { return this.$store.state.users.currentUser },
- userBackground () { return this.currentUser.background_image },
- instanceBackground () {
- return this.mergedConfig.hideInstanceWallpaper
- ? null
- : this.$store.state.instance.background
+ currentUser() {
+ return this.$store.state.users.currentUser
},
- background () { return this.userBackground || this.instanceBackground },
- bgStyle () {
+ userBackground() {
+ return this.currentUser.background_image
+ },
+ foreignProfileBackground() {
+ return (
+ useMergedConfigStore().mergedConfig.allowForeignUserBackground &&
+ useInterfaceStore().foreignProfileBackground
+ )
+ },
+ instanceBackground() {
+ return useMergedConfigStore().mergedConfig.hideInstanceWallpaper
+ ? null
+ : this.instanceBackgroundUrl
+ },
+ background() {
+ return (
+ this.foreignProfileBackground ||
+ this.userBackground ||
+ this.instanceBackground
+ )
+ },
+ bgStyle() {
if (this.background) {
return {
- '--body-background-image': `url(${this.background})`
+ '--body-background-image': `url(${this.background})`,
}
}
},
- shout () { return useShoutStore().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
+ shoutJoined() {
+ return useShoutStore().joined
},
- 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 this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
+ return (
+ useMergedConfigStore().mergedConfig.alwaysShowNewPostButton ||
+ this.layoutType === 'mobile'
+ )
},
- showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
- editingAvailable () { return this.$store.state.instance.editingAvailable },
- shoutboxPosition () {
- return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
+ shoutboxPosition() {
+ return (
+ useMergedConfigStore().mergedConfig.alwaysShowNewPostButton || false
+ )
},
- hideShoutbox () {
- return this.$store.getters.mergedConfig.hideShoutbox
+ hideShoutbox() {
+ return useMergedConfigStore().mergedConfig.hideShoutbox
},
- layoutType () { return useInterfaceStore().layoutType },
- privateMode () { return this.$store.state.instance.private },
- reverseLayout () {
- const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
+ reverseLayout() {
+ const { thirdColumnMode, sidebarRight: reverseSetting } =
+ useMergedConfigStore().mergedConfig
if (this.layoutType !== 'wide') {
return reverseSetting
} else {
- return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
+ return thirdColumnMode === 'notifications'
+ ? reverseSetting
+ : !reverseSetting
}
},
- noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
- showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
- scrollParent () { return window; /* this.$refs.appContentRef */ },
- ...mapGetters(['mergedConfig'])
+ 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,
+ }),
},
methods: {
- resizeHandler () {
+ resizeHandler() {
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
},
- scrollHandler () {
- const scrollPosition = this.scrollParent === window ? window.scrollY : this.scrollParent.scrollTop
+ scrollHandler() {
+ const scrollPosition =
+ this.scrollParent === window
+ ? window.scrollY
+ : this.scrollParent.scrollTop
if (scrollPosition != 0) {
this.$refs.appContentRef.classList.add(['-scrolled'])
@@ -189,10 +270,10 @@ export default {
this.$refs.appContentRef.classList.remove(['-scrolled'])
}
},
- setThemeBodyClass () {
+ setThemeBodyClass() {
const themeName = this.currentTheme
const classList = Array.from(document.body.classList)
- const oldTheme = classList.filter(c => c.startsWith('theme-'))
+ const oldTheme = classList.filter((c) => c.startsWith('theme-'))
if (themeName !== null && themeName !== '') {
const newTheme = `theme-${themeName.toLowerCase()}`
@@ -208,8 +289,10 @@ export default {
document.body.classList.remove(...oldTheme)
}
},
- removeSplash () {
- document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4))
+ removeSplash() {
+ document.querySelector('#status').textContent = this.$t(
+ 'splash.fun_' + Math.ceil(Math.random() * 4),
+ )
const splashscreenRoot = document.querySelector('#splash')
splashscreenRoot.addEventListener('transitionend', () => {
splashscreenRoot.remove()
@@ -219,6 +302,6 @@ export default {
}, 600)
splashscreenRoot.classList.add('hidden')
document.querySelector('#app').classList.remove('hidden')
- }
- }
+ },
+ },
}
diff --git a/src/App.scss b/src/App.scss
index ee1654bb7..952af5b47 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -50,7 +50,7 @@ body {
// have a cursor/pointer to operate them
@media (any-pointer: fine) {
* {
- scrollbar-color: var(--fg) transparent;
+ scrollbar-color: var(--icon) transparent;
&::-webkit-scrollbar {
background: transparent;
@@ -130,7 +130,7 @@ body {
}
// Body should have background to scrollbar otherwise it will use white (body color?)
html {
- scrollbar-color: var(--fg) var(--wallpaper);
+ scrollbar-color: var(--icon) var(--wallpaper);
background: var(--wallpaper);
}
}
@@ -200,6 +200,7 @@ nav {
background-color: var(--wallpaper);
background-image: var(--body-background-image);
background-position: 50%;
+ transition: background-image 1s;
}
.underlay {
@@ -513,6 +514,12 @@ nav {
}
}
+label {
+ &.-disabled {
+ color: var(--textFaint);
+ }
+}
+
input,
textarea {
border: none;
@@ -553,6 +560,10 @@ textarea {
&[disabled="disabled"],
&.disabled {
cursor: not-allowed;
+ color: var(--textFaint);
+
+ /* stylelint-disable-next-line declaration-no-important */
+ background-color: transparent !important;
}
&[type="range"] {
@@ -578,6 +589,8 @@ textarea {
& + label::before {
opacity: 0.5;
}
+
+ background-color: var(--background);
}
+ label::before {
@@ -677,7 +690,8 @@ option {
list-style: none;
display: grid;
grid-auto-flow: row dense;
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: repeat(auto-fit, minmax(20em, 1fr));
+ grid-gap: 0.5em;
li {
border: 1px solid var(--border);
@@ -698,7 +712,6 @@ option {
--_roundness-right: 0;
position: relative;
- flex: 1 1 auto;
}
> *:first-child,
@@ -775,6 +788,19 @@ 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 {
@@ -785,14 +811,19 @@ option {
}
.notice-dismissible {
- padding-right: 4rem;
- position: relative;
+ display: flex;
+ padding: 0.75em 1em;
+ align-items: baseline;
+ line-height: 1.5;
+
+ p,
+ span {
+ display: block;
+ flex: 1 1 auto;
+ margin: 0;
+ }
.dismiss {
- position: absolute;
- top: 0;
- right: 0;
- padding: 0.5em;
color: inherit;
}
}
diff --git a/src/App.vue b/src/App.vue
index e5e088bc3..c1a7199d7 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -28,10 +28,10 @@
>
-
-
-
-
+
+
+
+
@@ -60,8 +60,8 @@
/>
- {
return null
}
if (!staticInitialResults) {
- staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
+ staticInitialResults = JSON.parse(
+ document.getElementById('initial-results').textContent,
+ )
}
return staticInitialResults
}
@@ -54,7 +80,7 @@ const preloadFetch = async (request) => {
return {
ok: true,
json: () => requestData,
- text: () => requestData
+ text: () => requestData,
}
}
@@ -63,20 +89,38 @@ 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
- store.dispatch('setInstanceOption', { name: 'pleromaExtensionsAvailable', value: data.pleroma })
- store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
- store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
- store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma?.metadata.birthday_required })
- store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma?.metadata.birthday_min_age || 0 })
+ 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,
+ })
if (vapidPublicKey) {
- store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
+ useInstanceStore().set({
+ path: 'vapidPublicKey',
+ value: vapidPublicKey,
+ })
}
} else {
- throw (res)
+ throw res
}
} catch (error) {
console.error('Could not load instance config, potentially fatal')
@@ -93,10 +137,12 @@ const getBackendProvidedConfig = async () => {
const data = await res.json()
return data.pleroma_fe
} else {
- throw (res)
+ throw res
}
} catch (error) {
- console.error('Could not load backend-provided frontend config, potentially fatal')
+ console.error(
+ 'Could not load backend-provided frontend config, potentially fatal',
+ )
console.error(error)
}
}
@@ -107,11 +153,13 @@ 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.')
- console.warn(error)
+ console.warn(
+ 'Failed to load static/config.json, continuing without it.',
+ error,
+ )
return {}
}
}
@@ -129,51 +177,25 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
config = Object.assign({}, staticConfig, apiConfig)
}
- const copyInstanceOption = (name) => {
- store.dispatch('setInstanceOption', { name, value: config[name] })
- }
-
- copyInstanceOption('theme')
- copyInstanceOption('style')
- copyInstanceOption('palette')
- copyInstanceOption('embeddedToS')
- copyInstanceOption('nsfwCensorImage')
- copyInstanceOption('background')
- copyInstanceOption('hidePostStats')
- copyInstanceOption('hideBotIndication')
- copyInstanceOption('hideUserStats')
- copyInstanceOption('hideFilteredStatuses')
- copyInstanceOption('logo')
-
- store.dispatch('setInstanceOption', {
- name: 'logoMask',
- value: typeof config.logoMask === 'undefined'
- ? true
- : config.logoMask
+ 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}`,
+ })
})
- store.dispatch('setInstanceOption', {
- name: 'logoMargin',
- value: typeof config.logoMargin === 'undefined'
- ? 0
- : config.logoMargin
- })
- copyInstanceOption('logoLeft')
+ Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) =>
+ useInstanceStore().set({
+ value:
+ config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
+ path: `prefsStorage.${source}`,
+ }),
+ )
+
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 }) => {
@@ -181,9 +203,9 @@ const getTOS = async ({ store }) => {
const res = await window.fetch('/static/terms-of-service.html')
if (res.ok) {
const html = await res.text()
- store.dispatch('setInstanceOption', { name: 'tos', value: html })
+ useInstanceStore().set({ path: 'instanceIdentity.tos', value: html })
} else {
- throw (res)
+ throw res
}
} catch (e) {
console.warn("Can't load TOS\n", e)
@@ -195,9 +217,12 @@ const getInstancePanel = async ({ store }) => {
const res = await preloadFetch('/instance/panel.html')
if (res.ok) {
const html = await res.text()
- store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
+ useInstanceStore().set({
+ path: 'instanceIdentity.instanceSpecificPanelContent',
+ value: html,
+ })
} else {
- throw (res)
+ throw res
}
} catch (e) {
console.warn("Can't load instance panel\n", e)
@@ -209,25 +234,27 @@ 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)
})
- store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
+ useEmojiStore().setStickers(stickers)
} else {
- throw (res)
+ throw res
}
} catch (e) {
console.warn("Can't load stickers\n", e)
@@ -237,13 +264,19 @@ const getStickers = async ({ store }) => {
const getAppSecret = async ({ store }) => {
const oauth = useOAuthStore()
if (oauth.userToken) {
- store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
+ store.commit(
+ 'setBackendInteractor',
+ backendInteractorService(oauth.getToken),
+ )
}
}
const resolveStaffAccounts = ({ store, accounts }) => {
- const nicknames = accounts.map(uri => uri.split('/').pop())
- store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
+ const nicknames = accounts.map((uri) => uri.split('/').pop())
+ useInstanceStore().set({
+ path: 'staffAccounts',
+ value: nicknames,
+ })
}
const getNodeInfo = async ({ store }) => {
@@ -254,87 +287,177 @@ const getNodeInfo = async ({ store }) => {
const data = await res.json()
const metadata = data.metadata
const features = metadata.features
- store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
- store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
- store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
- store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
- store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
- store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
- store.dispatch('setInstanceOption', {
- name: 'pleromaCustomEmojiReactionsAvailable',
- value:
- features.includes('pleroma_custom_emoji_reactions') ||
- features.includes('custom_emoji_reactions')
+ useInstanceStore().set({
+ path: 'instanceIdentity.name',
+ value: metadata.nodeName,
})
- store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
- store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
- store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
- store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
- store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
- store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
- store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
- store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
- store.dispatch('setInstanceOption', { name: 'blockExpiration', value: features.includes('pleroma:block_expiration') })
- store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] })
+ 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',
+ 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
- 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: '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: 'restrictedNicknames', value: metadata.restrictedNicknames })
- store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
+ useInstanceStore().set({
+ path: 'restrictedNicknames',
+ value: metadata.restrictedNicknames,
+ })
+ useInstanceCapabilitiesStore().set('postFormats', metadata.postFormats)
const suggestions = metadata.suggestions
- store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
- store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
+ useInstanceCapabilitiesStore().set(
+ 'suggestionsEnabled',
+ suggestions.enabled,
+ )
+ // this is unused, why?
+ useInstanceCapabilitiesStore().set('suggestionsWeb', suggestions.web)
const software = data.software
- store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
- store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository })
+ useInstanceStore().set({
+ path: 'backendVersion',
+ value: software.version,
+ })
+ useInstanceStore().set({
+ path: 'backendRepository',
+ value: software.repository,
+ })
const priv = metadata.private
- store.dispatch('setInstanceOption', { name: 'private', value: priv })
+ useInstanceStore().set({ path: 'privateMode', value: priv })
const frontendVersion = window.___pleromafe_commit_hash
- store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
+ useInstanceStore().set({
+ path: 'frontendVersion',
+ value: frontendVersion,
+ })
const federation = metadata.federation
- store.dispatch('setInstanceOption', {
- name: 'tagPolicyAvailable',
- value: typeof federation.mrf_policies === 'undefined'
+ useInstanceCapabilitiesStore().set(
+ 'tagPolicyAvailable',
+ typeof federation.mrf_policies === 'undefined'
? false
- : metadata.federation.mrf_policies.includes('TagPolicy')
- })
+ : metadata.federation.mrf_policies.includes('TagPolicy'),
+ )
- store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
- store.dispatch('setInstanceOption', {
- name: 'federating',
- value: typeof federation.enabled === 'undefined'
- ? true
- : federation.enabled
+ useInstanceStore().set({
+ path: 'federationPolicy',
+ value: federation,
+ })
+ useInstanceStore().set({
+ path: 'federating',
+ value:
+ typeof federation.enabled === 'undefined' ? true : federation.enabled,
})
const accountActivationRequired = metadata.accountActivationRequired
- store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
+ useInstanceStore().set({
+ path: 'accountActivationRequired',
+ value: accountActivationRequired,
+ })
const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts })
} else {
- throw (res)
+ throw res
}
} catch (e) {
- console.warn('Could not load nodeinfo')
- console.warn(e)
+ console.warn('Could not load nodeinfo', 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]
@@ -365,29 +488,37 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
if (process.env.NODE_ENV === 'development') {
// do some checks to avoid common errors
if (!Object.keys(allStores).length) {
- throw new Error('No stores are available. Check the code in src/boot/after_store.js')
+ throw new Error(
+ 'No stores are available. Check the code in src/boot/after_store.js',
+ )
}
}
await Promise.all(
- Object.entries(allStores)
- .map(async ([name, mod]) => {
- const isStoreName = name => name.startsWith('use')
- if (process.env.NODE_ENV === 'development') {
- if (Object.keys(mod).filter(isStoreName).length !== 1) {
- throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/')
- }
+ Object.entries(allStores).map(async ([name, mod]) => {
+ const isStoreName = (name) => name.startsWith('use')
+ if (process.env.NODE_ENV === 'development') {
+ if (Object.keys(mod).filter(isStoreName).length !== 1) {
+ throw new Error(
+ 'Each store file must export exactly one store as a named export. Check your code in src/stores/',
+ )
}
- const storeFuncName = Object.keys(mod).find(isStoreName)
- if (storeFuncName && typeof mod[storeFuncName] === 'function') {
- const p = mod[storeFuncName]().$persistLoaded
- if (!(p instanceof Promise)) {
- throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`)
- }
- await p
- } else {
- throw new Error(`Store module ${name} does not export a 'use...' function`)
+ }
+ 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 {
@@ -398,30 +529,45 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
}
if (storageError) {
- useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' })
+ useInterfaceStore().pushGlobalNotice({
+ messageKey: 'errors.storage_unavailable',
+ level: 'error',
+ })
}
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
+ window.syncConfig = useSyncConfigStore()
+ window.mergedConfig = useMergedConfigStore()
+ window.localConfig = useLocalConfigStore()
+ window.highlightConfig = useUserHighlightStore()
+
FaviconService.initFaviconService()
initServiceWorker(store)
window.addEventListener('focus', () => updateFocus())
const overrides = window.___pleromafe_dev_overrides || {}
- const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
- store.dispatch('setInstanceOption', { name: 'server', value: server })
+ const server =
+ typeof overrides.target !== 'undefined'
+ ? overrides.target
+ : window.location.origin
+ useInstanceStore().set({ path: 'server', value: server })
await setConfig({ store })
try {
- await useInterfaceStore().applyTheme().catch((e) => { console.error('Error setting theme', e) })
+ await useInterfaceStore()
+ .applyTheme()
+ .catch((e) => {
+ console.error('Error setting theme', e)
+ })
} catch (e) {
window.splashError(e)
return Promise.reject(e)
}
- applyConfig(store.state.config, i18n.global)
+ applyStyleConfig(useMergedConfigStore().mergedConfig, i18n.global)
// Now we can try getting the server settings and logging in
// Most of these are preloaded into the index.html so blocking is minimized
@@ -429,8 +575,8 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
checkOAuthToken({ store }),
getInstancePanel({ store }),
getNodeInfo({ store }),
- getInstanceConfig({ store })
- ]).catch(e => Promise.reject(e))
+ getInstanceConfig({ store }),
+ ]).catch((e) => Promise.reject(e))
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
@@ -443,11 +589,11 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
history: createWebHistory(),
routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => {
- if (to.matched.some(m => m.meta.dontScroll)) {
+ if (to.matched.some((m) => m.meta.dontScroll)) {
return false
}
return savedPosition || { left: 0, top: 0 }
- }
+ },
})
useI18nStore().setI18n(i18n)
@@ -469,6 +615,9 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
app.component('FAIcon', FontAwesomeIcon)
app.component('FALayers', FontAwesomeLayers)
+ app.component('Status', Status)
+ app.component('RichContent', RichContent)
+ app.component('StillImage', StillImage)
// remove after vue 3.3
app.config.unwrapInjectedRef = true
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 02abf8ce6..4a075e955 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -1,42 +1,28 @@
-import PublicTimeline from 'components/public_timeline/public_timeline.vue'
-import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
-import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
-import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
-import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
-import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
-import ConversationPage from 'components/conversation-page/conversation-page.vue'
-import Interactions from 'components/interactions/interactions.vue'
-import DMs from 'components/dm_timeline/dm_timeline.vue'
-import ChatList from 'components/chat_list/chat_list.vue'
-import Chat from 'components/chat/chat.vue'
-import UserProfile from 'components/user_profile/user_profile.vue'
-import Search from 'components/search/search.vue'
-import Registration from 'components/registration/registration.vue'
-import PasswordReset from 'components/password_reset/password_reset.vue'
-import FollowRequests from 'components/follow_requests/follow_requests.vue'
-import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
-import Notifications from 'components/notifications/notifications.vue'
-import AuthForm from 'components/auth_form/auth_form.js'
-import ShoutPanel from 'components/shout_panel/shout_panel.vue'
-import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
-import About from 'components/about/about.vue'
-import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
-import Lists from 'components/lists/lists.vue'
-import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
-import ListsEdit from 'components/lists_edit/lists_edit.vue'
+import { defineAsyncComponent } from 'vue'
+
+import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue'
+import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue'
+import ConversationPage from 'src/components/conversation-page/conversation-page.vue'
+import DMs from 'src/components/dm_timeline/dm_timeline.vue'
+import FriendsTimeline from 'src/components/friends_timeline/friends_timeline.vue'
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
-import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
-import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
-import Drafts from 'components/drafts/drafts.vue'
-import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
-import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
+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'
export default (store) => {
const validateAuthenticatedRoute = (to, from, next) => {
if (store.state.users.currentUser) {
next()
} else {
- next(store.state.instance.redirectRootNoLogin || '/main/all')
+ next(
+ useInstanceStore().instanceIdentity.redirectRootNoLogin || '/main/all',
+ )
}
}
@@ -45,64 +31,276 @@ export default (store) => {
name: 'root',
path: '/',
redirect: () => {
- return (store.state.users.currentUser
- ? store.state.instance.redirectRootLogin
- : store.state.instance.redirectRootNoLogin) || '/main/all'
- }
+ 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,
},
- { 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
+ beforeEnter: validateAuthenticatedRoute,
+ },
+ {
+ name: 'external-user-profile',
+ path: '/users/$:id',
+ component: defineAsyncComponent(
+ () => import('src/components/user_profile/user_profile.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: defineAsyncComponent(
+ () => import('src/components/auth_form/auth_form.js'),
+ ),
+ },
+ {
+ 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'
+ ),
+ ),
},
- { 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: 'drafts', path: '/drafts', component: Drafts },
- { name: 'user-profile', path: '/users/:name', component: UserProfile },
- { name: 'legacy-user-profile', path: '/:name', component: UserProfile },
- { name: 'lists', path: '/lists', component: Lists },
- { name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
- { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
- { name: 'lists-new', path: '/lists/new', component: ListsEdit },
- { name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute },
- { name: 'bookmark-folders', path: '/bookmark_folders', component: BookmarkFolders },
- { name: 'bookmark-folder-new', path: '/bookmarks/new-folder', component: BookmarkFolderEdit },
- { name: 'bookmark-folder', path: '/bookmarks/:id', component: BookmarkTimeline },
- { name: 'bookmark-folder-edit', path: '/bookmarks/:id/edit', component: BookmarkFolderEdit }
]
- if (store.state.instance.pleromaChatMessagesAvailable) {
+ if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
routes = routes.concat([
- { name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
- { name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
+ {
+ name: 'chat',
+ path: '/users/:username/chats/:recipient_id',
+ component: 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,
+ },
])
}
diff --git a/src/components/about/about.js b/src/components/about/about.js
index 1df258450..e293895d5 100644
--- a/src/components/about/about.js
+++ b/src/components/about/about.js
@@ -1,8 +1,16 @@
-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'
+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/'
const About = {
components: {
@@ -10,16 +18,28 @@ const About = {
FeaturesPanel,
TermsOfServicePanel,
StaffPanel,
- MRFTransparencyPanel
+ MRFTransparencyPanel,
},
computed: {
- showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
- showInstanceSpecificPanel () {
- return this.$store.state.instance.showInstanceSpecificPanel &&
- !this.$store.getters.mergedConfig.hideISP &&
- this.$store.state.instance.instanceSpecificPanelContent
- }
- }
+ 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
+ )
+ },
+ },
}
export default About
diff --git a/src/components/about/about.vue b/src/components/about/about.vue
index 8a551485f..f7bc0d00d 100644
--- a/src/components/about/about.vue
+++ b/src/components/about/about.vue
@@ -1,11 +1,47 @@
-
+
+
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 2ac74ea76..9b4775b97 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -1,53 +1,58 @@
-import { mapState } from 'vuex'
-import ProgressButton from '../progress_button/progress_button.vue'
-import Popover from '../popover/popover.vue'
+import { mapState } from 'pinia'
+import { defineAsyncComponent } from 'vue'
+
+import Popover from 'src/components/popover/popover.vue'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
-import ConfirmModal from '../confirm_modal/confirm_modal.vue'
-import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faEllipsisV
-} from '@fortawesome/free-solid-svg-icons'
+
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useReportsStore } from 'src/stores/reports'
-library.add(
- faEllipsisV
-)
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faEllipsisV)
const AccountActions = {
- props: [
- 'user', 'relationship'
- ],
- data () {
+ props: ['user', 'relationship'],
+ data() {
return {
showingConfirmBlock: false,
- showingConfirmRemoveFollower: false
+ showingConfirmRemoveFollower: false,
}
},
components: {
ProgressButton,
Popover,
UserListMenu,
- ConfirmModal,
- UserTimedFilterModal
+ ConfirmModal: defineAsyncComponent(
+ () => import('src/components/confirm_modal/confirm_modal.vue'),
+ ),
+ UserTimedFilterModal: defineAsyncComponent(
+ () =>
+ import(
+ 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
+ ),
+ ),
},
methods: {
- showConfirmRemoveUserFromFollowers () {
+ showConfirmRemoveUserFromFollowers() {
this.showingConfirmRemoveFollower = true
},
- hideConfirmRemoveUserFromFollowers () {
+ hideConfirmRemoveUserFromFollowers() {
this.showingConfirmRemoveFollower = false
},
- hideConfirmBlock () {
+ hideConfirmBlock() {
this.showingConfirmBlock = false
},
- showRepeats () {
+ showRepeats() {
this.$store.dispatch('showReblogs', this.user.id)
},
- hideRepeats () {
+ hideRepeats() {
this.$store.dispatch('hideReblogs', this.user.id)
},
- blockUser () {
+ blockUser() {
if (this.$refs.timedBlockDialog) {
this.$refs.timedBlockDialog.optionallyPrompt()
} else {
@@ -58,46 +63,49 @@ const AccountActions = {
}
}
},
- doBlockUser () {
+ doBlockUser() {
this.$store.dispatch('blockUser', { id: this.user.id })
this.hideConfirmBlock()
},
- unblockUser () {
+ unblockUser() {
this.$store.dispatch('unblockUser', this.user.id)
},
- removeUserFromFollowers () {
+ removeUserFromFollowers() {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
- doRemoveUserFromFollowers () {
+ doRemoveUserFromFollowers() {
this.$store.dispatch('removeUserFromFollowers', this.user.id)
this.hideConfirmRemoveUserFromFollowers()
},
- reportUser () {
+ reportUser() {
useReportsStore().openUserReportingModal({ userId: this.user.id })
},
- openChat () {
+ openChat() {
this.$router.push({
name: 'chat',
- params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
+ params: {
+ username: this.$store.state.users.currentUser.screen_name,
+ recipient_id: this.user.id,
+ },
})
- }
+ },
},
computed: {
- shouldConfirmBlock () {
- return this.$store.getters.mergedConfig.modalOnBlock
+ shouldConfirmBlock() {
+ return useMergedConfigStore().mergedConfig.modalOnBlock
},
- shouldConfirmRemoveUserFromFollowers () {
- return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
+ shouldConfirmRemoveUserFromFollowers() {
+ return useMergedConfigStore().mergedConfig.modalOnRemoveUserFromFollowers
},
- ...mapState({
- blockExpirationSupported: state => state.instance.blockExpiration,
- pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
- })
- }
+ ...mapState(useInstanceCapabilitiesStore, [
+ 'blockExpiration',
+ 'pleromaChatMessagesAvailable',
+ ]),
+ },
}
export default AccountActions
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index cc00a735d..0c93872de 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -94,8 +94,8 @@
-
-
+
-
-
+
state.users.currentUser
+ currentUser: (state) => state.users.currentUser,
}),
- canEditAnnouncement () {
- return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
+ canEditAnnouncement() {
+ return (
+ this.currentUser &&
+ this.currentUser.privileges.includes(
+ 'announcements_manage_announcements',
+ )
+ )
},
- content () {
+ content() {
return this.announcement.content
},
- isRead () {
+ isRead() {
return this.announcement.read
},
- publishedAt () {
+ publishedAt() {
const time = this.announcement.published_at
if (!time) {
return
}
- return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+ return this.formatTimeOrDate(
+ time,
+ localeService.internalToBrowserLocale(this.$i18n.locale),
+ )
},
- startsAt () {
+ startsAt() {
const time = this.announcement.starts_at
if (!time) {
return
}
- return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+ return this.formatTimeOrDate(
+ time,
+ localeService.internalToBrowserLocale(this.$i18n.locale),
+ )
},
- endsAt () {
+ endsAt() {
const time = this.announcement.ends_at
if (!time) {
return
}
- return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+ return this.formatTimeOrDate(
+ time,
+ localeService.internalToBrowserLocale(this.$i18n.locale),
+ )
},
- inactive () {
+ inactive() {
return this.announcement.inactive
- }
+ },
},
methods: {
- markAsRead () {
+ markAsRead() {
if (!this.isRead) {
- return useAnnouncementsStore().markAnnouncementAsRead(this.announcement.id)
+ return useAnnouncementsStore().markAnnouncementAsRead(
+ this.announcement.id,
+ )
}
},
- deleteAnnouncement () {
+ deleteAnnouncement() {
return useAnnouncementsStore().deleteAnnouncement(this.announcement.id)
},
- formatTimeOrDate (time, locale) {
+ formatTimeOrDate(time, locale) {
const d = new Date(time)
- return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
+ return this.announcement.all_day
+ ? d.toLocaleDateString(locale)
+ : d.toLocaleString(locale)
},
- enterEditMode () {
+ enterEditMode() {
this.editedAnnouncement.content = this.announcement.pleroma.raw_content
this.editedAnnouncement.startsAt = this.announcement.starts_at
this.editedAnnouncement.endsAt = this.announcement.ends_at
this.editedAnnouncement.allDay = this.announcement.all_day
this.editing = true
},
- submitEdit () {
- useAnnouncementsStore().editAnnouncement({
- id: this.announcement.id,
- ...this.editedAnnouncement
- })
+ submitEdit() {
+ useAnnouncementsStore()
+ .editAnnouncement({
+ id: this.announcement.id,
+ ...this.editedAnnouncement,
+ })
.then(() => {
this.editing = false
})
- .catch(error => {
+ .catch((error) => {
this.editError = error.error
})
},
- cancelEdit () {
+ cancelEdit() {
this.editing = false
},
- clearError () {
+ clearError() {
this.editError = undefined
- }
- }
+ },
+ },
}
export default Announcement
diff --git a/src/components/announcement_editor/announcement_editor.js b/src/components/announcement_editor/announcement_editor.js
index 79a03afe1..c816083a4 100644
--- a/src/components/announcement_editor/announcement_editor.js
+++ b/src/components/announcement_editor/announcement_editor.js
@@ -1,13 +1,13 @@
-import Checkbox from '../checkbox/checkbox.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
const AnnouncementEditor = {
components: {
- Checkbox
+ Checkbox,
},
props: {
announcement: Object,
- disabled: Boolean
- }
+ disabled: Boolean,
+ },
}
export default AnnouncementEditor
diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js
index 9ce0b45f5..3c73198c5 100644
--- a/src/components/announcements_page/announcements_page.js
+++ b/src/components/announcements_page/announcements_page.js
@@ -1,59 +1,67 @@
import { mapState } from 'vuex'
-import Announcement from '../announcement/announcement.vue'
-import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
-import { useAnnouncementsStore } from 'src/stores/announcements'
+
+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'
const AnnouncementsPage = {
components: {
Announcement,
- AnnouncementEditor
+ AnnouncementEditor,
},
- data () {
+ data() {
return {
newAnnouncement: {
content: '',
startsAt: undefined,
endsAt: undefined,
- allDay: false
+ allDay: false,
},
posting: false,
- error: undefined
+ error: undefined,
}
},
- mounted () {
+ mounted() {
useAnnouncementsStore().fetchAnnouncements()
},
computed: {
...mapState({
- currentUser: state => state.users.currentUser
+ currentUser: (state) => state.users.currentUser,
}),
- announcements () {
+ announcements() {
return useAnnouncementsStore().announcements
},
- canPostAnnouncement () {
- return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
- }
+ canPostAnnouncement() {
+ return (
+ this.currentUser &&
+ this.currentUser.privileges.includes(
+ 'announcements_manage_announcements',
+ )
+ )
+ },
},
methods: {
- postAnnouncement () {
+ postAnnouncement() {
this.posting = true
- useAnnouncementsStore().postAnnouncement(this.newAnnouncement)
+ useAnnouncementsStore()
+ .postAnnouncement(this.newAnnouncement)
.then(() => {
this.newAnnouncement.content = ''
this.startsAt = undefined
this.endsAt = undefined
})
- .catch(error => {
+ .catch((error) => {
this.error = error.error
})
.finally(() => {
this.posting = false
})
},
- clearError () {
+ clearError() {
this.error = undefined
- }
- }
+ },
+ },
}
export default AnnouncementsPage
diff --git a/src/components/async_component_error/async_component_error.vue b/src/components/async_component_error/async_component_error.vue
index 2ff8974c1..baf430950 100644
--- a/src/components/async_component_error/async_component_error.vue
+++ b/src/components/async_component_error/async_component_error.vue
@@ -21,10 +21,10 @@
export default {
emits: ['resetAsyncComponent'],
methods: {
- retry () {
+ retry() {
this.$emit('resetAsyncComponent')
- }
- }
+ },
+ },
}
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 21d793930..4b733258f 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -1,24 +1,29 @@
-import StillImage from '../still-image/still-image.vue'
-import Flash from '../flash/flash.vue'
-import VideoAttachment from '../video_attachment/video_attachment.vue'
+import { mapState } from 'pinia'
+import { defineAsyncComponent } from 'vue'
+
+import Popover from 'src/components/popover/popover.vue'
+import VideoAttachment from 'src/components/video_attachment/video_attachment.vue'
import nsfwImage from '../../assets/nsfw.png'
-import fileTypeService from '../../services/file_type/file_type.service.js'
-import { mapGetters } from 'vuex'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useMediaViewerStore } from 'src/stores/media_viewer'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
import { library } from '@fortawesome/fontawesome-svg-core'
import {
+ faAlignRight,
faFile,
- faMusic,
faImage,
- faVideo,
- faPlayCircle,
- faTimes,
- faStop,
- faSearchPlus,
- faTrashAlt,
+ faMusic,
faPencilAlt,
- faAlignRight
+ faPlayCircle,
+ faSearchPlus,
+ faStop,
+ faTimes,
+ faTrashAlt,
+ faVideo,
} from '@fortawesome/free-solid-svg-icons'
-import { useMediaViewerStore } from 'src/stores/media_viewer'
library.add(
faFile,
@@ -31,7 +36,7 @@ library.add(
faSearchPlus,
faTrashAlt,
faPencilAlt,
- faAlignRight
+ faAlignRight,
)
const Attachment = {
@@ -46,72 +51,74 @@ const Attachment = {
'remove',
'shiftUp',
'shiftDn',
- 'edit'
+ 'edit',
],
- data () {
+ data() {
return {
localDescription: this.description || this.attachment.description,
- nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
- hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
- preloadImage: this.$store.getters.mergedConfig.preloadImage,
+ nsfwImage:
+ useInstanceStore().instanceIdentity.nsfwCensorImage || nsfwImage,
+ hideNsfwLocal: useMergedConfigStore().mergedConfig.hideNsfw,
+ preloadImage: useMergedConfigStore().mergedConfig.preloadImage,
loading: false,
- img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
+ img: this.attachment.type === 'image' && document.createElement('img'),
modalOpen: false,
showHidden: false,
flashLoaded: false,
- showDescription: false
}
},
components: {
- Flash,
- StillImage,
- VideoAttachment
+ Flash: defineAsyncComponent(() => import('src/components/flash/flash.vue')),
+
+ VideoAttachment: defineAsyncComponent(
+ () => import('src/components/video_attachment/video_attachment.vue'),
+ ),
+ Popover,
},
computed: {
- classNames () {
+ classNames() {
return [
{
'-loading': this.loading,
'-nsfw-placeholder': this.hidden,
'-editable': this.edit !== undefined,
- '-compact': this.compact
+ '-compact': this.compact,
},
- '-type-' + this.type,
+ '-type-' + this.attachment.type,
this.size && '-size-' + this.size,
- `-${this.useContainFit ? 'contain' : 'cover'}-fit`
+ `-${this.useContainFit ? 'contain' : 'cover'}-fit`,
]
},
- usePlaceholder () {
+ usePlaceholder() {
return this.size === 'hide'
},
- useContainFit () {
- return this.$store.getters.mergedConfig.useContainFit
+ useContainFit() {
+ return this.mergedConfig.useContainFit
},
- placeholderName () {
+ placeholderName() {
if (this.attachment.description === '' || !this.attachment.description) {
- return this.type.toUpperCase()
+ return this.attachment.type.toUpperCase()
}
return this.attachment.description
},
- placeholderIconClass () {
- if (this.type === 'image') return 'image'
- if (this.type === 'video') return 'video'
- if (this.type === 'audio') return 'music'
+ placeholderIconClass() {
+ if (this.attachment.type === 'image') return 'image'
+ if (this.attachment.type === 'video') return 'video'
+ if (this.attachment.type === 'audio') return 'music'
return 'file'
},
- referrerpolicy () {
- return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
+ referrerpolicy() {
+ return useInstanceCapabilitiesStore().mediaProxyAvailable
+ ? ''
+ : 'no-referrer'
},
- type () {
- return fileTypeService.fileType(this.attachment.mimetype)
- },
- hidden () {
+ hidden() {
return this.nsfw && this.hideNsfwLocal && !this.showHidden
},
- isEmpty () {
- return (this.type === 'html' && !this.attachment.oembed)
+ isEmpty() {
+ return this.attachment.type === 'html' && !this.attachment.oembed
},
- useModal () {
+ useModal() {
let modalTypes = []
switch (this.size) {
case 'hide':
@@ -124,64 +131,63 @@ const Attachment = {
: ['image']
break
}
- return modalTypes.includes(this.type)
+ return modalTypes.includes(this.attachment.type)
},
- videoTag () {
+ videoTag() {
return this.useModal ? 'button' : 'span'
},
- ...mapGetters(['mergedConfig'])
+ ...mapState(useMergedConfigStore, ['mergedConfig']),
},
watch: {
- 'attachment.description' (newVal) {
+ 'attachment.description'(newVal) {
this.localDescription = newVal
},
- localDescription (newVal) {
+ localDescription(newVal) {
this.onEdit(newVal)
- }
+ },
},
methods: {
- linkClicked ({ target }) {
+ linkClicked({ target }) {
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
- openModal () {
+ openModal() {
if (this.useModal) {
this.$emit('setMedia')
useMediaViewerStore().setCurrentMedia(this.attachment)
- } else if (this.type === 'unknown') {
+ } else if (this.attachment.type === 'unknown') {
window.open(this.attachment.url)
}
},
- openModalForce () {
+ openModalForce() {
this.$emit('setMedia')
useMediaViewerStore().setCurrentMedia(this.attachment)
},
- onEdit (event) {
+ onEdit(event) {
this.edit && this.edit(this.attachment, event)
},
- onRemove () {
+ onRemove() {
this.remove && this.remove(this.attachment)
},
- onShiftUp () {
+ onShiftUp() {
this.shiftUp && this.shiftUp(this.attachment)
},
- onShiftDn () {
+ onShiftDn() {
this.shiftDn && this.shiftDn(this.attachment)
},
- stopFlash () {
+ stopFlash() {
this.$refs.flash.closePlayer()
},
- setFlashLoaded (event) {
+ setFlashLoaded(event) {
this.flashLoaded = event
},
- toggleDescription () {
- this.showDescription = !this.showDescription
- },
- toggleHidden (event) {
+ toggleHidden(event) {
if (
- (this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
- (this.type !== 'video' || this.mergedConfig.playVideosInModal)
+ this.mergedConfig.useOneClickNsfw &&
+ !this.showHidden &&
+ (this.attachment.type !== 'video' ||
+ this.mergedConfig.playVideosInModal)
) {
this.openModal(event)
return
@@ -201,12 +207,12 @@ const Attachment = {
this.showHidden = !this.showHidden
}
},
- onImageLoad (image) {
+ onImageLoad(image) {
const width = image.naturalWidth
const height = image.naturalHeight
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
- }
- }
+ },
+ },
}
export default Attachment
diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss
index 97515eb32..3c3ba7751 100644
--- a/src/components/attachment/attachment.scss
+++ b/src/components/attachment/attachment.scss
@@ -134,7 +134,7 @@
width: 2em;
height: 2em;
margin-left: 0.5em;
- font-size: 1.25em;
+ font-size: 1em;
}
}
@@ -265,3 +265,27 @@
}
}
}
+
+.description-popover {
+ padding: 1em;
+ width: 50ch;
+ max-width: 90vw;
+ overflow: hidden;
+ box-sizing: border-box;
+
+ summary {
+ display: inline-block;
+ margin-bottom: 0.5em;
+ font-weight: bold;
+ pointer-events: none;
+ }
+
+ span {
+ display: block;
+ overflow-y: auto;
+ max-height: 10.5em;
+ text-wrap: pretty;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ }
+}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 696d3bb8a..1a1b84628 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -6,7 +6,7 @@
@click="openModal"
>
-
-
- {{ localDescription }}
-
+ />
@@ -80,23 +75,31 @@
class="attachment-buttons"
>
-
-
-
+
+
+
+
+
+ {{ $t('status.attachment_description') }}
+ {{ localDescription }}
+
+
+
@@ -229,7 +232,7 @@
-
-
- {{ localDescription }}
-
+ />
diff --git a/src/components/auth_form/auth_form.js b/src/components/auth_form/auth_form.js
index 243cbf574..7d853d119 100644
--- a/src/components/auth_form/auth_form.js
+++ b/src/components/auth_form/auth_form.js
@@ -1,28 +1,34 @@
-import { h, resolveComponent } from 'vue'
-import LoginForm from '../login_form/login_form.vue'
-import MFARecoveryForm from '../mfa_form/recovery_form.vue'
-import MFATOTPForm from '../mfa_form/totp_form.vue'
import { mapState } from 'pinia'
-import { useAuthFlowStore } from 'src/stores/auth_flow'
+import { h, resolveComponent } from 'vue'
+
+import LoginForm from 'src/components/login_form/login_form.vue'
+import MFARecoveryForm from 'src/components/mfa_form/recovery_form.vue'
+import MFATOTPForm from 'src/components/mfa_form/totp_form.vue'
+
+import { useAuthFlowStore } from 'src/stores/auth_flow.js'
const AuthForm = {
name: 'AuthForm',
- render () {
+ render() {
return h(resolveComponent(this.authForm))
},
computed: {
- authForm () {
- if (this.requiredTOTP) { return 'MFATOTPForm' }
- if (this.requiredRecovery) { return 'MFARecoveryForm' }
+ authForm() {
+ if (this.requiredTOTP) {
+ return 'MFATOTPForm'
+ }
+ if (this.requiredRecovery) {
+ return 'MFARecoveryForm'
+ }
return 'LoginForm'
},
- ...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery'])
+ ...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery']),
},
components: {
MFARecoveryForm,
MFATOTPForm,
- LoginForm
- }
+ LoginForm,
+ },
}
export default AuthForm
diff --git a/src/components/autosuggest/autosuggest.js b/src/components/autosuggest/autosuggest.js
index f58f17bb6..37b7706db 100644
--- a/src/components/autosuggest/autosuggest.js
+++ b/src/components/autosuggest/autosuggest.js
@@ -2,51 +2,55 @@ const debounceMilliseconds = 500
export default {
props: {
- query: { // function to query results and return a promise
+ query: {
+ // function to query results and return a promise
type: Function,
- required: true
+ required: true,
},
- filter: { // function to filter results in real time
- type: Function
+ filter: {
+ // function to filter results in real time
+ type: Function,
},
placeholder: {
type: String,
- default: 'Search...'
- }
+ default: 'Search...',
+ },
},
- data () {
+ data() {
return {
term: '',
timeout: null,
results: [],
- resultsVisible: false
+ resultsVisible: false,
}
},
computed: {
- filtered () {
+ filtered() {
return this.filter ? this.filter(this.results) : this.results
- }
+ },
},
watch: {
- term (val) {
+ term(val) {
this.fetchResults(val)
- }
+ },
},
methods: {
- fetchResults (term) {
+ fetchResults(term) {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.results = []
if (term) {
- this.query(term).then((results) => { this.results = results })
+ this.query(term).then((results) => {
+ this.results = results
+ })
}
}, debounceMilliseconds)
},
- onInputClick () {
+ onInputClick() {
this.resultsVisible = true
},
- onClickOutside () {
+ onClickOutside() {
this.resultsVisible = false
- }
- }
+ },
+ },
}
diff --git a/src/components/avatar_list/avatar_list.js b/src/components/avatar_list/avatar_list.js
index 9b6301b22..7806fba81 100644
--- a/src/components/avatar_list/avatar_list.js
+++ b/src/components/avatar_list/avatar_list.js
@@ -1,21 +1,28 @@
-import UserAvatar from '../user_avatar/user_avatar.vue'
+import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const AvatarList = {
props: ['users'],
computed: {
- slicedUsers () {
+ slicedUsers() {
return this.users ? this.users.slice(0, 15) : []
- }
+ },
},
components: {
- UserAvatar
+ UserAvatar,
},
methods: {
- userProfileLink (user) {
- return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
- }
- }
+ userProfileLink(user) {
+ return generateProfileLink(
+ user.id,
+ user.screen_name,
+ useInstanceStore().restrictedNicknames,
+ )
+ },
+ },
}
export default AvatarList
diff --git a/src/components/badge.style.js b/src/components/badge.style.js
index 0697cac6c..ab28429cc 100644
--- a/src/components/badge.style.js
+++ b/src/components/badge.style.js
@@ -1,30 +1,27 @@
export default {
name: 'Badge',
selector: '.badge',
- validInnerComponents: [
- 'Text',
- 'Icon'
- ],
+ validInnerComponents: ['Text', 'Icon'],
variants: {
- notification: '.-notification'
+ notification: '.-notification',
},
defaultRules: [
{
component: 'Root',
directives: {
- '--badgeNotification': 'color | --cRed'
- }
+ '--badgeNotification': 'color | --cRed',
+ },
},
{
directives: {
- background: '--cGreen'
- }
+ background: '--cGreen',
+ },
},
{
variant: 'notification',
directives: {
- background: '--cRed'
- }
- }
- ]
+ background: '--cRed',
+ },
+ },
+ ],
}
diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js
index 31de2d75f..1c69f81b4 100644
--- a/src/components/basic_user_card/basic_user_card.js
+++ b/src/components/basic_user_card/basic_user_card.js
@@ -1,24 +1,34 @@
-import UserPopover from '../user_popover/user_popover.vue'
-import UserAvatar from '../user_avatar/user_avatar.vue'
-import UserLink from '../user_link/user_link.vue'
-import RichContent from 'src/components/rich_content/rich_content.jsx'
+import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
+import UserLink from 'src/components/user_link/user_link.vue'
+import UserPopover from 'src/components/user_popover/user_popover.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = {
- props: [
- 'user'
- ],
+ props: ['user'],
components: {
UserPopover,
UserAvatar,
- RichContent,
- UserLink
+
+ UserLink,
},
methods: {
- userProfileLink (user) {
- return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
- }
- }
+ userProfileLink(user) {
+ return generateProfileLink(
+ user.id,
+ user.screen_name,
+ useInstanceStore().restrictedNicknames,
+ )
+ },
+ },
+ computed: {
+ allowNonSquareEmoji() {
+ return useMergedConfigStore().mergedConfig.nonSquareEmoji
+ },
+ },
}
export default BasicUserCard
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 29a60cd1e..572b07abb 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -27,6 +27,7 @@
class="basic-user-card-user-name-value"
:html="user.name"
:emoji="user.emoji"
+ :allow-non-square-emoji="allowNonSquareEmoji"
/>
diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js
index 9a618db3f..6484accff 100644
--- a/src/components/block_card/block_card.js
+++ b/src/components/block_card/block_card.js
@@ -1,46 +1,50 @@
-import { mapState } from 'vuex'
+import { mapState } from 'pinia'
-import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
+import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
+
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
const BlockCard = {
props: ['userId'],
computed: {
- user () {
+ user() {
return this.$store.getters.findUser(this.userId)
},
- relationship () {
+ relationship() {
return this.$store.getters.relationship(this.userId)
},
- blocked () {
+ blocked() {
return this.relationship.blocking
},
- blockExpiryAvailable () {
- return this.user.block_expires_at !== undefined
+ blockExpiryAvailable() {
+ return Object.hasOwn(this.user, 'block_expires_at')
},
- blockExpiry () {
- return this.user.block_expires_at == null
+ blockExpiry() {
+ return this.user.block_expires_at === false
? this.$t('user_card.block_expires_forever')
- : this.$t('user_card.block_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
+ : this.$t('user_card.block_expires_at', [
+ new Date(this.user.mute_expires_at).toLocaleString(),
+ ])
},
- ...mapState({
- blockExpirationSupported: state => state.instance.blockExpiration,
- })
+ ...mapState(useInstanceCapabilitiesStore, ['blockExpiration']),
},
components: {
- BasicUserCard
+ BasicUserCard,
+ UserTimedFilterModal,
},
methods: {
- unblockUser () {
+ unblockUser() {
this.$store.dispatch('unblockUser', this.user.id)
},
- blockUser () {
- if (this.blockExpirationSupported) {
+ blockUser() {
+ if (this.blockExpiration) {
this.$refs.timedBlockDialog.optionallyPrompt()
} else {
this.$store.dispatch('blockUser', { id: this.user.id })
}
- }
- }
+ },
+ },
}
export default BlockCard
diff --git a/src/components/bookmark_folder_card/bookmark_folder_card.js b/src/components/bookmark_folder_card/bookmark_folder_card.js
index bf274d9d7..37b3f2e5e 100644
--- a/src/components/bookmark_folder_card/bookmark_folder_card.js
+++ b/src/components/bookmark_folder_card/bookmark_folder_card.js
@@ -1,22 +1,15 @@
import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faEllipsisH
-} from '@fortawesome/free-solid-svg-icons'
+import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
-library.add(
- faEllipsisH
-)
+library.add(faEllipsisH)
const BookmarkFolderCard = {
- props: [
- 'folder',
- 'allBookmarks'
- ],
+ props: ['folder', 'allBookmarks'],
computed: {
- firstLetter () {
+ firstLetter() {
return this.folder ? this.folder.name[0] : null
- }
- }
+ },
+ },
}
export default BookmarkFolderCard
diff --git a/src/components/bookmark_folder_edit/bookmark_folder_edit.js b/src/components/bookmark_folder_edit/bookmark_folder_edit.js
index bb96585bf..43aa239b2 100644
--- a/src/components/bookmark_folder_edit/bookmark_folder_edit.js
+++ b/src/components/bookmark_folder_edit/bookmark_folder_edit.js
@@ -1,10 +1,11 @@
-import EmojiPicker from '../emoji_picker/emoji_picker.vue'
+import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue'
import apiService from '../../services/api/api.service'
-import { useInterfaceStore } from 'src/stores/interface'
-import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
+
+import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
+import { useInterfaceStore } from 'src/stores/interface.js'
const BookmarkFolderEdit = {
- data () {
+ data() {
return {
name: '',
nameDraft: '',
@@ -13,54 +14,59 @@ const BookmarkFolderEdit = {
emojiDraft: '',
emojiUrlDraft: null,
emojiPickerExpanded: false,
- reallyDelete: false
+ reallyDelete: false,
}
},
components: {
- EmojiPicker
+ EmojiPicker,
},
- created () {
+ created() {
if (!this.id) return
const credentials = this.$store.state.users.currentUser.credentials
- apiService.fetchBookmarkFolders({ credentials })
- .then((folders) => {
- const folder = folders.find(folder => folder.id === this.id)
- if (!folder) return
+ apiService.fetchBookmarkFolders({ credentials }).then((folders) => {
+ const folder = folders.find((folder) => folder.id === this.id)
+ if (!folder) return
- this.nameDraft = this.name = folder.name
- this.emojiDraft = this.emoji = folder.emoji
- this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
- })
+ this.nameDraft = this.name = folder.name
+ this.emojiDraft = this.emoji = folder.emoji
+ this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
+ })
},
computed: {
- id () {
+ id() {
return this.$route.params.id
- }
+ },
},
methods: {
- selectEmoji (event) {
+ selectEmoji(event) {
this.emojiDraft = event.insertion
this.emojiUrlDraft = event.insertionUrl
},
- showEmojiPicker () {
+ showEmojiPicker() {
if (!this.emojiPickerExpanded) {
this.$refs.picker.showPicker()
}
},
- onShowPicker () {
+ onShowPicker() {
this.emojiPickerExpanded = true
},
- onClosePicker () {
+ onClosePicker() {
this.emojiPickerExpanded = false
},
- updateFolder () {
- useBookmarkFoldersStore().updateBookmarkFolder({ folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
+ updateFolder() {
+ useBookmarkFoldersStore()
+ .updateBookmarkFolder({
+ folderId: this.id,
+ name: this.nameDraft,
+ emoji: this.emojiDraft,
+ })
.then(() => {
this.$router.push({ name: 'bookmark-folders' })
})
},
- createFolder () {
- useBookmarkFoldersStore().createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
+ createFolder() {
+ useBookmarkFoldersStore()
+ .createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
.then(() => {
this.$router.push({ name: 'bookmark-folders' })
})
@@ -68,15 +74,15 @@ const BookmarkFolderEdit = {
useInterfaceStore().pushGlobalNotice({
messageKey: 'bookmark_folders.error',
messageArgs: [e.message],
- level: 'error'
+ level: 'error',
})
})
},
- deleteFolder () {
+ deleteFolder() {
useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id })
this.$router.push({ name: 'bookmark-folders' })
- }
- }
+ },
+ },
}
export default BookmarkFolderEdit
diff --git a/src/components/bookmark_folders/bookmark_folders.js b/src/components/bookmark_folders/bookmark_folders.js
index 096f3769d..9fd62dae0 100644
--- a/src/components/bookmark_folders/bookmark_folders.js
+++ b/src/components/bookmark_folders/bookmark_folders.js
@@ -1,28 +1,29 @@
-import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
-import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
+import BookmarkFolderCard from 'src/components/bookmark_folder_card/bookmark_folder_card.vue'
+
+import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
const BookmarkFolders = {
- data () {
+ data() {
return {
- isNew: false
+ isNew: false,
}
},
components: {
- BookmarkFolderCard
+ BookmarkFolderCard,
},
computed: {
- bookmarkFolders () {
+ bookmarkFolders() {
return useBookmarkFoldersStore().allFolders
- }
+ },
},
methods: {
- cancelNewFolder () {
+ cancelNewFolder() {
this.isNew = false
},
- newFolder () {
+ newFolder() {
this.isNew = true
- }
- }
+ },
+ },
}
export default BookmarkFolders
diff --git a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js
index dc46b91b3..be1fb06ea 100644
--- a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js
+++ b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js
@@ -1,20 +1,20 @@
import { mapState } from 'pinia'
-import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
+
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
-import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
+import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
+
+import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
export const BookmarkFoldersMenuContent = {
- props: [
- 'showPin'
- ],
+ props: ['showPin'],
components: {
- NavigationEntry
+ NavigationEntry,
},
computed: {
...mapState(useBookmarkFoldersStore, {
- folders: getBookmarkFolderEntries
- })
- }
+ folders: getBookmarkFolderEntries,
+ }),
+ },
}
export default BookmarkFoldersMenuContent
diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js
index 9571d630f..bf633c18c 100644
--- a/src/components/bookmark_timeline/bookmark_timeline.js
+++ b/src/components/bookmark_timeline/bookmark_timeline.js
@@ -1,32 +1,38 @@
-import Timeline from '../timeline/timeline.vue'
+import Timeline from 'src/components/timeline/timeline.vue'
const Bookmarks = {
- created () {
+ created() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
- this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
+ this.$store.dispatch('startFetchingTimeline', {
+ timeline: 'bookmarks',
+ bookmarkFolderId: this.folderId || null,
+ })
},
components: {
- Timeline
+ Timeline,
},
computed: {
- folderId () {
+ folderId() {
return this.$route.params.id
},
- timeline () {
+ timeline() {
return this.$store.state.statuses.timelines.bookmarks
- }
+ },
},
watch: {
- folderId () {
+ folderId() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
- this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
- }
+ this.$store.dispatch('startFetchingTimeline', {
+ timeline: 'bookmarks',
+ bookmarkFolderId: this.folderId || null,
+ })
+ },
},
- unmounted () {
+ unmounted() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
- }
+ },
}
export default Bookmarks
diff --git a/src/components/border.style.js b/src/components/border.style.js
index 7f2c30163..e7cc31c57 100644
--- a/src/components/border.style.js
+++ b/src/components/border.style.js
@@ -6,8 +6,8 @@ export default {
{
directives: {
textColor: '$mod(--parent 10)',
- textAuto: 'no-auto'
- }
- }
- ]
+ textAuto: 'no-auto',
+ },
+ },
+ ],
}
diff --git a/src/components/bubble_timeline/bubble_timeline.js b/src/components/bubble_timeline/bubble_timeline.js
index 6f73dd2b8..fcbc31ad2 100644
--- a/src/components/bubble_timeline/bubble_timeline.js
+++ b/src/components/bubble_timeline/bubble_timeline.js
@@ -1,18 +1,20 @@
-import Timeline from '../timeline/timeline.vue'
+import Timeline from 'src/components/timeline/timeline.vue'
+
const BubbleTimeline = {
components: {
- Timeline
+ Timeline,
},
computed: {
- timeline () { return this.$store.state.statuses.timelines.bubble }
+ timeline() {
+ return this.$store.state.statuses.timelines.bubble
+ },
},
- created () {
+ created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
},
- unmounted () {
+ unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'bubble')
- }
-
+ },
}
export default BubbleTimeline
diff --git a/src/components/button.style.js b/src/components/button.style.js
index 5cffefd91..1644accc4 100644
--- a/src/components/button.style.js
+++ b/src/components/button.style.js
@@ -12,25 +12,22 @@ export default {
focused: ':focus-within',
pressed: ':active',
hover: ':is(:hover, :focus-visible):not(:disabled)',
- disabled: ':disabled'
+ disabled: ':disabled',
},
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
variants: {
// Variants save on computation time since adding new variant just adds one more "set".
// normal: '', // you can override normal variant, it will be appenended to the main class
danger: '.-danger',
- transparent: '.-transparent'
+ transparent: '.-transparent',
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
// This (currently) is further multipled by number of places where component can exist.
},
editor: {
- aspect: '2 / 1'
+ aspect: '2 / 1',
},
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
- validInnerComponents: [
- 'Text',
- 'Icon'
- ],
+ validInnerComponents: ['Text', 'Icon'],
// Default rules, used as "default theme", essentially.
defaultRules: [
{
@@ -39,9 +36,11 @@ export default {
'--buttonDefaultHoverGlow': 'shadow | 0 0 1 2 --text / 0.4',
'--buttonDefaultFocusGlow': 'shadow | 0 0 1 2 --link / 0.5',
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
- '--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)',
- '--buttonPressedBevel': 'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)'
- }
+ '--buttonDefaultBevel':
+ 'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)',
+ '--buttonPressedBevel':
+ 'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)',
+ },
},
{
// component: 'Button', // no need to specify components every time unless you're specifying how other component should look
@@ -49,128 +48,128 @@ export default {
directives: {
background: '--fg',
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
- roundness: 3
- }
+ roundness: 3,
+ },
},
{
variant: 'danger',
directives: {
- background: '--cRed'
- }
+ background: '$blend(--cRed 0.25 --inheritedBackground)',
+ },
},
{
variant: 'transparent',
directives: {
- opacity: 0.5
- }
+ opacity: 0.5,
+ },
},
{
component: 'Text',
parent: {
component: 'Button',
- variant: 'transparent'
+ variant: 'transparent',
},
directives: {
- textColor: '--text'
- }
+ textColor: '--text',
+ },
},
{
component: 'Icon',
parent: {
component: 'Button',
- variant: 'transparent'
+ variant: 'transparent',
},
directives: {
- textColor: '--text'
- }
+ textColor: '--text',
+ },
},
{
state: ['hover'],
directives: {
- shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
- }
+ shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel'],
+ },
},
{
state: ['focused'],
directives: {
- shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel']
- }
+ shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel'],
+ },
},
{
state: ['pressed'],
directives: {
- shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
- }
+ shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
+ },
},
{
state: ['pressed', 'hover'],
directives: {
- shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow']
- }
+ shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow'],
+ },
},
{
state: ['toggled'],
directives: {
background: '--accent,-24.2',
- shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
- }
+ shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
+ },
},
{
state: ['toggled', 'hover'],
directives: {
background: '--accent,-24.2',
- shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
- }
+ shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
+ },
},
{
state: ['toggled', 'focused'],
directives: {
background: '--accent,-24.2',
- shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
- }
+ shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
+ },
},
{
state: ['toggled', 'hover', 'focused'],
directives: {
background: '--accent,-24.2',
- shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
- }
+ shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
+ },
},
{
state: ['toggled', 'disabled'],
directives: {
background: '$blend(--accent 0.25 --parent)',
- shadow: ['--buttonPressedBevel']
- }
+ shadow: ['--buttonPressedBevel'],
+ },
},
{
state: ['disabled'],
directives: {
background: '$blend(--inheritedBackground 0.25 --parent)',
- shadow: ['--buttonDefaultBevel']
- }
+ shadow: ['--buttonDefaultBevel'],
+ },
},
{
component: 'Text',
parent: {
component: 'Button',
- state: ['disabled']
+ state: ['disabled'],
},
directives: {
textOpacity: 0.25,
- textOpacityMode: 'blend'
- }
+ textOpacityMode: 'blend',
+ },
},
{
component: 'Icon',
parent: {
component: 'Button',
- state: ['disabled']
+ state: ['disabled'],
},
directives: {
textOpacity: 0.25,
- textOpacityMode: 'blend'
- }
- }
- ]
+ textOpacityMode: 'blend',
+ },
+ },
+ ],
}
diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js
index 9e1a2ca90..c35fb8f69 100644
--- a/src/components/button_unstyled.style.js
+++ b/src/components/button_unstyled.style.js
@@ -7,91 +7,86 @@ export default {
toggled: '.toggled',
disabled: ':disabled',
hover: ':is(:hover, :focus-visible):not(:disabled)',
- focused: ':focus-within:not(:is(:focus-visible))'
+ focused: ':focus-within:not(:is(:focus-visible))',
},
- validInnerComponents: [
- 'Text',
- 'Link',
- 'Icon',
- 'Badge'
- ],
+ validInnerComponents: ['Text', 'Link', 'Icon', 'Badge'],
defaultRules: [
{
directives: {
- shadow: []
- }
+ shadow: [],
+ },
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
- state: ['hover']
+ state: ['hover'],
},
directives: {
- textColor: '--parent--text'
- }
+ textColor: '--parent--text',
+ },
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
- state: ['toggled']
+ state: ['toggled'],
},
directives: {
- textColor: '--parent--text'
- }
+ textColor: '--parent--text',
+ },
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
- state: ['toggled', 'hover']
+ state: ['toggled', 'hover'],
},
directives: {
- textColor: '--parent--text'
- }
+ textColor: '--parent--text',
+ },
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
- state: ['toggled', 'focused']
+ state: ['toggled', 'focused'],
},
directives: {
- textColor: '--parent--text'
- }
+ textColor: '--parent--text',
+ },
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
- state: ['toggled', 'focused', 'hover']
+ state: ['toggled', 'focused', 'hover'],
},
directives: {
- textColor: '--parent--text'
- }
+ textColor: '--parent--text',
+ },
},
{
component: 'Text',
parent: {
component: 'ButtonUnstyled',
- state: ['disabled']
+ state: ['disabled'],
},
directives: {
textOpacity: 0.25,
- textOpacityMode: 'blend'
- }
+ textOpacityMode: 'blend',
+ },
},
{
component: 'Icon',
parent: {
component: 'ButtonUnstyled',
- state: ['disabled']
+ state: ['disabled'],
},
directives: {
textOpacity: 0.25,
- textOpacityMode: 'blend'
- }
- }
- ]
+ textOpacityMode: 'blend',
+ },
+ },
+ ],
}
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index 56f389e60..7428c0dc7 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -1,25 +1,27 @@
-import _ from 'lodash'
-import { WSConnectionStatus } from '../../services/api/api.service.js'
-import { mapGetters, mapState } from 'vuex'
+import { throttle } from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
-import ChatMessage from '../chat_message/chat_message.vue'
-import PostStatusForm from '../post_status_form/post_status_form.vue'
-import ChatTitle from '../chat_title/chat_title.vue'
+import { mapGetters, mapState } from 'vuex'
+
+import ChatMessage from 'src/components/chat_message/chat_message.vue'
+import ChatTitle from 'src/components/chat_title/chat_title.vue'
+import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
+import { WSConnectionStatus } from '../../services/api/api.service.js'
import chatService from '../../services/chat_service/chat_service.js'
-import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
-import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faChevronDown,
- faChevronLeft
-} from '@fortawesome/free-solid-svg-icons'
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
+import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
+import {
+ getNewTopPosition,
+ getScrollPosition,
+ isBottomedOut,
+ isScrollable,
+} from './chat_layout_utils.js'
+
import { useInterfaceStore } from 'src/stores/interface.js'
-library.add(
- faChevronDown,
- faChevronLeft
-)
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faChevronDown, faChevronLeft)
const BOTTOMED_OUT_OFFSET = 10
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
@@ -31,78 +33,95 @@ const Chat = {
components: {
ChatMessage,
ChatTitle,
- PostStatusForm
+ PostStatusForm,
},
- data () {
+ data() {
return {
jumpToBottomButtonVisible: false,
hoveredMessageChainId: undefined,
lastScrollPosition: {},
scrollableContainerHeight: '100%',
errorLoadingChat: false,
- messageRetriers: {}
+ messageRetriers: {},
}
},
- created () {
+ created() {
this.startFetching()
window.addEventListener('resize', this.handleResize)
},
- mounted () {
+ mounted() {
window.addEventListener('scroll', this.handleScroll)
if (typeof document.hidden !== 'undefined') {
- document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
+ document.addEventListener(
+ 'visibilitychange',
+ this.handleVisibilityChange,
+ false,
+ )
}
this.$nextTick(() => {
this.handleResize()
})
},
- unmounted () {
+ unmounted() {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleResize)
- if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
+ if (typeof document.hidden !== 'undefined')
+ document.removeEventListener(
+ 'visibilitychange',
+ this.handleVisibilityChange,
+ false,
+ )
this.$store.dispatch('clearCurrentChat')
},
computed: {
- recipient () {
+ recipient() {
return this.currentChat && this.currentChat.account
},
- recipientId () {
+ recipientId() {
return this.$route.params.recipient_id
},
- formPlaceholder () {
+ formPlaceholder() {
if (this.recipient) {
- return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
+ return this.$t('chats.message_user', {
+ nickname: this.recipient.screen_name_ui,
+ })
} else {
return ''
}
},
- chatViewItems () {
+ chatViewItems() {
return chatService.getView(this.currentChatMessageService)
},
- newMessageCount () {
- return this.currentChatMessageService && this.currentChatMessageService.newMessageCount
+ newMessageCount() {
+ return (
+ this.currentChatMessageService &&
+ this.currentChatMessageService.newMessageCount
+ )
},
- streamingEnabled () {
- return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
+ streamingEnabled() {
+ return (
+ this.mergedConfig.useStreamingApi &&
+ this.mastoUserSocketStatus === WSConnectionStatus.JOINED
+ )
},
...mapGetters([
'currentChat',
'currentChatMessageService',
'findOpenedChatByRecipientId',
- 'mergedConfig'
+ 'mergedConfig',
]),
...mapPiniaState(useInterfaceStore, {
- mobileLayout: store => store.layoutType === 'mobile'
+ mobileLayout: (store) => store.layoutType === 'mobile',
}),
...mapState({
- backendInteractor: state => state.api.backendInteractor,
- mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
- currentUser: state => state.users.currentUser
- })
+ backendInteractor: (state) => state.api.backendInteractor,
+ mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
+ currentUser: (state) => state.users.currentUser,
+ }),
},
watch: {
- chatViewItems () {
+ chatViewItems() {
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
@@ -115,23 +134,23 @@ const Chat = {
$route: function () {
this.startFetching()
},
- mastoUserSocketStatus (newValue) {
+ mastoUserSocketStatus(newValue) {
if (newValue === WSConnectionStatus.JOINED) {
this.fetchChat({ isFirstFetch: true })
}
- }
+ },
},
methods: {
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
- onMessageHover ({ isHovered, messageChainId }) {
+ onMessageHover({ isHovered, messageChainId }) {
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
},
- onFilesDropped () {
+ onFilesDropped() {
this.$nextTick(() => {
this.handleResize()
})
},
- handleVisibilityChange () {
+ handleVisibilityChange() {
this.$nextTick(() => {
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
this.scrollDown({ forceRead: true })
@@ -139,7 +158,7 @@ const Chat = {
})
},
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
- handleResize (opts = {}) {
+ handleResize(opts = {}) {
const { delayed = false } = opts
if (delayed) {
@@ -160,40 +179,56 @@ const Chat = {
this.lastScrollPosition = getScrollPosition()
})
},
- scrollDown (options = {}) {
+ scrollDown(options = {}) {
const { behavior = 'auto', forceRead = false } = options
this.$nextTick(() => {
- window.scrollTo({ top: document.documentElement.scrollHeight, behavior })
+ window.scrollTo({
+ top: document.documentElement.scrollHeight,
+ behavior,
+ })
})
if (forceRead) {
this.readChat()
}
},
- readChat () {
- if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
- if (document.hidden) { return }
+ readChat() {
+ if (
+ !(
+ this.currentChatMessageService && this.currentChatMessageService.maxId
+ )
+ ) {
+ return
+ }
+ if (document.hidden) {
+ return
+ }
const lastReadId = this.currentChatMessageService.maxId
this.$store.dispatch('readChat', {
id: this.currentChat.id,
- lastReadId
+ lastReadId,
})
},
- bottomedOut (offset) {
+ bottomedOut(offset) {
return isBottomedOut(offset)
},
- reachedTop () {
+ reachedTop() {
return window.scrollY <= 0
},
- cullOlderCheck () {
+ cullOlderCheck() {
window.setTimeout(() => {
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
- this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
+ this.$store.dispatch(
+ 'cullOlderMessages',
+ this.currentChatMessageService.chatId,
+ )
}
}, 5000)
},
- handleScroll: _.throttle(function () {
+ handleScroll: throttle(function () {
this.lastScrollPosition = getScrollPosition()
- if (!this.currentChat) { return }
+ if (!this.currentChat) {
+ return
+ }
if (this.reachedTop()) {
this.fetchChat({ maxId: this.currentChatMessageService.minId })
@@ -213,22 +248,27 @@ const Chat = {
this.jumpToBottomButtonVisible = true
}
}, 200),
- handleScrollUp (positionBeforeLoading) {
+ handleScrollUp(positionBeforeLoading) {
const positionAfterLoading = getScrollPosition()
window.scrollTo({
- top: getNewTopPosition(positionBeforeLoading, positionAfterLoading)
+ top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
})
},
- fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
+ fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) {
const chatMessageService = this.currentChatMessageService
- if (!chatMessageService) { return }
- if (fetchLatest && this.streamingEnabled) { return }
+ if (!chatMessageService) {
+ return
+ }
+ if (fetchLatest && this.streamingEnabled) {
+ return
+ }
const chatId = chatMessageService.chatId
const fetchOlderMessages = !!maxId
const sinceId = fetchLatest && chatMessageService.maxId
- return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
+ return this.backendInteractor
+ .chatMessages({ id: chatId, maxId, sinceId })
.then((messages) => {
// Clear the current chat in case we're recovering from a ws connection loss.
if (isFirstFetch) {
@@ -236,28 +276,34 @@ const Chat = {
}
const positionBeforeUpdate = getScrollPosition()
- this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
- this.$nextTick(() => {
- if (fetchOlderMessages) {
- this.handleScrollUp(positionBeforeUpdate)
- }
+ this.$store
+ .dispatch('addChatMessages', { chatId, messages })
+ .then(() => {
+ this.$nextTick(() => {
+ if (fetchOlderMessages) {
+ this.handleScrollUp(positionBeforeUpdate)
+ }
- // In vertical screens, the first batch of fetched messages may not always take the
- // full height of the scrollable container.
- // If this is the case, we want to fetch the messages until the scrollable container
- // is fully populated so that the user has the ability to scroll up and load the history.
- if (!isScrollable() && messages.length > 0) {
- this.fetchChat({ maxId: this.currentChatMessageService.minId })
- }
+ // In vertical screens, the first batch of fetched messages may not always take the
+ // full height of the scrollable container.
+ // If this is the case, we want to fetch the messages until the scrollable container
+ // is fully populated so that the user has the ability to scroll up and load the history.
+ if (!isScrollable() && messages.length > 0) {
+ this.fetchChat({
+ maxId: this.currentChatMessageService.minId,
+ })
+ }
+ })
})
- })
})
},
- async startFetching () {
+ async startFetching() {
let chat = this.findOpenedChatByRecipientId(this.recipientId)
if (!chat) {
try {
- chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
+ chat = await this.backendInteractor.getOrCreateChat({
+ accountId: this.recipientId,
+ })
} catch (e) {
console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true
@@ -271,13 +317,14 @@ const Chat = {
this.doStartFetching()
}
},
- doStartFetching () {
+ doStartFetching() {
this.$store.dispatch('startFetchingCurrentChat', {
- fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
+ fetcher: () =>
+ promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000),
})
this.fetchChat({ isFirstFetch: true })
},
- handleAttachmentPosting () {
+ handleAttachmentPosting() {
this.$nextTick(() => {
this.handleResize()
// When the posting form size changes because of a media attachment, we need an extra resize
@@ -285,11 +332,11 @@ const Chat = {
this.scrollDown({ forceRead: true })
})
},
- sendMessage ({ status, media, idempotencyKey }) {
+ sendMessage({ status, media, idempotencyKey }) {
const params = {
id: this.currentChat.id,
content: status,
- idempotencyKey
+ idempotencyKey,
}
if (media[0]) {
@@ -301,52 +348,72 @@ const Chat = {
chatId: this.currentChat.id,
content: status,
userId: this.currentUser.id,
- idempotencyKey
+ idempotencyKey,
})
- this.$store.dispatch('addChatMessages', {
- chatId: this.currentChat.id,
- messages: [fakeMessage]
- }).then(() => {
- this.handleAttachmentPosting()
- })
+ this.$store
+ .dispatch('addChatMessages', {
+ chatId: this.currentChat.id,
+ messages: [fakeMessage],
+ })
+ .then(() => {
+ this.handleAttachmentPosting()
+ })
- return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
+ return this.doSendMessage({
+ params,
+ fakeMessage,
+ retriesLeft: MAX_RETRIES,
+ })
},
- doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
+ doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
if (retriesLeft <= 0) return
- this.backendInteractor.sendChatMessage(params)
- .then(data => {
+ this.backendInteractor
+ .sendChatMessage(params)
+ .then((data) => {
this.$store.dispatch('addChatMessages', {
chatId: this.currentChat.id,
updateMaxId: false,
- messages: [{ ...data, fakeId: fakeMessage.id }]
+ messages: [{ ...data, fakeId: fakeMessage.id }],
})
return data
})
- .catch(error => {
+ .catch((error) => {
console.error('Error sending message', error)
this.$store.dispatch('handleMessageError', {
chatId: this.currentChat.id,
fakeId: fakeMessage.id,
- isRetry: retriesLeft !== MAX_RETRIES
+ isRetry: retriesLeft !== MAX_RETRIES,
})
- if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') {
- this.messageRetriers[fakeMessage.id] = setTimeout(() => {
- this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 })
- }, 1000 * (2 ** (MAX_RETRIES - retriesLeft)))
+ if (
+ (error.statusCode >= 500 && error.statusCode < 600) ||
+ error.message === 'Failed to fetch'
+ ) {
+ this.messageRetriers[fakeMessage.id] = setTimeout(
+ () => {
+ this.doSendMessage({
+ params,
+ fakeMessage,
+ retriesLeft: retriesLeft - 1,
+ })
+ },
+ 1000 * 2 ** (MAX_RETRIES - retriesLeft),
+ )
}
return {}
})
return Promise.resolve(fakeMessage)
},
- goBack () {
- this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
- }
- }
+ goBack() {
+ this.$router.push({
+ name: 'chats',
+ params: { username: this.currentUser.screen_name },
+ })
+ },
+ },
}
export default Chat
diff --git a/src/components/chat/chat.style.js b/src/components/chat/chat.style.js
index 9ae2b7d71..55cf657c2 100644
--- a/src/components/chat/chat.style.js
+++ b/src/components/chat/chat.style.js
@@ -1,19 +1,13 @@
export default {
name: 'Chat',
selector: '.chat-message-list',
- validInnerComponents: [
- 'Text',
- 'Link',
- 'Icon',
- 'Avatar',
- 'ChatMessage'
- ],
+ validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'],
defaultRules: [
{
directives: {
background: '--bg',
- blur: '5px'
- }
- }
- ]
+ blur: '5px',
+ },
+ },
+ ],
}
diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue
index dfd197e56..cedbdce69 100644
--- a/src/components/chat/chat.vue
+++ b/src/components/chat/chat.vue
@@ -73,6 +73,7 @@
:disable-notice="true"
:disable-lock-warning="true"
:disable-polls="true"
+ :disable-quotes="true"
:disable-sensitivity-checkbox="true"
:disable-submit="errorLoadingChat || !currentChat"
:disable-preview="true"
diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js
index c187892d9..10d0a5e45 100644
--- a/src/components/chat/chat_layout_utils.js
+++ b/src/components/chat/chat_layout_utils.js
@@ -3,14 +3,17 @@ export const getScrollPosition = () => {
return {
scrollTop: window.scrollY,
scrollHeight: document.documentElement.scrollHeight,
- offsetHeight: window.innerHeight
+ offsetHeight: window.innerHeight,
}
}
// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
// Takes two scroll positions, before and after the update.
export const getNewTopPosition = (previousPosition, newPosition) => {
- return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
+ return (
+ previousPosition.scrollTop +
+ (newPosition.scrollHeight - previousPosition.scrollHeight)
+ )
}
export const isBottomedOut = (offset = 0) => {
diff --git a/src/components/chat_list/chat_list.js b/src/components/chat_list/chat_list.js
index 95708d1dd..597fcc709 100644
--- a/src/components/chat_list/chat_list.js
+++ b/src/components/chat_list/chat_list.js
@@ -1,37 +1,38 @@
-import { mapState, mapGetters } from 'vuex'
-import ChatListItem from '../chat_list_item/chat_list_item.vue'
-import ChatNew from '../chat_new/chat_new.vue'
-import List from '../list/list.vue'
+import { mapGetters, mapState } from 'vuex'
+
+import ChatListItem from 'src/components/chat_list_item/chat_list_item.vue'
+import ChatNew from 'src/components/chat_new/chat_new.vue'
+import List from 'src/components/list/list.vue'
const ChatList = {
components: {
ChatListItem,
List,
- ChatNew
+ ChatNew,
},
computed: {
...mapState({
- currentUser: state => state.users.currentUser
+ currentUser: (state) => state.users.currentUser,
}),
- ...mapGetters(['sortedChatList'])
+ ...mapGetters(['sortedChatList']),
},
- data () {
+ data() {
return {
- isNew: false
+ isNew: false,
}
},
- created () {
+ created() {
this.$store.dispatch('fetchChats', { latest: true })
},
methods: {
- cancelNewChat () {
+ cancelNewChat() {
this.isNew = false
this.$store.dispatch('fetchChats', { latest: true })
},
- newChat () {
+ newChat() {
this.isNew = true
- }
- }
+ },
+ },
}
export default ChatList
diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js
index 8f8c491f5..3bbb93d60 100644
--- a/src/components/chat_list_item/chat_list_item.js
+++ b/src/components/chat_list_item/chat_list_item.js
@@ -1,31 +1,31 @@
import { mapState } from 'vuex'
-import StatusBody from '../status_content/status_content.vue'
-import fileType from 'src/services/file_type/file_type.service'
-import UserAvatar from '../user_avatar/user_avatar.vue'
-import AvatarList from '../avatar_list/avatar_list.vue'
-import Timeago from '../timeago/timeago.vue'
-import ChatTitle from '../chat_title/chat_title.vue'
+
+import AvatarList from 'src/components/avatar_list/avatar_list.vue'
+import ChatTitle from 'src/components/chat_title/chat_title.vue'
+import StatusBody from 'src/components/status_content/status_content.vue'
+import Timeago from 'src/components/timeago/timeago.vue'
+import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
const ChatListItem = {
name: 'ChatListItem',
- props: [
- 'chat'
- ],
+ props: ['chat'],
components: {
UserAvatar,
AvatarList,
Timeago,
ChatTitle,
- StatusBody
+ StatusBody,
},
computed: {
...mapState({
- currentUser: state => state.users.currentUser
+ currentUser: (state) => state.users.currentUser,
}),
- attachmentInfo () {
- if (this.chat.lastMessage.attachments.length === 0) { return }
+ attachmentInfo() {
+ if (this.chat.lastMessage.attachments.length === 0) {
+ return
+ }
- const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype))
+ const types = this.chat.lastMessage.attachments.map((file) => file.type)
if (types.includes('video')) {
return this.$t('file_type.video')
} else if (types.includes('audio')) {
@@ -36,34 +36,36 @@ const ChatListItem = {
return this.$t('file_type.file')
}
},
- messageForStatusContent () {
+ messageForStatusContent() {
const message = this.chat.lastMessage
const messageEmojis = message ? message.emojis : []
const isYou = message && message.account_id === this.currentUser.id
- const content = message ? (this.attachmentInfo || message.content) : ''
- const messagePreview = isYou ? `${this.$t('chats.you')} ${content}` : content
+ const content = message ? this.attachmentInfo || message.content : ''
+ const messagePreview = isYou
+ ? `${this.$t('chats.you')} ${content}`
+ : content
return {
summary: '',
emojis: messageEmojis,
raw_html: messagePreview,
text: messagePreview,
- attachments: []
+ attachments: [],
}
- }
+ },
},
methods: {
- openChat () {
+ openChat() {
if (this.chat.id) {
this.$router.push({
name: 'chat',
params: {
username: this.currentUser.screen_name,
- recipient_id: this.chat.account.id
- }
+ recipient_id: this.chat.account.id,
+ },
})
}
- }
- }
+ },
+ },
}
export default ChatListItem
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
index 837f6d214..ba06172ed 100644
--- a/src/components/chat_message/chat_message.js
+++ b/src/components/chat_message/chat_message.js
@@ -1,24 +1,24 @@
-import { mapState, mapGetters } from 'vuex'
import { mapState as mapPiniaState } from 'pinia'
-import Popover from '../popover/popover.vue'
-import Attachment from '../attachment/attachment.vue'
-import UserAvatar from '../user_avatar/user_avatar.vue'
-import Gallery from '../gallery/gallery.vue'
-import LinkPreview from '../link-preview/link-preview.vue'
-import StatusContent from '../status_content/status_content.vue'
-import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
import { defineAsyncComponent } from 'vue'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faTimes,
- faEllipsisH
-} from '@fortawesome/free-solid-svg-icons'
-import { useInterfaceStore } from 'src/stores/interface'
+import { mapGetters, mapState } from 'vuex'
-library.add(
- faTimes,
- faEllipsisH
-)
+import Attachment from 'src/components/attachment/attachment.vue'
+import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue'
+import Gallery from 'src/components/gallery/gallery.vue'
+import LinkPreview from 'src/components/link-preview/link-preview.vue'
+import Popover from 'src/components/popover/popover.vue'
+import StatusContent from 'src/components/status_content/status_content.vue'
+import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
+import UserPopover from 'src/components/user_popover/user_popover.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useInterfaceStore } from 'src/stores/interface'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faEllipsisH, faTimes } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faTimes, faEllipsisH)
const ChatMessage = {
name: 'ChatMessage',
@@ -27,7 +27,7 @@ const ChatMessage = {
'edited',
'noHeading',
'chatViewItem',
- 'hoveredMessageChain'
+ 'hoveredMessageChain',
],
emits: ['hover'],
components: {
@@ -38,73 +38,80 @@ const ChatMessage = {
Gallery,
LinkPreview,
ChatMessageDate,
- UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
+ UserPopover,
},
computed: {
// Returns HH:MM (hours and minutes) in local time.
- createdAt () {
+ createdAt() {
const time = this.chatViewItem.data.created_at
- return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false })
+ return time.toLocaleTimeString('en', {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ })
},
- isCurrentUser () {
+ isCurrentUser() {
return this.message.account_id === this.currentUser.id
},
- message () {
+ message() {
return this.chatViewItem.data
},
- isMessage () {
+ isMessage() {
return this.chatViewItem.type === 'message'
},
- messageForStatusContent () {
+ messageForStatusContent() {
return {
summary: '',
emojis: this.message.emojis,
raw_html: this.message.content || '',
text: this.message.content || '',
- attachments: this.message.attachments
+ attachments: this.message.attachments,
}
},
- hasAttachment () {
+ hasAttachment() {
return this.message.attachments.length > 0
},
...mapPiniaState(useInterfaceStore, {
- betterShadow: store => store.browserSupport.cssFilter
+ betterShadow: (store) => store.browserSupport.cssFilter,
}),
...mapState({
- currentUser: state => state.users.currentUser,
- restrictedNicknames: state => state.instance.restrictedNicknames
+ currentUser: (state) => state.users.currentUser,
+ restrictedNicknames: (state) => useInstanceStore().restrictedNicknames,
}),
- popoverMarginStyle () {
+ popoverMarginStyle() {
if (this.isCurrentUser) {
return {}
} else {
return { left: 50 }
}
},
- ...mapGetters(['mergedConfig', 'findUser'])
+ ...mapPiniaState(useMergedConfigStore, ['mergedConfig', 'findUser']),
},
- data () {
+ data() {
return {
hovered: false,
- menuOpened: false
+ menuOpened: false,
}
},
methods: {
- onHover (bool) {
- this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId })
+ onHover(bool) {
+ this.$emit('hover', {
+ isHovered: bool,
+ messageChainId: this.chatViewItem.messageChainId,
+ })
},
- async deleteMessage () {
+ async deleteMessage() {
const confirmed = window.confirm(this.$t('chats.delete_confirm'))
if (confirmed) {
await this.$store.dispatch('deleteChatMessage', {
messageId: this.chatViewItem.data.id,
- chatId: this.chatViewItem.data.chat_id
+ chatId: this.chatViewItem.data.chat_id,
})
}
this.hovered = false
this.menuOpened = false
- }
- }
+ },
+ },
}
export default ChatMessage
diff --git a/src/components/chat_message/chat_message.style.js b/src/components/chat_message/chat_message.style.js
index 76b565823..f7632bc6f 100644
--- a/src/components/chat_message/chat_message.style.js
+++ b/src/components/chat_message/chat_message.style.js
@@ -2,26 +2,21 @@ export default {
name: 'ChatMessage',
selector: '.chat-message',
variants: {
- outgoing: '.outgoing'
+ outgoing: '.outgoing',
},
- validInnerComponents: [
- 'Text',
- 'Icon',
- 'Border',
- 'PollGraph'
- ],
+ validInnerComponents: ['Text', 'Icon', 'Border', 'PollGraph'],
defaultRules: [
{
directives: {
background: '--bg, 2',
- backgroundNoCssColor: 'yes'
- }
+ backgroundNoCssColor: 'yes',
+ },
},
{
variant: 'outgoing',
directives: {
- background: '--bg, 5'
- }
- }
- ]
+ background: '--bg, 5',
+ },
+ },
+ ],
}
diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue
index 98349b753..f0cadb6e7 100644
--- a/src/components/chat_message_date/chat_message_date.vue
+++ b/src/components/chat_message_date/chat_message_date.vue
@@ -11,16 +11,19 @@ export default {
name: 'Timeago',
props: ['date'],
computed: {
- displayDate () {
+ displayDate() {
const today = new Date()
today.setHours(0, 0, 0, 0)
if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today')
} else {
- return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
+ return this.date.toLocaleDateString(
+ localeService.internalToBrowserLocale(this.$i18n.locale),
+ { day: 'numeric', month: 'long' },
+ )
}
- }
- }
+ },
+ },
}
diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js
index 71585995a..5ee2b610d 100644
--- a/src/components/chat_new/chat_new.js
+++ b/src/components/chat_new/chat_new.js
@@ -1,39 +1,35 @@
-import { mapState, mapGetters } from 'vuex'
-import BasicUserCard from '../basic_user_card/basic_user_card.vue'
-import UserAvatar from '../user_avatar/user_avatar.vue'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faSearch,
- faChevronLeft
-} from '@fortawesome/free-solid-svg-icons'
+import { mapGetters, mapState } from 'vuex'
-library.add(
- faSearch,
- faChevronLeft
-)
+import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
+import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faSearch, faChevronLeft)
const chatNew = {
components: {
BasicUserCard,
- UserAvatar
+ UserAvatar,
},
- data () {
+ data() {
return {
suggestions: [],
userIds: [],
loading: false,
- query: ''
+ query: '',
}
},
- async created () {
+ async created() {
const { chats } = await this.backendInteractor.chats()
- chats.forEach(chat => this.suggestions.push(chat.account))
+ chats.forEach((chat) => this.suggestions.push(chat.account))
},
computed: {
- users () {
- return this.userIds.map(userId => this.findUser(userId))
+ users() {
+ return this.userIds.map((userId) => this.findUser(userId))
},
- availableUsers () {
+ availableUsers() {
if (this.query.length !== 0) {
return this.users
} else {
@@ -41,29 +37,29 @@ const chatNew = {
}
},
...mapState({
- currentUser: state => state.users.currentUser,
- backendInteractor: state => state.api.backendInteractor
+ currentUser: (state) => state.users.currentUser,
+ backendInteractor: (state) => state.api.backendInteractor,
}),
- ...mapGetters(['findUser'])
+ ...mapGetters(['findUser']),
},
methods: {
- goBack () {
+ goBack() {
this.$emit('cancel')
},
- goToChat (user) {
+ goToChat(user) {
this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
},
- onInput () {
+ onInput() {
this.search(this.query)
},
- addUser (user) {
+ addUser(user) {
this.selectedUserIds.push(user.id)
this.query = ''
},
- removeUser (userId) {
- this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
+ removeUser(userId) {
+ this.selectedUserIds = this.selectedUserIds.filter((id) => id !== userId)
},
- search (query) {
+ search(query) {
if (!query) {
this.loading = false
return
@@ -71,13 +67,14 @@ const chatNew = {
this.loading = true
this.userIds = []
- this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' })
- .then(data => {
+ this.$store
+ .dispatch('search', { q: query, resolve: true, type: 'accounts' })
+ .then((data) => {
this.loading = false
- this.userIds = data.accounts.map(a => a.id)
+ this.userIds = data.accounts.map((a) => a.id)
})
- }
- }
+ },
+ },
}
export default chatNew
diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js
index b87211265..52d4445b6 100644
--- a/src/components/chat_title/chat_title.js
+++ b/src/components/chat_title/chat_title.js
@@ -1,23 +1,27 @@
-import UserAvatar from '../user_avatar/user_avatar.vue'
-import RichContent from 'src/components/rich_content/rich_content.jsx'
import { defineAsyncComponent } from 'vue'
+import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
+import UserPopover from 'src/components/user_popover/user_popover.vue'
+
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
export default {
name: 'ChatTitle',
components: {
UserAvatar,
- RichContent,
- UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
+
+ UserPopover,
},
- props: [
- 'user', 'withAvatar'
- ],
+ props: ['user', 'withAvatar'],
computed: {
- title () {
+ title() {
return this.user ? this.user.screen_name_ui : ''
},
- htmlTitle () {
+ htmlTitle() {
return this.user ? this.user.name_html : ''
- }
- }
+ },
+ allowNonSquareEmoji() {
+ return useMergedConfigStore().mergedConfig.nonSquareEmoji
+ },
+ },
}
diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue
index 00521260f..313e66ce3 100644
--- a/src/components/chat_title/chat_title.vue
+++ b/src/components/chat_title/chat_title.vue
@@ -19,6 +19,7 @@
:title="'@'+(user && user.screen_name_ui)"
:html="htmlTitle"
:emoji="user.emoji || []"
+ :allow-non-square-emoji="allowNonSquareEmoji"
:is-local="user.is_local"
/>
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index c8bba4c44..4a3ea49bc 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -1,7 +1,7 @@
export default {
- props: [
- 'radio',
- 'modelValue',
- 'indeterminate',
- 'disabled'
- ],
+ props: ['radio', 'modelValue', 'indeterminate', 'disabled'],
emits: ['update:modelValue'],
data: (vm) => ({
- indeterminateTransitionFix: vm.indeterminate
+ indeterminateTransitionFix: vm.indeterminate,
}),
watch: {
- indeterminate (e) {
+ indeterminate(e) {
if (e) {
this.indeterminateTransitionFix = true
}
- }
+ },
},
methods: {
- onTransitionEnd () {
+ onTransitionEnd() {
if (!this.indeterminate) {
this.indeterminateTransitionFix = false
}
- }
- }
+ },
+ },
}
diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss
index 3128381f5..fad2bfca7 100644
--- a/src/components/color_input/color_input.scss
+++ b/src/components/color_input/color_input.scss
@@ -1,18 +1,27 @@
.color-input {
display: inline-flex;
+ flex-wrap: wrap;
+ max-width: 10em;
+
+ &.-compact {
+ max-width: none;
+ }
.label {
flex: 1 1 auto;
+ grid-area: label;
}
.opt {
+ grid-area: checkbox;
margin-right: 0.5em;
}
&-field.input {
- display: inline-flex;
- flex: 0 0 0;
- max-width: 9em;
+ flex: 1 1 10em;
+ max-width: 10em;
+ grid-area: input;
+ display: flex;
align-items: stretch;
input {
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index 266ad4d4e..208bdf2dc 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -1,7 +1,7 @@
diff --git a/src/components/component_preview/component_preview.js b/src/components/component_preview/component_preview.js
index 9f830cd72..1d54f58de 100644
--- a/src/components/component_preview/component_preview.js
+++ b/src/components/component_preview/component_preview.js
@@ -2,12 +2,15 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
import ColorInput from 'src/components/color_input/color_input.vue'
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
-import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
+import {
+ adoptStyleSheets,
+ createStyleSheet,
+} from 'src/services/style_setter/style_setter.js'
export default {
components: {
Checkbox,
- ColorInput
+ ColorInput,
},
props: [
'shadow',
@@ -17,41 +20,41 @@ export default {
'previewCss',
'disabled',
'invalid',
- 'noColorControl'
+ 'noColorControl',
],
emits: ['update:shadow'],
- data () {
+ data() {
return {
colorOverride: undefined,
lightGrid: false,
zoom: 100,
- randomSeed: genRandomSeed()
+ randomSeed: genRandomSeed(),
}
},
- mounted () {
+ mounted() {
this.update()
},
computed: {
- hideControls () {
+ hideControls() {
return typeof this.shadow === 'string'
- }
+ },
},
watch: {
- previewCss () {
+ previewCss() {
this.update()
},
- previewStyle () {
+ previewStyle() {
this.update()
},
- zoom () {
+ zoom() {
this.update()
- }
+ },
},
methods: {
- updateProperty (axis, value) {
+ updateProperty(axis, value) {
this.$emit('update:shadow', { axis, value: Number(value) })
},
- update () {
+ update() {
const sheet = createStyleSheet('style-component-preview', 90)
sheet.clear()
@@ -60,23 +63,25 @@ export default {
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
const styleRule = [
- '#component-preview-', this.randomSeed, ' {\n',
+ '#component-preview-',
+ this.randomSeed,
+ ' {\n',
'.preview-block {\n',
`zoom: ${this.zoom / 100};`,
this.previewStyle,
'\n}',
- '\n}'
+ '\n}',
].join('')
sheet.addRule(styleRule)
- sheet.addRule([
- '#component-preview-', this.randomSeed, ' {\n',
- ...result,
- '\n}'
- ].join(''))
+ sheet.addRule(
+ ['#component-preview-', this.randomSeed, ' {\n', ...result, '\n}'].join(
+ '',
+ ),
+ )
sheet.ready = true
adoptStyleSheets()
- }
- }
+ },
+ },
}
diff --git a/src/components/component_preview/component_preview.vue b/src/components/component_preview/component_preview.vue
index 0d81128c7..0a0f9541e 100644
--- a/src/components/component_preview/component_preview.vue
+++ b/src/components/component_preview/component_preview.vue
@@ -104,6 +104,7 @@
v-model="colorOverride"
class="input-color-input"
fallback="#606060"
+ :compact="true"
:label="$t('settings.style.shadows.color_override')"
/>
diff --git a/src/components/confirm_modal/confirm_modal.js b/src/components/confirm_modal/confirm_modal.js
index 3e2bc2cb7..08761177b 100644
--- a/src/components/confirm_modal/confirm_modal.js
+++ b/src/components/confirm_modal/confirm_modal.js
@@ -1,4 +1,4 @@
-import DialogModal from '../dialog_modal/dialog_modal.vue'
+import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
/**
* This component emits the following events:
@@ -9,30 +9,32 @@ import DialogModal from '../dialog_modal/dialog_modal.vue'
*/
const ConfirmModal = {
components: {
- DialogModal
+ DialogModal,
},
props: {
title: {
- type: String
+ type: String,
},
cancelText: {
- type: String
+ type: String,
},
confirmText: {
- type: String
- }
+ type: String,
+ },
+ confirmDanger: {
+ type: Boolean,
+ },
},
emits: ['cancelled', 'accepted'],
- computed: {
- },
+ computed: {},
methods: {
- onCancel () {
+ onCancel() {
this.$emit('cancelled')
},
- onAccept () {
+ onAccept() {
this.$emit('accepted')
- }
- }
+ },
+ },
}
export default ConfirmModal
diff --git a/src/components/confirm_modal/confirm_modal.vue b/src/components/confirm_modal/confirm_modal.vue
index 992792ede..7e1a7ff68 100644
--- a/src/components/confirm_modal/confirm_modal.vue
+++ b/src/components/confirm_modal/confirm_modal.vue
@@ -14,6 +14,7 @@
diff --git a/src/components/confirm_modal/generic_confirm.js b/src/components/confirm_modal/generic_confirm.js
index 53b41e83e..82bb0e54e 100644
--- a/src/components/confirm_modal/generic_confirm.js
+++ b/src/components/confirm_modal/generic_confirm.js
@@ -4,37 +4,37 @@ import ConfirmModal from './confirm_modal.vue'
export default {
props: {
title: {
- type: String
+ type: String,
},
message: {
- type: String
+ type: String,
},
cancelText: {
- type: String
+ type: String,
},
confirmText: {
- type: String
- }
+ type: String,
+ },
},
emits: ['hide', 'show', 'action'],
data: () => ({
- showing: false
+ showing: false,
}),
components: {
- ConfirmModal
+ ConfirmModal,
},
methods: {
- show () {
+ show() {
this.showing = true
this.$emit('show')
},
- hide () {
+ hide() {
this.showing = false
this.$emit('hide')
},
- doGeneric () {
+ doGeneric() {
this.$emit('action')
this.hide()
- }
- }
+ },
+ },
}
diff --git a/src/components/confirm_modal/mute_confirm.js b/src/components/confirm_modal/mute_confirm.js
index a279dc716..c2f5ff888 100644
--- a/src/components/confirm_modal/mute_confirm.js
+++ b/src/components/confirm_modal/mute_confirm.js
@@ -1,70 +1,78 @@
-import { mapGetters } from 'vuex'
+import { mapState } from 'pinia'
+import { defineAsyncComponent } from 'vue'
-import ConfirmModal from './confirm_modal.vue'
import Select from 'src/components/select/select.vue'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
export default {
props: ['type', 'user', 'status'],
emits: ['hide', 'show', 'muted'],
data: () => ({
- showing: false
+ showing: false,
}),
components: {
- ConfirmModal,
- Select
+ ConfirmModal: defineAsyncComponent(
+ () => import('src/components/confirm_modal/confirm_modal.vue'),
+ ),
+
+ Select,
},
computed: {
- domain () {
+ domain() {
return this.user.fqn.split('@')[1]
},
- keypath () {
+ keypath() {
if (this.type === 'domain') {
- return 'status.mute_domain_confirm'
+ return 'user_card.mute_domain_confirm'
} else if (this.type === 'conversation') {
- return 'status.mute_conversation_confirm'
+ return 'user_card.mute_conversation_confirm'
}
},
- conversationIsMuted () {
+ conversationIsMuted() {
return this.status.conversation_muted
},
- domainIsMuted () {
- return new Set(this.$store.state.users.currentUser.domainMutes).has(this.domain)
+ domainIsMuted() {
+ return new Set(this.$store.state.users.currentUser.domainMutes).has(
+ this.domain,
+ )
},
- shouldConfirm () {
+ shouldConfirm() {
switch (this.type) {
case 'domain': {
return this.mergedConfig.modalOnMuteDomain
}
- default: { // conversation
+ default: {
+ // conversation
return this.mergedConfig.modalOnMuteConversation
}
}
},
- ...mapGetters(['mergedConfig'])
+ ...mapState(useMergedConfigStore, ['mergedConfig']),
},
methods: {
- optionallyPrompt () {
+ optionallyPrompt() {
if (this.shouldConfirm) {
this.show()
} else {
this.doMute()
}
},
- show () {
+ show() {
this.showing = true
this.$emit('show')
},
- hide () {
+ hide() {
this.showing = false
this.$emit('hide')
},
- doMute () {
+ doMute() {
switch (this.type) {
case 'domain': {
if (!this.domainIsMuted) {
- this.$store.dispatch('muteDomain', { id: this.domain })
+ this.$store.dispatch('muteDomain', this.domain)
} else {
- this.$store.dispatch('unmuteDomain', { id: this.domain })
+ this.$store.dispatch('unmuteDomain', this.domain)
}
break
}
@@ -79,6 +87,6 @@ export default {
}
this.$emit('muted')
this.hide()
- }
- }
+ },
+ },
}
diff --git a/src/components/confirm_modal/mute_confirm.vue b/src/components/confirm_modal/mute_confirm.vue
index 7c754b006..108a72477 100644
--- a/src/components/confirm_modal/mute_confirm.vue
+++ b/src/components/confirm_modal/mute_confirm.vue
@@ -1,5 +1,5 @@
-
-
+
diff --git a/src/components/confirm_modal/text_confirm.js b/src/components/confirm_modal/text_confirm.js
index 11319da39..b27bd78f0 100644
--- a/src/components/confirm_modal/text_confirm.js
+++ b/src/components/confirm_modal/text_confirm.js
@@ -4,38 +4,38 @@ import ConfirmModal from './confirm_modal.vue'
export default {
props: {
title: {
- type: String
+ type: String,
},
message: {
- type: String
+ type: String,
},
cancelText: {
- type: String
+ type: String,
},
confirmText: {
- type: String
- }
+ type: String,
+ },
},
emits: ['hide', 'show', 'action'],
data: () => ({
showing: false,
- text: ""
+ text: '',
}),
components: {
- ConfirmModal
+ ConfirmModal,
},
methods: {
- show () {
+ show() {
this.showing = true
this.$emit('show')
},
- hide () {
+ hide() {
this.showing = false
this.$emit('hide')
},
- doWithText () {
+ doWithText() {
this.$emit('action', this.text)
this.hide()
- }
- }
+ },
+ },
}
diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue
index 93799e4c2..1f8b62809 100644
--- a/src/components/contrast_ratio/contrast_ratio.vue
+++ b/src/components/contrast_ratio/contrast_ratio.vue
@@ -63,54 +63,68 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAdjust,
faExclamationTriangle,
- faThumbsUp
+ faThumbsUp,
} from '@fortawesome/free-solid-svg-icons'
-library.add(
- faAdjust,
- faExclamationTriangle,
- faThumbsUp
-)
+library.add(faAdjust, faExclamationTriangle, faThumbsUp)
export default {
components: {
- Tooltip
+ Tooltip,
},
props: {
large: {
required: false,
type: Boolean,
- default: false
+ default: false,
},
// TODO: Make theme switcher compute theme initially so that contrast
// component won't be called without contrast data
contrast: {
required: false,
type: Object,
- default: () => ({})
+ default: () => ({
+ /* no-op */
+ }),
},
showRatio: {
required: false,
type: Boolean,
- default: false
- }
+ default: false,
+ },
},
computed: {
- hint () {
- const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
+ hint() {
+ const levelVal = this.contrast.aaa
+ ? 'aaa'
+ : this.contrast.aa
+ ? 'aa'
+ : 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.text')
const ratio = this.contrast.text
- return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
+ return this.$t('settings.style.common.contrast.hint', {
+ level,
+ context,
+ ratio,
+ })
},
- hint_18pt () {
- const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad')
+ hint_18pt() {
+ const levelVal = this.contrast.laaa
+ ? 'aaa'
+ : this.contrast.laa
+ ? 'aa'
+ : 'bad'
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
const context = this.$t('settings.style.common.contrast.context.18pt')
const ratio = this.contrast.text
- return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
- }
- }
+ return this.$t('settings.style.common.contrast.hint', {
+ level,
+ context,
+ ratio,
+ })
+ },
+ },
}
diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js
index 8f996be12..84494332d 100644
--- a/src/components/conversation-page/conversation-page.js
+++ b/src/components/conversation-page/conversation-page.js
@@ -1,14 +1,14 @@
-import Conversation from '../conversation/conversation.vue'
+import Conversation from 'src/components/conversation/conversation.vue'
const conversationPage = {
components: {
- Conversation
+ Conversation,
},
computed: {
- statusId () {
+ statusId() {
return this.$route.params.id
- }
- }
+ },
+ },
}
export default conversationPage
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 491a8543f..da15c79d6 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,25 +1,23 @@
-import { reduce, filter, findIndex, clone, get } from 'lodash'
-import Status from '../status/status.vue'
-import ThreadTree from '../thread_tree/thread_tree.vue'
-import { WSConnectionStatus } from '../../services/api/api.service.js'
-import { mapGetters, mapState } from 'vuex'
+import { clone, filter, findIndex, get, reduce } from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
-import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
-import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
+import { mapState } from 'vuex'
+
+import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
+import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
+import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
+import { WSConnectionStatus } from '../../services/api/api.service.js'
+
import { useInterfaceStore } from 'src/stores/interface'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAngleDoubleDown,
faAngleDoubleLeft,
- faChevronLeft
+ faChevronLeft,
} from '@fortawesome/free-solid-svg-icons'
-library.add(
- faAngleDoubleDown,
- faAngleDoubleLeft,
- faChevronLeft
-)
+library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@@ -43,23 +41,25 @@ const sortAndFilterConversation = (conversation, statusoid) => {
if (statusoid.type === 'retweet') {
conversation = filter(
conversation,
- (status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id)
+ (status) =>
+ status.type === 'retweet' ||
+ status.id !== statusoid.retweeted_status.id,
)
} else {
conversation = filter(conversation, (status) => status.type !== 'retweet')
}
- return conversation.filter(_ => _).sort(sortById)
+ return conversation.filter((_) => _).sort(sortById)
}
const conversation = {
- data () {
+ data() {
return {
highlight: null,
expanded: false,
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
statusContentPropertiesObject: {},
inlineDivePosition: null,
- loadStatusError: null
+ loadStatusError: null,
}
},
props: [
@@ -69,76 +69,80 @@ const conversation = {
'pinnedStatusIdsObject',
'inProfile',
'profileUserId',
- 'virtualHidden'
+ 'virtualHidden',
],
- created () {
+ created() {
if (this.isPage) {
this.fetchConversation()
}
},
computed: {
- maxDepthToShowByDefault () {
+ maxDepthToShowByDefault() {
// maxDepthInThread = max number of depths that is *visible*
// since our depth starts with 0 and "showing" means "showing children"
// there is a -2 here
- const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
+ const maxDepth = this.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1
},
- streamingEnabled () {
- return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
+ streamingEnabled() {
+ return (
+ this.mergedConfig.useStreamingApi &&
+ this.mastoUserSocketStatus === WSConnectionStatus.JOINED
+ )
},
- displayStyle () {
- return this.$store.getters.mergedConfig.conversationDisplay
+ displayStyle() {
+ return this.mergedConfig.conversationDisplay
},
- isTreeView () {
+ isTreeView() {
return !this.isLinearView
},
- treeViewIsSimple () {
- return !this.$store.getters.mergedConfig.conversationTreeAdvanced
+ treeViewIsSimple() {
+ return !this.mergedConfig.conversationTreeAdvanced
},
- isLinearView () {
+ isLinearView() {
return this.displayStyle === 'linear'
},
- shouldFadeAncestors () {
- return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
+ shouldFadeAncestors() {
+ return this.mergedConfig.conversationTreeFadeAncestors
},
- otherRepliesButtonPosition () {
- return this.$store.getters.mergedConfig.conversationOtherRepliesButton
+ otherRepliesButtonPosition() {
+ return this.mergedConfig.conversationOtherRepliesButton
},
- showOtherRepliesButtonBelowStatus () {
+ showOtherRepliesButtonBelowStatus() {
return this.otherRepliesButtonPosition === 'below'
},
- showOtherRepliesButtonInsideStatus () {
+ showOtherRepliesButtonInsideStatus() {
return this.otherRepliesButtonPosition === 'inside'
},
- suspendable () {
+ suspendable() {
if (this.isTreeView) {
- return Object.entries(this.statusContentProperties)
- .every(([, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
+ return Object.entries(this.statusContentProperties).every(
+ ([, prop]) => !prop.replying && prop.mediaPlaying.length === 0,
+ )
}
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
- return this.$refs.statusComponent.every(s => s.suspendable)
+ return this.$refs.statusComponent.every((s) => s.suspendable)
} else {
return true
}
},
- hideStatus () {
+ hideStatus() {
return this.virtualHidden && this.suspendable
},
- status () {
+ status() {
return this.$store.state.statuses.allStatusesObject[this.statusId]
},
- originalStatusId () {
+ originalStatusId() {
if (this.status.retweeted_status) {
return this.status.retweeted_status.id
} else {
return this.statusId
}
},
- conversationId () {
+ conversationId() {
return this.getConversationId(this.statusId)
},
- conversation () {
+ conversation() {
if (!this.status) {
return []
}
@@ -147,7 +151,9 @@ const conversation = {
return [this.status]
}
- const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
+ const conversation = clone(
+ this.$store.state.statuses.conversationsObject[this.conversationId],
+ )
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
if (statusIndex !== -1) {
conversation[statusIndex] = this.status
@@ -155,144 +161,188 @@ const conversation = {
return sortAndFilterConversation(conversation, this.status)
},
- statusMap () {
+ statusMap() {
return this.conversation.reduce((res, s) => {
res[s.id] = s
return res
}, {})
},
- threadTree () {
- const reverseLookupTable = this.conversation.reduce((table, status, index) => {
- table[status.id] = index
- return table
- }, {})
+ threadTree() {
+ const reverseLookupTable = this.conversation.reduce(
+ (table, status, index) => {
+ table[status.id] = index
+ return table
+ },
+ {},
+ )
- const threads = this.conversation.reduce((a, cur) => {
- const id = cur.id
- a.forest[id] = this.getReplies(id)
- .map(s => s.id)
+ const threads = this.conversation.reduce(
+ (a, cur) => {
+ const id = cur.id
+ a.forest[id] = this.getReplies(id).map((s) => s.id)
- return a
- }, {
- forest: {}
- })
+ return a
+ },
+ {
+ forest: {},
+ },
+ )
- const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
- if (processed[id]) {
- return []
- }
+ const walk = (forest, topLevel, depth = 0, processed = {}) =>
+ topLevel
+ .map((id) => {
+ if (processed[id]) {
+ return []
+ }
- processed[id] = true
- return [{
- status: this.conversation[reverseLookupTable[id]],
- id,
- depth
- }, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
- }).reduce((a, b) => a.concat(b), [])
+ processed[id] = true
+ return [
+ {
+ status: this.conversation[reverseLookupTable[id]],
+ id,
+ depth,
+ },
+ walk(forest, forest[id], depth + 1, processed),
+ ].reduce((a, b) => a.concat(b), [])
+ })
+ .reduce((a, b) => a.concat(b), [])
- const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
+ const linearized = walk(
+ threads.forest,
+ this.topLevel.map((k) => k.id),
+ )
return linearized
},
- replyIds () {
- return this.conversation.map(k => k.id)
+ replyIds() {
+ return this.conversation
+ .map((k) => k.id)
.reduce((res, id) => {
- res[id] = (this.replies[id] || []).map(k => k.id)
+ res[id] = (this.replies[id] || []).map((k) => k.id)
return res
}, {})
},
- totalReplyCount () {
+ totalReplyCount() {
const sizes = {}
const subTreeSizeFor = (id) => {
if (sizes[id]) {
return sizes[id]
}
- sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
+ sizes[id] =
+ 1 +
+ this.replyIds[id]
+ .map((cid) => subTreeSizeFor(cid))
+ .reduce((a, b) => a + b, 0)
return sizes[id]
}
- this.conversation.map(k => k.id).map(subTreeSizeFor)
+ this.conversation.map((k) => k.id).map(subTreeSizeFor)
return Object.keys(sizes).reduce((res, id) => {
res[id] = sizes[id] - 1 // exclude itself
return res
}, {})
},
- totalReplyDepth () {
+ totalReplyDepth() {
const depths = {}
const subTreeDepthFor = (id) => {
if (depths[id]) {
return depths[id]
}
- depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
+ depths[id] =
+ 1 +
+ this.replyIds[id]
+ .map((cid) => subTreeDepthFor(cid))
+ .reduce((a, b) => (a > b ? a : b), 0)
return depths[id]
}
- this.conversation.map(k => k.id).map(subTreeDepthFor)
+ this.conversation.map((k) => k.id).map(subTreeDepthFor)
return Object.keys(depths).reduce((res, id) => {
res[id] = depths[id] - 1 // exclude itself
return res
}, {})
},
- depths () {
+ depths() {
return this.threadTree.reduce((a, k) => {
a[k.id] = k.depth
return a
}, {})
},
- topLevel () {
- const topLevel = this.conversation.reduce((tl, cur) =>
- tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
+ topLevel() {
+ const topLevel = this.conversation.reduce(
+ (tl, cur) =>
+ tl.filter(
+ (k) =>
+ this.getReplies(cur.id)
+ .map((v) => v.id)
+ .indexOf(k.id) === -1,
+ ),
+ this.conversation,
+ )
return topLevel
},
- otherTopLevelCount () {
+ otherTopLevelCount() {
return this.topLevel.length - 1
},
- showingTopLevel () {
+ showingTopLevel() {
if (this.canDive && this.diveRoot) {
return [this.statusMap[this.diveRoot]]
}
return this.topLevel
},
- diveRoot () {
+ diveRoot() {
const statusId = this.inlineDivePosition || this.statusId
const isTopLevel = !this.parentOf(statusId)
return isTopLevel ? null : statusId
},
- diveDepth () {
+ diveDepth() {
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
},
- diveMode () {
+ diveMode() {
return this.canDive && !!this.diveRoot
},
- shouldShowAllConversationButton () {
+ shouldShowAllConversationButton() {
// The "show all conversation" button tells the user that there exist
// other toplevel statuses, so do not show it if there is only a single root
- return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
+ return (
+ this.isTreeView &&
+ this.isExpanded &&
+ this.diveMode &&
+ this.topLevel.length > 1
+ )
},
- shouldShowAncestors () {
- return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
+ shouldShowAncestors() {
+ return (
+ this.isTreeView &&
+ this.isExpanded &&
+ this.ancestorsOf(this.diveRoot).length
+ )
},
- replies () {
+ replies() {
let i = 1
- return reduce(this.conversation, (result, { id, in_reply_to_status_id: irid }) => {
- if (irid) {
- result[irid] = result[irid] || []
- result[irid].push({
- name: `#${i}`,
- id
- })
- }
- i++
- return result
- }, {})
+ return reduce(
+ this.conversation,
+ (result, { id, in_reply_to_status_id: irid }) => {
+ if (irid) {
+ result[irid] = result[irid] || []
+ result[irid].push({
+ name: `#${i}`,
+ id,
+ })
+ }
+ i++
+ return result
+ },
+ {},
+ )
},
- isExpanded () {
+ isExpanded() {
return !!(this.expanded || this.isPage)
},
- hiddenStyle () {
+ hiddenStyle() {
const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {}
},
- threadDisplayStatus () {
+ threadDisplayStatus() {
return this.conversation.reduce((a, k) => {
const id = k.id
const depth = this.depths[id]
@@ -300,7 +350,7 @@ const conversation = {
if (this.threadDisplayStatusObject[id]) {
return this.threadDisplayStatusObject[id]
}
- if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
+ if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
return 'showing'
} else {
return 'hidden'
@@ -311,7 +361,7 @@ const conversation = {
return a
}, {})
},
- statusContentProperties () {
+ statusContentProperties() {
return this.conversation.reduce((a, k) => {
const id = k.id
const props = (() => {
@@ -320,13 +370,13 @@ const conversation = {
expandingSubject: false,
showingLongSubject: false,
isReplying: false,
- mediaPlaying: []
+ mediaPlaying: [],
}
if (this.statusContentPropertiesObject[id]) {
return {
...def,
- ...this.statusContentPropertiesObject[id]
+ ...this.statusContentPropertiesObject[id],
}
}
return def
@@ -336,54 +386,58 @@ const conversation = {
return a
}, {})
},
- canDive () {
+ canDive() {
return this.isTreeView && this.isExpanded
},
- maybeHighlight () {
+ maybeHighlight() {
return this.isExpanded ? this.highlight : null
},
- ...mapGetters(['mergedConfig']),
+ ...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
...mapState({
- mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
+ mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
}),
...mapPiniaState(useInterfaceStore, {
- mobileLayout: store => store.layoutType === 'mobile'
- })
+ mobileLayout: (store) => store.layoutType === 'mobile',
+ }),
},
components: {
- Status,
ThreadTree,
QuickFilterSettings,
- QuickViewSettings
+ QuickViewSettings,
},
watch: {
- statusId (newVal, oldVal) {
+ statusId(newVal, oldVal) {
const newConversationId = this.getConversationId(newVal)
const oldConversationId = this.getConversationId(oldVal)
- if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
+ if (
+ newConversationId &&
+ oldConversationId &&
+ newConversationId === oldConversationId
+ ) {
this.setHighlight(this.originalStatusId)
} else {
this.fetchConversation()
}
},
- expanded (value) {
+ expanded(value) {
if (value) {
this.fetchConversation()
} else {
this.resetDisplayState()
}
},
- virtualHidden () {
- this.$store.dispatch(
- 'setVirtualHeight',
- { statusId: this.statusId, height: `${this.$el.clientHeight}px` }
- )
- }
+ virtualHidden() {
+ this.$store.dispatch('setVirtualHeight', {
+ statusId: this.statusId,
+ height: `${this.$el.clientHeight}px`,
+ })
+ },
},
methods: {
- fetchConversation () {
+ fetchConversation() {
if (this.status) {
- this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
+ this.$store.state.api.backendInteractor
+ .fetchConversation({ id: this.statusId })
.then(({ ancestors, descendants }) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
this.$store.dispatch('addNewStatuses', { statuses: descendants })
@@ -391,7 +445,8 @@ const conversation = {
})
} else {
this.loadStatusError = null
- this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
+ this.$store.state.api.backendInteractor
+ .fetchStatus({ id: this.statusId })
.then((status) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] })
this.fetchConversation()
@@ -401,16 +456,16 @@ const conversation = {
})
}
},
- isFocused (id) {
- return (this.isExpanded) && id === this.highlight
+ isFocused(id) {
+ return this.isExpanded && id === this.highlight
},
- getReplies (id) {
+ getReplies(id) {
return this.replies[id] || []
},
- getHighlight () {
+ getHighlight() {
return this.isExpanded ? this.highlight : null
},
- setHighlight (id) {
+ setHighlight(id) {
if (!id) return
this.highlight = id
@@ -421,44 +476,54 @@ const conversation = {
this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id)
},
- toggleExpanded () {
+ toggleExpanded() {
this.expanded = !this.expanded
},
- getConversationId (statusId) {
+ getConversationId(statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId]
- return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
+ return get(
+ status,
+ 'retweeted_status.statusnet_conversation_id',
+ get(status, 'statusnet_conversation_id'),
+ )
},
- setThreadDisplay (id, nextStatus) {
+ setThreadDisplay(id, nextStatus) {
this.threadDisplayStatusObject = {
...this.threadDisplayStatusObject,
- [id]: nextStatus
+ [id]: nextStatus,
}
},
- toggleThreadDisplay (id) {
+ toggleThreadDisplay(id) {
const curStatus = this.threadDisplayStatus[id]
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
this.setThreadDisplay(id, nextStatus)
},
- setThreadDisplayRecursively (id, nextStatus) {
+ setThreadDisplayRecursively(id, nextStatus) {
this.setThreadDisplay(id, nextStatus)
- this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
+ this.getReplies(id)
+ .map((k) => k.id)
+ .map((id) => this.setThreadDisplayRecursively(id, nextStatus))
},
- showThreadRecursively (id) {
+ showThreadRecursively(id) {
this.setThreadDisplayRecursively(id, 'showing')
},
- setStatusContentProperty (id, name, value) {
+ setStatusContentProperty(id, name, value) {
this.statusContentPropertiesObject = {
...this.statusContentPropertiesObject,
[id]: {
...this.statusContentPropertiesObject[id],
- [name]: value
- }
+ [name]: value,
+ },
}
},
- toggleStatusContentProperty (id, name) {
- this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
+ toggleStatusContentProperty(id, name) {
+ this.setStatusContentProperty(
+ id,
+ name,
+ !this.statusContentProperties[id][name],
+ )
},
- leastVisibleAncestor (id) {
+ leastVisibleAncestor(id) {
let cur = id
let parent = this.parentOf(cur)
while (cur) {
@@ -472,18 +537,20 @@ const conversation = {
// nothing found, fall back to toplevel
return this.topLevel[0] ? this.topLevel[0].id : undefined
},
- diveIntoStatus (id) {
+ diveIntoStatus(id) {
this.tryScrollTo(id)
},
- diveToTopLevel () {
- this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
+ diveToTopLevel() {
+ this.tryScrollTo(
+ this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id,
+ )
},
// only used when we are not on a page
- undive () {
+ undive() {
this.inlineDivePosition = null
this.setHighlight(this.statusId)
},
- tryScrollTo (id) {
+ tryScrollTo(id) {
if (!id) {
return
}
@@ -512,13 +579,13 @@ const conversation = {
this.setHighlight(id)
})
},
- goToCurrent () {
+ goToCurrent() {
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
},
- statusById (id) {
+ statusById(id) {
return this.statusMap[id]
},
- parentOf (id) {
+ parentOf(id) {
const status = this.statusById(id)
if (!status) {
return undefined
@@ -529,11 +596,11 @@ const conversation = {
}
return parentId
},
- parentOrSelf (id) {
+ parentOrSelf(id) {
return this.parentOf(id) || id
},
// Ancestors of some status, from top to bottom
- ancestorsOf (id) {
+ ancestorsOf(id) {
const ancestors = []
let cur = this.parentOf(id)
while (cur) {
@@ -542,7 +609,7 @@ const conversation = {
}
return ancestors
},
- topLevelAncestorOrSelfId (id) {
+ topLevelAncestorOrSelfId(id) {
let cur = id
let parent = this.parentOf(id)
while (parent) {
@@ -551,11 +618,11 @@ const conversation = {
}
return cur
},
- resetDisplayState () {
+ resetDisplayState() {
this.undive()
this.threadDisplayStatusObject = {}
- }
- }
+ },
+ },
}
export default conversation
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 2f3de3a86..8d4734083 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -88,38 +88,34 @@
class="thread-ancestor"
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
>
- import('src/components/confirm_modal/confirm_modal.vue'),
+ ),
},
data: () => ({
searchBarHidden: true,
- supportsMask: window.CSS && window.CSS.supports && (
- window.CSS.supports('mask-size', 'contain') ||
+ supportsMask:
+ window.CSS &&
+ window.CSS.supports &&
+ (window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') ||
- window.CSS.supports('-o-mask-size', 'contain')
- ),
- showingConfirmLogout: false
+ window.CSS.supports('-o-mask-size', 'contain')),
+ showingConfirmLogout: false,
}),
computed: {
- enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
- logoStyle () {
+ enableMask() {
+ return this.supportsMask && this.logoMask
+ },
+ logoStyle() {
return {
- visibility: this.enableMask ? 'hidden' : 'visible'
+ visibility: this.enableMask ? 'hidden' : 'visible',
}
},
- logoMaskStyle () {
+ logoMaskStyle() {
return this.enableMask
? {
- 'mask-image': `url(${this.$store.state.instance.logo})`
+ 'mask-image': `url(${this.logo})`,
}
: {
- 'background-color': this.enableMask ? '' : 'transparent'
+ 'background-color': this.enableMask ? '' : 'transparent',
}
},
- logoBgStyle () {
- return Object.assign({
- margin: `${this.$store.state.instance.logoMargin} 0`,
- opacity: this.searchBarHidden ? 1 : 0
- }, this.enableMask
- ? {}
- : {
- 'background-color': this.enableMask ? '' : 'transparent'
- })
+ logoBgStyle() {
+ return Object.assign(
+ {
+ margin: `${this.logoMargin} 0`,
+ opacity: this.searchBarHidden ? 1 : 0,
+ },
+ this.enableMask
+ ? {}
+ : {
+ 'background-color': this.enableMask ? '' : 'transparent',
+ },
+ )
+ },
+ ...mapState(useInstanceStore, ['privateMode']),
+ ...mapState(useInstanceStore, {
+ logoMask: (store) => store.instanceIdentity.logoMask,
+ logo: (store) => store.instanceIdentity.logo,
+ logoLeft: (store) => store.instanceIdentity.logoLeft,
+ logoMargin: (store) => store.instanceIdentity.logoMargin,
+ sitename: (store) => store.instanceIdentity.name,
+ hideSitename: (store) => store.instanceIdentity.hideSitename,
+ }),
+ currentUser() {
+ return this.$store.state.users.currentUser
+ },
+ shouldConfirmLogout() {
+ return useMergedConfigStore().mergedConfig.modalOnLogout
},
- logo () { return this.$store.state.instance.logo },
- sitename () { return this.$store.state.instance.name },
- hideSitename () { return this.$store.state.instance.hideSitename },
- logoLeft () { return this.$store.state.instance.logoLeft },
- currentUser () { return this.$store.state.users.currentUser },
- privateMode () { return this.$store.state.instance.private },
- shouldConfirmLogout () {
- return this.$store.getters.mergedConfig.modalOnLogout
- }
},
methods: {
- scrollToTop () {
+ scrollToTop() {
window.scrollTo(0, 0)
},
- showConfirmLogout () {
+ showConfirmLogout() {
this.showingConfirmLogout = true
},
- hideConfirmLogout () {
+ hideConfirmLogout() {
this.showingConfirmLogout = false
},
- logout () {
+ logout() {
if (!this.shouldConfirmLogout) {
this.doLogout()
} else {
this.showConfirmLogout()
}
},
- doLogout () {
+ doLogout() {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
this.hideConfirmLogout()
},
- onSearchBarToggled (hidden) {
+ onSearchBarToggled(hidden) {
this.searchBarHidden = hidden
},
- openSettingsModal () {
- useInterfaceStore().openSettingsModal('user')
- },
- openAdminModal () {
- useInterfaceStore().openSettingsModal('admin')
- }
- }
+ ...mapActions(useInterfaceStore, ['openSettingsModal']),
+ },
}
diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss
index 35a57d9fe..9755ac590 100644
--- a/src/components/desktop_nav/desktop_nav.scss
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -9,7 +9,7 @@
.inner-nav {
display: grid;
grid-template-rows: var(--navbar-height);
- grid-template-columns: 2fr auto 2fr;
+ grid-template-columns: minmax(5em, 1fr) auto minmax(5em, 1fr);
grid-template-areas: "sitename logo actions";
box-sizing: border-box;
padding: 0 1.2em;
@@ -31,7 +31,7 @@
}
&.-logoLeft .inner-nav {
- grid-template-columns: auto 2fr 2fr;
+ grid-template-columns: auto minmax(5em, 1fr) minmax(5em, 1fr);
grid-template-areas: "logo sitename actions";
}
@@ -92,23 +92,18 @@
.actions {
grid-area: actions;
+ justify-content: flex-end;
+ text-align: right;
+ z-index: 1;
}
.item {
- flex: 1;
line-height: var(--navbar-height);
height: var(--navbar-height);
- overflow: hidden;
display: flex;
- flex-wrap: wrap;
-
- &.right {
- justify-content: flex-end;
- text-align: right;
- }
}
.spacer {
- width: 1em;
+ min-width: 1em;
}
}
diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue
index 49382f8ee..477a7634d 100644
--- a/src/components/desktop_nav/desktop_nav.vue
+++ b/src/components/desktop_nav/desktop_nav.vue
@@ -32,61 +32,64 @@
>
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
{{ $t('login.logout_confirm') }}
-
+
diff --git a/src/components/dialog_modal/dialog_modal.js b/src/components/dialog_modal/dialog_modal.js
index b39851fe7..083f7a21d 100644
--- a/src/components/dialog_modal/dialog_modal.js
+++ b/src/components/dialog_modal/dialog_modal.js
@@ -1,19 +1,23 @@
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
const DialogModal = {
props: {
darkOverlay: {
default: true,
- type: Boolean
+ type: Boolean,
},
onCancel: {
- default: () => {},
- type: Function
- }
+ default: () => {
+ /* no-op */
+ },
+ type: Function,
+ },
},
computed: {
- mobileCenter () {
- return this.$store.getters.mergedConfig.modalMobileCenter
- }
- }
+ mobileCenter() {
+ return useMergedConfigStore().mergedConfig.modalMobileCenter
+ },
+ },
}
export default DialogModal
diff --git a/src/components/dm_timeline/dm_timeline.js b/src/components/dm_timeline/dm_timeline.js
index 8b5393a98..d54044ff7 100644
--- a/src/components/dm_timeline/dm_timeline.js
+++ b/src/components/dm_timeline/dm_timeline.js
@@ -1,14 +1,14 @@
-import Timeline from '../timeline/timeline.vue'
+import Timeline from 'src/components/timeline/timeline.vue'
const DMs = {
computed: {
- timeline () {
+ timeline() {
return this.$store.state.statuses.timelines.dms
- }
+ },
},
components: {
- Timeline
- }
+ Timeline,
+ },
}
export default DMs
diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js
index f234dcb0f..d83896618 100644
--- a/src/components/domain_mute_card/domain_mute_card.js
+++ b/src/components/domain_mute_card/domain_mute_card.js
@@ -1,26 +1,26 @@
-import ProgressButton from '../progress_button/progress_button.vue'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
const DomainMuteCard = {
props: ['domain'],
components: {
- ProgressButton
+ ProgressButton,
},
computed: {
- user () {
+ user() {
return this.$store.state.users.currentUser
},
- muted () {
+ muted() {
return this.user.domainMutes.includes(this.domain)
- }
+ },
},
methods: {
- unmuteDomain () {
+ unmuteDomain() {
return this.$store.dispatch('unmuteDomain', this.domain)
},
- muteDomain () {
+ muteDomain() {
return this.$store.dispatch('muteDomain', this.domain)
- }
- }
+ },
+ },
}
export default DomainMuteCard
diff --git a/src/components/draft/draft.js b/src/components/draft/draft.js
index 55ee11a15..529d3ddf1 100644
--- a/src/components/draft/draft.js
+++ b/src/components/draft/draft.js
@@ -1,42 +1,44 @@
-import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
-import EditStatusForm from 'src/components/edit_status_form/edit_status_form.vue'
-import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
-import StatusContent from 'src/components/status_content/status_content.vue'
-import Gallery from 'src/components/gallery/gallery.vue'
import { cloneDeep } from 'lodash'
+import { defineAsyncComponent } from 'vue'
+
+import Gallery from 'src/components/gallery/gallery.vue'
+import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
+import StatusContent from 'src/components/status_content/status_content.vue'
+
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faPollH
-} from '@fortawesome/free-solid-svg-icons'
+import { faPollH } from '@fortawesome/free-solid-svg-icons'
-library.add(
- faPollH
-)
+library.add(faPollH)
const Draft = {
components: {
PostStatusForm,
- EditStatusForm,
- ConfirmModal,
+ EditStatusForm: defineAsyncComponent(
+ () => import('src/components/edit_status_form/edit_status_form.vue'),
+ ),
+ ConfirmModal: defineAsyncComponent(
+ () => import('src/components/confirm_modal/confirm_modal.vue'),
+ ),
StatusContent,
- Gallery
+ Gallery,
},
props: {
draft: {
type: Object,
- required: true
- }
+ required: true,
+ },
},
- data () {
+ data() {
return {
referenceDraft: cloneDeep(this.draft),
editing: false,
- showingConfirmDialog: false
+ showingConfirmDialog: false,
}
},
computed: {
- relAttrs () {
+ relAttrs() {
if (this.draft.type === 'edit') {
return { statusId: this.draft.refId }
} else if (this.draft.type === 'reply') {
@@ -45,24 +47,29 @@ const Draft = {
return {}
}
},
- safeToSave () {
- return this.draft.status ||
+ safeToSave() {
+ return (
+ this.draft.status ||
this.draft.files?.length ||
- this.draft.hasPoll
+ this.draft.hasPoll ||
+ this.draft.hasQuote
+ )
},
- postStatusFormProps () {
+ postStatusFormProps() {
return {
draftId: this.draft.id,
- ...this.relAttrs
+ ...this.relAttrs,
}
},
- refStatus () {
- return this.draft.refId ? this.$store.state.statuses.allStatusesObject[this.draft.refId] : undefined
+ refStatus() {
+ return this.draft.refId
+ ? this.$store.state.statuses.allStatusesObject[this.draft.refId]
+ : undefined
},
- localCollapseSubjectDefault () {
- return this.$store.getters.mergedConfig.collapseMessageWithSubject
+ localCollapseSubjectDefault() {
+ return useMergedConfigStore().mergedConfig.collapseMessageWithSubject
},
- nsfwClickthrough () {
+ nsfwClickthrough() {
if (!this.draft.nsfw) {
return false
}
@@ -70,35 +77,34 @@ const Draft = {
return false
}
return true
- }
+ },
},
watch: {
- editing (newVal) {
+ editing(newVal) {
if (newVal) return
if (this.safeToSave) {
this.$store.dispatch('addOrSaveDraft', { draft: this.draft })
} else {
this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft })
}
- }
+ },
},
methods: {
- toggleEditing () {
+ toggleEditing() {
this.editing = !this.editing
},
- abandon () {
+ abandon() {
this.showingConfirmDialog = true
},
- doAbandon () {
- this.$store.dispatch('abandonDraft', { id: this.draft.id })
- .then(() => {
- this.hideConfirmDialog()
- })
+ doAbandon() {
+ this.$store.dispatch('abandonDraft', { id: this.draft.id }).then(() => {
+ this.hideConfirmDialog()
+ })
},
- hideConfirmDialog () {
+ hideConfirmDialog() {
this.showingConfirmDialog = false
- }
- }
+ },
+ },
}
export default Draft
diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue
index 433ebae31..c91675e35 100644
--- a/src/components/draft/draft.vue
+++ b/src/components/draft/draft.vue
@@ -39,7 +39,7 @@
class="faint"
>{{ $t('drafts.empty') }}
-
-
{{ $t('drafts.abandon_confirm') }}
-
+
import('src/components/dialog_modal/dialog_modal.vue'),
+ ),
},
- emits: [
- 'save',
- 'discard'
- ],
+ emits: ['save', 'discard'],
computed: {
- action () {
- if (this.$store.getters.mergedConfig.autoSaveDraft) {
+ action() {
+ if (useMergedConfigStore().mergedConfig.autoSaveDraft) {
return 'save'
} else {
- return this.$store.getters.mergedConfig.unsavedPostAction
+ return useMergedConfigStore().mergedConfig.unsavedPostAction
}
},
- shouldConfirm () {
+ shouldConfirm() {
return this.action === 'confirm'
- }
+ },
},
methods: {
- requestClose () {
+ requestClose() {
if (this.shouldConfirm) {
this.showing = true
} else if (this.action === 'save') {
@@ -35,18 +36,18 @@ const DraftCloser = {
this.discard()
}
},
- save () {
+ save() {
this.$emit('save')
this.showing = false
},
- discard () {
+ discard() {
this.$emit('discard')
this.showing = false
},
- cancel () {
+ cancel() {
this.showing = false
- }
- }
+ },
+ },
}
export default DraftCloser
diff --git a/src/components/draft_closer/draft_closer.vue b/src/components/draft_closer/draft_closer.vue
index 1afb1f44d..bb8e5ec55 100644
--- a/src/components/draft_closer/draft_closer.vue
+++ b/src/components/draft_closer/draft_closer.vue
@@ -1,6 +1,6 @@
-
-
+
diff --git a/src/components/drafts/drafts.js b/src/components/drafts/drafts.js
index 201417f66..3d93edb14 100644
--- a/src/components/drafts/drafts.js
+++ b/src/components/drafts/drafts.js
@@ -1,16 +1,39 @@
+import { defineAsyncComponent } from 'vue'
+
import Draft from 'src/components/draft/draft.vue'
import List from 'src/components/list/list.vue'
const Drafts = {
components: {
Draft,
- List
+ List,
+ ConfirmModal: defineAsyncComponent(
+ () => import('src/components/confirm_modal/confirm_modal.vue'),
+ ),
+ },
+ data() {
+ return {
+ showingConfirmDialog: false,
+ }
},
computed: {
- drafts () {
+ drafts() {
return this.$store.getters.draftsArray
- }
- }
+ },
+ },
+ methods: {
+ abandonAll() {
+ this.showingConfirmDialog = true
+ },
+ doAbandonAll() {
+ this.$store
+ .dispatch('abandonAllDrafts')
+ .then(() => this.hideConfirmDialog())
+ },
+ hideConfirmDialog() {
+ this.showingConfirmDialog = false
+ },
+ },
}
export default Drafts
diff --git a/src/components/drafts/drafts.vue b/src/components/drafts/drafts.vue
index 1cce255da..2e3f3432c 100644
--- a/src/components/drafts/drafts.vue
+++ b/src/components/drafts/drafts.vue
@@ -13,36 +13,66 @@
>
{{ $t('drafts.no_drafts') }}
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {{ $t('drafts.clean_drafts') }}
+
+
+
+
+
+ {{ $t('drafts.abandon_all_confirm') }}
+
+
diff --git a/src/components/edit_status_form/edit_status_form.js b/src/components/edit_status_form/edit_status_form.js
index 323763370..f7fd3d04d 100644
--- a/src/components/edit_status_form/edit_status_form.js
+++ b/src/components/edit_status_form/edit_status_form.js
@@ -1,21 +1,21 @@
-import PostStatusForm from '../post_status_form/post_status_form.vue'
+import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import statusPosterService from '../../services/status_poster/status_poster.service.js'
const EditStatusForm = {
components: {
- PostStatusForm
+ PostStatusForm,
},
props: {
params: {
type: Object,
- required: true
- }
+ required: true,
+ },
},
methods: {
- requestClose () {
+ requestClose() {
this.$refs.postStatusForm.requestClose()
},
- doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
+ doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
const params = {
store: this.$store,
statusId: this.params.statusId,
@@ -24,21 +24,22 @@ const EditStatusForm = {
sensitive,
poll,
media,
- contentType
+ contentType,
}
- return statusPosterService.editStatus(params)
+ return statusPosterService
+ .editStatus(params)
.then((data) => {
return data
})
.catch((err) => {
console.error('Error editing status', err)
return {
- error: err.message
+ error: err.message,
}
})
- }
- }
+ },
+ },
}
export default EditStatusForm
diff --git a/src/components/edit_status_form/edit_status_form.vue b/src/components/edit_status_form/edit_status_form.vue
index 0a7ec760a..1452be422 100644
--- a/src/components/edit_status_form/edit_status_form.vue
+++ b/src/components/edit_status_form/edit_status_form.vue
@@ -4,6 +4,7 @@
v-bind="params"
:post-handler="doEditStatus"
:disable-polls="true"
+ :disable-quotes="true"
:disable-visibility-selector="true"
/>
diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js
index 4c10c21a0..c3ba7e4cb 100644
--- a/src/components/edit_status_modal/edit_status_modal.js
+++ b/src/components/edit_status_modal/edit_status_modal.js
@@ -1,34 +1,38 @@
-import EditStatusForm from '../edit_status_form/edit_status_form.vue'
-import Modal from '../modal/modal.vue'
-import get from 'lodash/get'
-import { useEditStatusStore } from 'src/stores/editStatus'
+import { get } from 'lodash'
+import { defineAsyncComponent } from 'vue'
+
+import Modal from 'src/components/modal/modal.vue'
+
+import { useEditStatusStore } from 'src/stores/editStatus.js'
const EditStatusModal = {
components: {
- EditStatusForm,
- Modal
+ EditStatusForm: defineAsyncComponent(
+ () => import('src/components/edit_status_form/edit_status_form.vue'),
+ ),
+ Modal,
},
- data () {
+ data() {
return {
- resettingForm: false
+ resettingForm: false,
}
},
computed: {
- isLoggedIn () {
+ isLoggedIn() {
return !!this.$store.state.users.currentUser
},
- modalActivated () {
+ modalActivated() {
return useEditStatusStore().modalActivated
},
- isFormVisible () {
+ isFormVisible() {
return this.isLoggedIn && !this.resettingForm && this.modalActivated
},
- params () {
+ params() {
return useEditStatusStore().params || {}
- }
+ },
},
watch: {
- params (newVal, oldVal) {
+ params(newVal, oldVal) {
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
this.resettingForm = true
this.$nextTick(() => {
@@ -36,20 +40,22 @@ const EditStatusModal = {
})
}
},
- isFormVisible (val) {
+ isFormVisible(val) {
if (val) {
- this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
+ this.$nextTick(
+ () => this.$el && this.$el.querySelector('textarea').focus(),
+ )
}
- }
+ },
},
methods: {
- closeModal () {
+ closeModal() {
this.$refs.editStatusForm.requestClose()
},
- doCloseModal () {
+ doCloseModal() {
useEditStatusStore().closeEditStatusModal()
- }
- }
+ },
+ },
}
export default EditStatusModal
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index f6ba6e245..0cce2494d 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -1,20 +1,20 @@
-import Completion from '../../services/completion/completion.js'
-import genRandomSeed from '../../services/random_seed/random_seed.service.js'
-import EmojiPicker from '../emoji_picker/emoji_picker.vue'
+import { take } from 'lodash'
+
import Popover from 'src/components/popover/popover.vue'
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
-import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
-import { take } from 'lodash'
-import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { ensureFinalFallback } from '../../i18n/languages.js'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faSmileBeam
-} from '@fortawesome/free-regular-svg-icons'
+import Completion from '../../services/completion/completion.js'
+import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
+import genRandomSeed from '../../services/random_seed/random_seed.service.js'
+import EmojiPicker from '../emoji_picker/emoji_picker.vue'
+import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
-library.add(
- faSmileBeam
-)
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
+
+library.add(faSmileBeam)
/**
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
@@ -60,14 +60,14 @@ const EmojiInput = {
* For commonly used suggestors (emoji, users, both) use suggestor.js
*/
required: true,
- type: Function
+ type: Function,
},
modelValue: {
/**
* Used for v-model
*/
required: true,
- type: String
+ type: String,
},
enableEmojiPicker: {
/**
@@ -75,7 +75,7 @@ const EmojiInput = {
*/
required: false,
type: Boolean,
- default: false
+ default: false,
},
hideEmojiButton: {
/**
@@ -84,7 +84,7 @@ const EmojiInput = {
*/
required: false,
type: Boolean,
- default: false
+ default: false,
},
enableStickerPicker: {
/**
@@ -92,7 +92,7 @@ const EmojiInput = {
*/
required: false,
type: Boolean,
- default: false
+ default: false,
},
placement: {
/**
@@ -101,15 +101,15 @@ const EmojiInput = {
*/
required: false,
type: String, // 'auto', 'top', 'bottom'
- default: 'auto'
+ default: 'auto',
},
newlineOnCtrlEnter: {
required: false,
type: Boolean,
- default: false
- }
+ default: false,
+ },
},
- data () {
+ data() {
return {
randomSeed: genRandomSeed(),
input: undefined,
@@ -122,58 +122,65 @@ const EmojiInput = {
disableClickOutside: false,
suggestions: [],
overlayStyle: {},
- pickerShown: false
+ pickerShown: false,
}
},
components: {
Popover,
EmojiPicker,
UnicodeDomainIndicator,
- ScreenReaderNotice
+ ScreenReaderNotice,
},
computed: {
- padEmoji () {
- return this.$store.getters.mergedConfig.padEmoji
+ padEmoji() {
+ return useMergedConfigStore().mergedConfig.padEmoji
},
- defaultCandidateIndex () {
- return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
+ defaultCandidateIndex() {
+ return useMergedConfigStore().mergedConfig.autocompleteSelect ? 0 : -1
},
- preText () {
+ preText() {
return this.modelValue.slice(0, this.caret)
},
- postText () {
+ postText() {
return this.modelValue.slice(this.caret)
},
- showSuggestions () {
- return this.focused &&
+ showSuggestions() {
+ return (
+ this.focused &&
this.suggestions &&
this.suggestions.length > 0 &&
!this.pickerShown &&
!this.temporarilyHideSuggestions
+ )
},
- textAtCaret () {
+ textAtCaret() {
return this.wordAtCaret?.word
},
- wordAtCaret () {
+ wordAtCaret() {
if (this.modelValue && this.caret) {
- const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
+ const word =
+ Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
return word
}
},
- languages () {
- return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
+ languages() {
+ return ensureFinalFallback(
+ useMergedConfigStore().mergedConfig.interfaceLanguage,
+ )
},
- maybeLocalizedEmojiNamesAndKeywords () {
- return emoji => {
+ maybeLocalizedEmojiNamesAndKeywords() {
+ return (emoji) => {
const names = [emoji.displayText]
const keywords = []
if (emoji.displayTextI18n) {
- names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
+ names.push(
+ this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args),
+ )
}
if (emoji.annotations) {
- this.languages.forEach(lang => {
+ this.languages.forEach((lang) => {
names.push(emoji.annotations[lang]?.name)
keywords.push(...(emoji.annotations[lang]?.keywords || []))
@@ -181,13 +188,13 @@ const EmojiInput = {
}
return {
- names: names.filter(k => k),
- keywords: keywords.filter(k => k)
+ names: names.filter((k) => k),
+ keywords: keywords.filter((k) => k),
}
}
},
- maybeLocalizedEmojiName () {
- return emoji => {
+ maybeLocalizedEmojiName() {
+ return (emoji) => {
if (!emoji.annotations) {
return emoji.displayText
}
@@ -205,16 +212,18 @@ const EmojiInput = {
return emoji.displayText
}
},
- suggestionListId () {
+ suggestionListId() {
return `suggestions-${this.randomSeed}`
},
- suggestionItemId () {
+ suggestionItemId() {
return (index) => `suggestion-item-${index}-${this.randomSeed}`
- }
+ },
},
- mounted () {
+ mounted() {
const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs
- const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
+ const input =
+ root.querySelector('.emoji-input > input') ||
+ root.querySelector('.emoji-input > textarea')
if (!input) return
this.input = input
this.caretEl = hiddenOverlayCaret
@@ -243,7 +252,7 @@ const EmojiInput = {
input.addEventListener('input', this.onInput)
input.addEventListener('scroll', this.onInputScroll)
},
- unmounted () {
+ unmounted() {
const { input } = this
if (input) {
input.removeEventListener('blur', this.onBlur)
@@ -273,36 +282,40 @@ const EmojiInput = {
this.suggestions = []
return
}
- const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
+ const matchedSuggestions = await this.suggest(
+ newWord,
+ this.maybeLocalizedEmojiNamesAndKeywords,
+ )
// Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) {
this.suggestions = []
return
}
- this.suggestions = take(matchedSuggestions, 5)
- .map(({ imageUrl, ...rest }) => ({
+ this.suggestions = take(matchedSuggestions, 5).map(
+ ({ imageUrl, ...rest }) => ({
...rest,
- img: imageUrl || ''
- }))
+ img: imageUrl || '',
+ }),
+ )
this.highlighted = this.defaultCandidateIndex
this.$refs.screenReaderNotice.announce(
this.$t(
'tool_tip.autocomplete_available',
{ number: this.suggestions.length },
- this.suggestions.length
- )
+ this.suggestions.length,
+ ),
)
- }
+ },
},
methods: {
- onInputScroll (e) {
+ onInputScroll(e) {
this.$refs.hiddenOverlay.scrollTo({
top: this.input.scrollTop,
- left: this.input.scrollLeft
+ left: this.input.scrollLeft,
})
this.setCaret(e)
},
- triggerShowPicker () {
+ triggerShowPicker() {
this.$nextTick(() => {
this.$refs.picker.showPicker()
this.scrollIntoView()
@@ -315,22 +328,25 @@ const EmojiInput = {
this.disableClickOutside = false
}, 0)
},
- togglePicker () {
+ togglePicker() {
this.input.focus()
if (!this.pickerShown) {
this.scrollIntoView()
this.$refs.picker.showPicker()
- this.$refs.picker.startEmojiLoad()
} else {
this.$refs.picker.hidePicker()
}
},
- replace (replacement) {
- const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
+ replace(replacement) {
+ const newValue = Completion.replaceWord(
+ this.modelValue,
+ this.wordAtCaret,
+ replacement,
+ )
this.$emit('update:modelValue', newValue)
this.caret = 0
},
- insert ({ insertion, keepOpen, surroundingSpace = true }) {
+ insert({ insertion, keepOpen, surroundingSpace = true }) {
const before = this.modelValue.substring(0, this.caret) || ''
const after = this.modelValue.substring(this.caret) || ''
@@ -349,18 +365,24 @@ const EmojiInput = {
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
*/
const isSpaceRegex = /\s/
- const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
- const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
+ const spaceBefore =
+ surroundingSpace &&
+ !isSpaceRegex.exec(before.slice(-1)) &&
+ before.length &&
+ this.padEmoji > 0
+ ? ' '
+ : ''
+ const spaceAfter =
+ surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
+ ? ' '
+ : ''
- const newValue = [
- before,
- spaceBefore,
- insertion,
- spaceAfter,
- after
- ].join('')
+ const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
+ '',
+ )
this.$emit('update:modelValue', newValue)
- const position = this.caret + (insertion + spaceAfter + spaceBefore).length
+ const position =
+ this.caret + (insertion + spaceAfter + spaceBefore).length
if (!keepOpen) {
this.input.focus()
}
@@ -372,13 +394,20 @@ const EmojiInput = {
this.caret = position
})
},
- replaceText (e, suggestion) {
+ replaceText(e, suggestion) {
const len = this.suggestions.length || 0
- if (this.textAtCaret.length === 1) { return }
+ if (this.textAtCaret.length === 1) {
+ return
+ }
if (len > 0 || suggestion) {
- const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
+ const chosenSuggestion =
+ suggestion || this.suggestions[this.highlighted]
const replacement = chosenSuggestion.replacement
- const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
+ const newValue = Completion.replaceWord(
+ this.modelValue,
+ this.wordAtCaret,
+ replacement,
+ )
this.$emit('update:modelValue', newValue)
this.highlighted = 0
const position = this.wordAtCaret.start + replacement.length
@@ -393,7 +422,7 @@ const EmojiInput = {
e.preventDefault()
}
},
- cycleBackward (e) {
+ cycleBackward(e) {
const len = this.suggestions.length || 0
this.highlighted -= 1
@@ -406,7 +435,7 @@ const EmojiInput = {
e.preventDefault()
}
},
- cycleForward (e) {
+ cycleForward(e) {
const len = this.suggestions.length || 0
this.highlighted += 1
@@ -418,26 +447,28 @@ const EmojiInput = {
e.preventDefault()
}
},
- scrollIntoView () {
+ scrollIntoView() {
const rootRef = this.$refs.picker.$el
/* Scroller is either `window` (replies in TL), sidebar (main post form,
* replies in notifs) or mobile post form. Note that getting and setting
* scroll is different for `Window` and `Element`s
*/
- const scrollerRef = this.$el.closest('.sidebar-scroller') ||
- this.$el.closest('.post-form-modal-view') ||
- window
- const currentScroll = scrollerRef === window
- ? scrollerRef.scrollY
- : scrollerRef.scrollTop
- const scrollerHeight = scrollerRef === window
- ? scrollerRef.innerHeight
- : scrollerRef.offsetHeight
+ const scrollerRef =
+ this.$el.closest('.sidebar-scroller') ||
+ this.$el.closest('.post-form-modal-view') ||
+ window
+ const currentScroll =
+ scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
+ const scrollerHeight =
+ scrollerRef === window
+ ? scrollerRef.innerHeight
+ : scrollerRef.offsetHeight
const scrollerBottomBorder = currentScroll + scrollerHeight
// We check where the bottom border of root element is, this uses findOffset
// to find offset relative to scrollable container (scroller)
- const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
+ const rootBottomBorder =
+ rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
// could also check top delta but there's no case for it
@@ -459,13 +490,13 @@ const EmojiInput = {
}
})
},
- onPickerShown () {
+ onPickerShown() {
this.pickerShown = true
},
- onPickerClosed () {
+ onPickerClosed() {
this.pickerShown = false
},
- onBlur (e) {
+ onBlur(e) {
// Clicking on any suggestion removes focus from autocomplete,
// preventing click handler ever executing.
this.blurTimeout = setTimeout(() => {
@@ -473,10 +504,10 @@ const EmojiInput = {
this.setCaret(e)
}, 200)
},
- onClick (e, suggestion) {
+ onClick(e, suggestion) {
this.replaceText(e, suggestion)
},
- onFocus (e) {
+ onFocus(e) {
if (this.blurTimeout) {
clearTimeout(this.blurTimeout)
this.blurTimeout = null
@@ -486,7 +517,7 @@ const EmojiInput = {
this.setCaret(e)
this.temporarilyHideSuggestions = false
},
- onKeyUp (e) {
+ onKeyUp(e) {
const { key } = e
this.setCaret(e)
@@ -498,10 +529,10 @@ const EmojiInput = {
this.temporarilyHideSuggestions = false
}
},
- onPaste (e) {
+ onPaste(e) {
this.setCaret(e)
},
- onKeyDown (e) {
+ onKeyDown(e) {
const { ctrlKey, shiftKey, key } = e
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
this.insert({ insertion: '\n', surroundingSpace: false })
@@ -545,30 +576,30 @@ const EmojiInput = {
}
}
},
- onInput (e) {
+ onInput(e) {
this.setCaret(e)
this.$emit('update:modelValue', e.target.value)
},
- onStickerUploaded (e) {
+ onStickerUploaded(e) {
this.$emit('sticker-uploaded', e)
},
- onStickerUploadFailed (e) {
+ onStickerUploadFailed(e) {
this.$emit('sticker-upload-Failed', e)
},
- setCaret ({ target: { selectionStart } }) {
+ setCaret({ target: { selectionStart } }) {
this.caret = selectionStart
this.$nextTick(() => {
- this.$refs.suggestorPopover.updateStyles()
+ this.$refs.suggestorPopover?.updateStyles()
})
},
- autoCompleteItemLabel (suggestion) {
+ autoCompleteItemLabel(suggestion) {
if (suggestion.user) {
return suggestion.displayText + ' ' + suggestion.detailText
} else {
return this.maybeLocalizedEmojiName(suggestion)
}
- }
- }
+ },
+ },
}
export default EmojiInput
diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue
index a1cba33bc..8b69f3d9d 100644
--- a/src/components/emoji_input/emoji_input.vue
+++ b/src/components/emoji_input/emoji_input.vue
@@ -2,7 +2,7 @@
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js
index 031013559..4969352f6 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.js
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.js
@@ -1,57 +1,56 @@
import { debounce } from 'lodash'
+
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+import { usePostStatusStore } from 'src/stores/post_status.js'
+
import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faPen
-} from '@fortawesome/free-solid-svg-icons'
-import { usePostStatusStore } from 'src/stores/post_status'
+import { faPen } from '@fortawesome/free-solid-svg-icons'
-library.add(
- faPen
-)
+library.add(faPen)
-const HIDDEN_FOR_PAGES = new Set([
- 'chats',
- 'chat',
- 'lists-edit'
-])
+const HIDDEN_FOR_PAGES = new Set(['chats', 'chat', 'lists-edit'])
const MobilePostStatusButton = {
- data () {
+ data() {
return {
hidden: false,
scrollingDown: false,
inputActive: false,
oldScrollPos: 0,
- amountScrolled: 0
+ amountScrolled: 0,
}
},
- created () {
+ created() {
if (this.autohideFloatingPostButton) {
this.activateFloatingPostButtonAutohide()
}
window.addEventListener('resize', this.handleOSK)
},
- unmounted () {
+ unmounted() {
if (this.autohideFloatingPostButton) {
this.deactivateFloatingPostButtonAutohide()
}
window.removeEventListener('resize', this.handleOSK)
},
computed: {
- isLoggedIn () {
+ isLoggedIn() {
return !!this.$store.state.users.currentUser
},
- isHidden () {
- if (HIDDEN_FOR_PAGES.has(this.$route.name)) { return true }
+ isHidden() {
+ if (HIDDEN_FOR_PAGES.has(this.$route.name)) {
+ return true
+ }
- return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
+ return (
+ this.autohideFloatingPostButton && (this.hidden || this.inputActive)
+ )
},
- isPersistent () {
- return !!this.$store.getters.mergedConfig.alwaysShowNewPostButton
+ isPersistent() {
+ return !!useMergedConfigStore().mergedConfig.alwaysShowNewPostButton
+ },
+ autohideFloatingPostButton() {
+ return !!useMergedConfigStore().mergedConfig.autohideFloatingPostButton
},
- autohideFloatingPostButton () {
- return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
- }
},
watch: {
autohideFloatingPostButton: function (isEnabled) {
@@ -60,21 +59,21 @@ const MobilePostStatusButton = {
} else {
this.deactivateFloatingPostButtonAutohide()
}
- }
+ },
},
methods: {
- activateFloatingPostButtonAutohide () {
+ activateFloatingPostButtonAutohide() {
window.addEventListener('scroll', this.handleScrollStart)
window.addEventListener('scroll', this.handleScrollEnd)
},
- deactivateFloatingPostButtonAutohide () {
+ deactivateFloatingPostButtonAutohide() {
window.removeEventListener('scroll', this.handleScrollStart)
window.removeEventListener('scroll', this.handleScrollEnd)
},
- openPostForm () {
+ openPostForm() {
usePostStatusStore().openPostStatusModal()
},
- handleOSK () {
+ handleOSK() {
// This is a big hack: we're guessing from changed window sizes if the
// on-screen keyboard is active or not. This is only really important
// for phones in portrait mode and it's more important to show the button
@@ -94,20 +93,28 @@ const MobilePostStatusButton = {
this.inputActive = false
}
},
- handleScrollStart: debounce(function () {
- if (window.scrollY > this.oldScrollPos) {
- this.hidden = true
- } else {
- this.hidden = false
- }
- this.oldScrollPos = window.scrollY
- }, 100, { leading: true, trailing: false }),
+ handleScrollStart: debounce(
+ function () {
+ if (window.scrollY > this.oldScrollPos) {
+ this.hidden = true
+ } else {
+ this.hidden = false
+ }
+ this.oldScrollPos = window.scrollY
+ },
+ 100,
+ { leading: true, trailing: false },
+ ),
- handleScrollEnd: debounce(function () {
- this.hidden = false
- this.oldScrollPos = window.scrollY
- }, 100, { leading: false, trailing: true })
- }
+ handleScrollEnd: debounce(
+ function () {
+ this.hidden = false
+ this.oldScrollPos = window.scrollY
+ },
+ 100,
+ { leading: false, trailing: true },
+ ),
+ },
}
export default MobilePostStatusButton
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index 032e7dbeb..a022f7427 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -13,27 +13,27 @@
diff --git a/src/components/modal/modals.style.js b/src/components/modal/modals.style.js
index 1cb4a34a7..9c8c279f7 100644
--- a/src/components/modal/modals.style.js
+++ b/src/components/modal/modals.style.js
@@ -3,8 +3,6 @@ export default {
selector: ['.modal-view', '#modal', '.shout-panel'],
lazy: true,
notEditable: true,
- validInnerComponents: [
- 'Panel'
- ],
- defaultRules: []
+ validInnerComponents: ['Panel'],
+ defaultRules: [],
}
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
index bd57a353a..d31002fad 100644
--- a/src/components/moderation_tools/moderation_tools.js
+++ b/src/components/moderation_tools/moderation_tools.js
@@ -1,9 +1,12 @@
+import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
+import Popover from 'src/components/popover/popover.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+
import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
-import DialogModal from '../dialog_modal/dialog_modal.vue'
-import Popover from '../popover/popover.vue'
-
library.add(faChevronDown)
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
@@ -15,10 +18,8 @@ const SANDBOX = 'mrf_tag:sandbox'
const QUARANTINE = 'mrf_tag:quarantine'
const ModerationTools = {
- props: [
- 'user'
- ],
- data () {
+ props: ['user'],
+ data() {
return {
tags: {
FORCE_NSFW,
@@ -27,92 +28,124 @@ const ModerationTools = {
DISABLE_REMOTE_SUBSCRIPTION,
DISABLE_ANY_SUBSCRIPTION,
SANDBOX,
- QUARANTINE
+ QUARANTINE,
},
showDeleteUserDialog: false,
- toggled: false
+ toggled: false,
}
},
components: {
DialogModal,
- Popover
+ Popover,
},
computed: {
- tagsSet () {
+ tagsSet() {
return new Set(this.user.tags)
},
- canGrantRole () {
- return this.user.is_local && !this.user.deactivated && this.$store.state.users.currentUser.role === 'admin'
+ canGrantRole() {
+ return (
+ this.user.is_local &&
+ !this.user.deactivated &&
+ this.$store.state.users.currentUser.role === 'admin'
+ )
},
- canChangeActivationState () {
+ canChangeActivationState() {
return this.privileged('users_manage_activation_state')
},
- canDeleteAccount () {
+ canDeleteAccount() {
return this.privileged('users_delete')
},
- canUseTagPolicy () {
- return this.$store.state.instance.tagPolicyAvailable && this.privileged('users_manage_tags')
- }
+ canUseTagPolicy() {
+ return (
+ useInstanceCapabilitiesStore().tagPolicyAvailable &&
+ this.privileged('users_manage_tags')
+ )
+ },
},
methods: {
- hasTag (tagName) {
+ hasTag(tagName) {
return this.tagsSet.has(tagName)
},
- privileged (privilege) {
+ privileged(privilege) {
return this.$store.state.users.currentUser.privileges.includes(privilege)
},
- toggleTag (tag) {
+ toggleTag(tag) {
const store = this.$store
if (this.tagsSet.has(tag)) {
- store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
- if (!response.ok) { return }
- store.commit('untagUser', { user: this.user, tag })
- })
+ store.state.api.backendInteractor
+ .untagUser({ user: this.user, tag })
+ .then((response) => {
+ if (!response.ok) {
+ return
+ }
+ store.commit('untagUser', { user: this.user, tag })
+ })
} else {
- store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
- if (!response.ok) { return }
- store.commit('tagUser', { user: this.user, tag })
- })
+ store.state.api.backendInteractor
+ .tagUser({ user: this.user, tag })
+ .then((response) => {
+ if (!response.ok) {
+ return
+ }
+ store.commit('tagUser', { user: this.user, tag })
+ })
}
},
- toggleRight (right) {
+ toggleRight(right) {
const store = this.$store
if (this.user.rights[right]) {
- store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => {
- if (!response.ok) { return }
- store.commit('updateRight', { user: this.user, right, value: false })
- })
+ store.state.api.backendInteractor
+ .deleteRight({ user: this.user, right })
+ .then((response) => {
+ if (!response.ok) {
+ return
+ }
+ store.commit('updateRight', {
+ user: this.user,
+ right,
+ value: false,
+ })
+ })
} else {
- store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => {
- if (!response.ok) { return }
- store.commit('updateRight', { user: this.user, right, value: true })
- })
+ store.state.api.backendInteractor
+ .addRight({ user: this.user, right })
+ .then((response) => {
+ if (!response.ok) {
+ return
+ }
+ store.commit('updateRight', { user: this.user, right, value: true })
+ })
}
},
- toggleActivationStatus () {
+ toggleActivationStatus() {
this.$store.dispatch('toggleActivationStatus', { user: this.user })
},
- deleteUserDialog (show) {
+ deleteUserDialog(show) {
this.showDeleteUserDialog = show
},
- deleteUser () {
+ deleteUser() {
const store = this.$store
const user = this.user
const { id, name } = user
- store.state.api.backendInteractor.deleteUser({ user })
- .then(() => {
- this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
- const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
- const isTargetUser = this.$route.params.name === name || this.$route.params.id === id
- if (isProfile && isTargetUser) {
- window.history.back()
- }
- })
+ store.state.api.backendInteractor.deleteUser({ user }).then(() => {
+ this.$store.dispatch(
+ 'markStatusesAsDeleted',
+ (status) => user.id === status.user.id,
+ )
+ const isProfile =
+ this.$route.name === 'external-user-profile' ||
+ this.$route.name === 'user-profile'
+ const isTargetUser =
+ this.$route.params.name === name || this.$route.params.id === id
+ if (isProfile && isTargetUser) {
+ window.history.back()
+ }
+ })
},
- setToggled (value) {
+ setToggled(value) {
this.toggled = value
- }
- }
+ },
+ },
}
export default ModerationTools
diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js
index 13cfb52ee..b2048984d 100644
--- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js
+++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js
@@ -1,5 +1,7 @@
-import { mapState } from 'vuex'
import { get } from 'lodash'
+import { mapState } from 'pinia'
+
+import { useInstanceStore } from 'src/stores/instance.js'
/**
* This is for backwards compatibility. We originally didn't recieve
@@ -8,7 +10,7 @@ import { get } from 'lodash'
* to add an extra "info" key.
*/
const toInstanceReasonObject = (instances, info, key) => {
- return instances.map(instance => {
+ return instances.map((instance) => {
if (info[key] && info[key][instance] && info[key][instance].reason) {
return { instance, reason: info[key][instance].reason }
}
@@ -18,57 +20,78 @@ const toInstanceReasonObject = (instances, info, key) => {
const MRFTransparencyPanel = {
computed: {
- ...mapState({
- federationPolicy: state => get(state, 'instance.federationPolicy'),
- mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
- quarantineInstances: state => toInstanceReasonObject(
- get(state, 'instance.federationPolicy.quarantined_instances', []),
- get(state, 'instance.federationPolicy.quarantined_instances_info', []),
- 'quarantined_instances'
- ),
- acceptInstances: state => toInstanceReasonObject(
- get(state, 'instance.federationPolicy.mrf_simple.accept', []),
- get(state, 'instance.federationPolicy.mrf_simple_info', []),
- 'accept'
- ),
- rejectInstances: state => toInstanceReasonObject(
- get(state, 'instance.federationPolicy.mrf_simple.reject', []),
- get(state, 'instance.federationPolicy.mrf_simple_info', []),
- 'reject'
- ),
- ftlRemovalInstances: state => toInstanceReasonObject(
- get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
- get(state, 'instance.federationPolicy.mrf_simple_info', []),
- 'federated_timeline_removal'
- ),
- mediaNsfwInstances: state => toInstanceReasonObject(
- get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
- get(state, 'instance.federationPolicy.mrf_simple_info', []),
- 'media_nsfw'
- ),
- mediaRemovalInstances: state => toInstanceReasonObject(
- get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
- get(state, 'instance.federationPolicy.mrf_simple_info', []),
- 'media_removal'
- ),
- keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
- keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
- keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])
+ ...mapState(useInstanceStore, {
+ federationPolicy: (state) => state.federationPolicy,
+ mrfPolicies: (state) => get(state, 'federationPolicy.mrf_policies', []),
+ quarantineInstances: (state) =>
+ toInstanceReasonObject(
+ get(state, 'federationPolicy.quarantined_instances', []),
+ get(state, 'federationPolicy.quarantined_instances_info', []),
+ 'quarantined_instances',
+ ),
+ acceptInstances: (state) =>
+ toInstanceReasonObject(
+ get(state, 'federationPolicy.mrf_simple.accept', []),
+ get(state, 'federationPolicy.mrf_simple_info', []),
+ 'accept',
+ ),
+ rejectInstances: (state) =>
+ toInstanceReasonObject(
+ get(state, 'federationPolicy.mrf_simple.reject', []),
+ get(state, 'federationPolicy.mrf_simple_info', []),
+ 'reject',
+ ),
+ ftlRemovalInstances: (state) =>
+ toInstanceReasonObject(
+ get(
+ state,
+ 'federationPolicy.mrf_simple.federated_timeline_removal',
+ [],
+ ),
+ get(state, 'federationPolicy.mrf_simple_info', []),
+ 'federated_timeline_removal',
+ ),
+ mediaNsfwInstances: (state) =>
+ toInstanceReasonObject(
+ get(state, 'federationPolicy.mrf_simple.media_nsfw', []),
+ get(state, 'federationPolicy.mrf_simple_info', []),
+ 'media_nsfw',
+ ),
+ mediaRemovalInstances: (state) =>
+ toInstanceReasonObject(
+ get(state, 'federationPolicy.mrf_simple.media_removal', []),
+ get(state, 'federationPolicy.mrf_simple_info', []),
+ 'media_removal',
+ ),
+ keywordsFtlRemoval: (state) =>
+ get(
+ state,
+ 'federationPolicy.mrf_keyword.federated_timeline_removal',
+ [],
+ ),
+ keywordsReject: (state) =>
+ get(state, 'federationPolicy.mrf_keyword.reject', []),
+ keywordsReplace: (state) =>
+ get(state, 'federationPolicy.mrf_keyword.replace', []),
}),
- hasInstanceSpecificPolicies () {
- return this.quarantineInstances.length ||
+ hasInstanceSpecificPolicies() {
+ return (
+ this.quarantineInstances.length ||
this.acceptInstances.length ||
this.rejectInstances.length ||
this.ftlRemovalInstances.length ||
this.mediaNsfwInstances.length ||
this.mediaRemovalInstances.length
+ )
},
- hasKeywordPolicies () {
- return this.keywordsFtlRemoval.length ||
+ hasKeywordPolicies() {
+ return (
+ this.keywordsFtlRemoval.length ||
this.keywordsReject.length ||
this.keywordsReplace.length
- }
- }
+ )
+ },
+ },
}
export default MRFTransparencyPanel
diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js
index 895586888..0d247930a 100644
--- a/src/components/mute_card/mute_card.js
+++ b/src/components/mute_card/mute_card.js
@@ -1,39 +1,41 @@
-import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
const MuteCard = {
props: ['userId'],
computed: {
- user () {
+ user() {
return this.$store.getters.findUser(this.userId)
},
- relationship () {
+ relationship() {
return this.$store.getters.relationship(this.userId)
},
- muted () {
+ muted() {
return this.relationship.muting
},
- muteExpiryAvailable () {
- return this.user.mute_expires_at !== undefined
+ muteExpiryAvailable() {
+ return Object.hasOwn(this.user, 'mute_expires_at')
},
- muteExpiry () {
- return this.user.mute_expires_at == null
+ muteExpiry() {
+ return this.user.mute_expires_at === false
? this.$t('user_card.mute_expires_forever')
- : this.$t('user_card.mute_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
- }
+ : this.$t('user_card.mute_expires_at', [
+ new Date(this.user.mute_expires_at).toLocaleString(),
+ ])
+ },
},
components: {
BasicUserCard,
- UserTimedFilterModal
+ UserTimedFilterModal,
},
methods: {
- unmuteUser () {
+ unmuteUser() {
this.$store.dispatch('unmuteUser', this.userId)
},
- muteUser () {
+ muteUser() {
this.$refs.timedMuteDialog.optionallyPrompt()
- }
- }
+ },
+ },
}
export default MuteCard
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index a155abe0c..ae6264217 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -1,32 +1,35 @@
-import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue'
-import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue'
-import { mapState, mapGetters } from 'vuex'
import { mapState as mapPiniaState } from 'pinia'
-import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js'
+import { mapGetters, mapState } from 'vuex'
+
+import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue'
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue'
import { filterNavigation } from 'src/components/navigation/filter.js'
+import { ROOT_ITEMS, TIMELINES } from 'src/components/navigation/navigation.js'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
-import Checkbox from 'src/components/checkbox/checkbox.vue'
import { useAnnouncementsStore } from 'src/stores/announcements'
-import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
- faUsers,
- faGlobe,
- faCity,
+ faBell,
faBookmark,
- faEnvelope,
+ faBullhorn,
faChevronDown,
faChevronUp,
+ faCity,
faComments,
- faBell,
+ faEnvelope,
+ faFilePen,
+ faGlobe,
faInfoCircle,
- faStream,
faList,
- faBullhorn,
- faFilePen
+ faStream,
+ faUsers,
} from '@fortawesome/free-solid-svg-icons'
library.add(
@@ -43,80 +46,94 @@ library.add(
faStream,
faList,
faBullhorn,
- faFilePen
+ faFilePen,
)
const NavPanel = {
props: ['forceExpand', 'forceEditMode'],
- created () {
- },
components: {
BookmarkFoldersMenuContent,
ListsMenuContent,
NavigationEntry,
NavigationPins,
- Checkbox
+ Checkbox,
},
- data () {
+ data() {
return {
editMode: false,
showTimelines: false,
showLists: false,
showBookmarkFolders: false,
- timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })),
- rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k }))
+ timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({
+ ...v,
+ name: k,
+ })),
+ rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k })),
}
},
methods: {
- toggleTimelines () {
+ toggleTimelines() {
this.showTimelines = !this.showTimelines
},
- toggleLists () {
+ toggleLists() {
this.showLists = !this.showLists
},
- toggleBookmarkFolders () {
+ toggleBookmarkFolders() {
this.showBookmarkFolders = !this.showBookmarkFolders
},
- toggleEditMode () {
+ toggleEditMode() {
this.editMode = !this.editMode
},
- toggleCollapse () {
- useServerSideStorageStore().setPreference({ path: 'simple.collapseNav', value: !this.collapsed })
- useServerSideStorageStore().pushServerSideStorage()
+ toggleCollapse() {
+ useSyncConfigStore().setSimplePrefAndSave({
+ path: 'collapseNav',
+ value: !this.collapsed,
+ })
+ useSyncConfigStore().pushSyncConfig()
},
- isPinned (item) {
+ isPinned(item) {
return this.pinnedItems.has(item)
},
- togglePin (item) {
+ togglePin(item) {
if (this.isPinned(item)) {
- useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value: item })
+ useSyncConfigStore().removeCollectionPreference({
+ path: 'collections.pinnedNavItems',
+ value: item,
+ })
} else {
- useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value: item })
+ useSyncConfigStore().addCollectionPreference({
+ path: 'collections.pinnedNavItems',
+ value: item,
+ })
}
- useServerSideStorageStore().pushServerSideStorage()
- }
+ useSyncConfigStore().pushSyncConfig()
+ },
},
computed: {
...mapPiniaState(useAnnouncementsStore, {
unreadAnnouncementCount: 'unreadAnnouncementCount',
- supportsAnnouncements: store => store.supportsAnnouncements
+ supportsAnnouncements: (store) => store.supportsAnnouncements,
}),
- ...mapPiniaState(useServerSideStorageStore, {
- collapsed: store => store.prefsStorage.simple.collapseNav,
- pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
+ ...mapPiniaState(useInstanceCapabilitiesStore, [
+ 'pleromaChatMessagesAvailable',
+ 'pleromaBookmarkFoldersAvailable',
+ 'localBubble',
+ ]),
+ ...mapPiniaState(useInstanceStore, ['federating']),
+ ...mapPiniaState(useInstanceStore, {
+ privateMode: (store) => store.private,
+ }),
+ ...mapPiniaState(useSyncConfigStore, {
+ collapsed: (store) => store.prefsStorage.simple.collapseNav,
+ pinnedItems: (store) =>
+ new Set(store.prefsStorage.collections.pinnedNavItems),
}),
...mapState({
- currentUser: state => state.users.currentUser,
- followRequestCount: state => state.api.followRequests.length,
- privateMode: state => state.instance.private,
- federating: state => state.instance.federating,
- pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
- bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable,
- bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
+ currentUser: (state) => state.users.currentUser,
+ followRequestCount: (state) => state.api.followRequests.length,
}),
- timelinesItems () {
+ timelinesItems() {
return filterNavigation(
- Object
- .entries({ ...TIMELINES })
+ Object.entries({ ...TIMELINES })
// do not show in timeliens list since it's in a better place now
.filter(([key]) => key !== 'bookmarks')
.map(([k, v]) => ({ ...v, name: k })),
@@ -126,29 +143,27 @@ const NavPanel = {
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser,
- supportsBubbleTimeline: this.bubbleTimeline,
- supportsBookmarkFolders: this.bookmarkFolders
- }
+ supportsBubbleTimeline: this.localBubble,
+ supportsBookmarkFolders: this.pleromaBookmarkFoldersAvailable,
+ },
)
},
- rootItems () {
+ rootItems() {
return filterNavigation(
- Object
- .entries({ ...ROOT_ITEMS })
- .map(([k, v]) => ({ ...v, name: k })),
+ Object.entries({ ...ROOT_ITEMS }).map(([k, v]) => ({ ...v, name: k })),
{
hasChats: this.pleromaChatMessagesAvailable,
hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser,
- supportsBubbleTimeline: this.bubbleTimeline,
- supportsBookmarkFolders: this.bookmarkFolders
- }
+ supportsBubbleTimeline: this.localBubble,
+ supportsBookmarkFolders: this.pleromaBookmarkFoldersAvailable,
+ },
)
},
- ...mapGetters(['unreadChatCount'])
- }
+ ...mapGetters(['unreadChatCount']),
+ },
}
export default NavPanel
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 10903bcae..ae5c6eae4 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -84,7 +84,7 @@
/>
{
+export const filterNavigation = (
+ list = [],
+ {
+ hasChats,
+ hasAnnouncements,
+ isFederating,
+ isPrivate,
+ currentUser,
+ supportsBookmarkFolders,
+ supportsBubbleTimeline,
+ },
+) => {
return list.filter(({ criteria, anon, anonRoute }) => {
const set = new Set(criteria || [])
if (!isFederating && set.has('federating')) return false
if (!currentUser && isPrivate && set.has('!private')) return false
if (!currentUser && !(anon || anonRoute)) return false
- if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
+ if ((!currentUser || !currentUser.locked) && set.has('lockedUser'))
+ return false
if (!hasChats && set.has('chats')) return false
if (!hasAnnouncements && set.has('announcements')) return false
- if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline')) return false
- if (!supportsBookmarkFolders && set.has('supportsBookmarkFolders')) return false
- if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders')) return false
+ if (!supportsBubbleTimeline && set.has('supportsBubbleTimeline'))
+ return false
+ if (!supportsBookmarkFolders && set.has('supportsBookmarkFolders'))
+ return false
+ if (supportsBookmarkFolders && set.has('!supportsBookmarkFolders'))
+ return false
return true
})
}
-export const getListEntries = store => store.allLists.map(list => ({
- name: 'list-' + list.id,
- routeObject: { name: 'lists-timeline', params: { id: list.id } },
- labelRaw: list.title,
- iconLetter: list.title[0]
-}))
+export const getListEntries = (store) =>
+ store.allLists.map((list) => ({
+ name: 'list-' + list.id,
+ routeObject: { name: 'lists-timeline', params: { id: list.id } },
+ labelRaw: list.title,
+ iconLetter: list.title[0],
+ }))
-export const getBookmarkFolderEntries = store => store.allFolders ? store.allFolders.map(folder => ({
- name: 'bookmark-folder-' + folder.id,
- routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
- labelRaw: folder.name,
- iconEmoji: folder.emoji,
- iconEmojiUrl: folder.emoji_url,
- iconLetter: folder.name[0]
-})) : []
+export const getBookmarkFolderEntries = (store) =>
+ store.allFolders
+ ? store.allFolders.map((folder) => ({
+ name: 'bookmark-folder-' + folder.id,
+ routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
+ labelRaw: folder.name,
+ iconEmoji: folder.emoji,
+ iconEmojiUrl: folder.emoji_url,
+ iconLetter: folder.name[0],
+ }))
+ : []
diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js
index d1c2b6763..66fb0d347 100644
--- a/src/components/navigation/navigation.js
+++ b/src/components/navigation/navigation.js
@@ -4,42 +4,39 @@ export const USERNAME_ROUTES = new Set([
'interactions',
'notifications',
'chat',
- 'chats'
+ 'chats',
])
// routes that take :name property
-export const NAME_ROUTES = new Set([
- 'user-profile',
- 'legacy-user-profile'
-])
+export const NAME_ROUTES = new Set(['user-profile', 'legacy-user-profile'])
export const TIMELINES = {
home: {
route: 'friends',
icon: 'home',
label: 'nav.home_timeline',
- criteria: ['!private']
+ criteria: ['!private'],
},
public: {
route: 'public-timeline',
anon: true,
icon: 'users',
label: 'nav.public_tl',
- criteria: ['!private']
+ criteria: ['!private'],
},
bubble: {
route: 'bubble',
anon: true,
icon: 'city',
label: 'nav.bubble',
- criteria: ['!private', 'federating', 'supportsBubbleTimeline']
+ criteria: ['!private', 'federating', 'supportsBubbleTimeline'],
},
twkn: {
route: 'public-external-timeline',
anon: true,
icon: 'globe',
label: 'nav.twkn',
- criteria: ['!private', 'federating']
+ criteria: ['!private', 'federating'],
},
// bookmarks are still technically a timeline so we should show it in the dropdown
bookmarks: {
@@ -50,13 +47,13 @@ export const TIMELINES = {
favorites: {
routeObject: { name: 'user-profile', query: { tab: 'favorites' } },
icon: 'star',
- label: 'user_card.favorites'
+ label: 'user_card.favorites',
},
dms: {
route: 'dms',
icon: 'envelope',
- label: 'nav.dms'
- }
+ label: 'nav.dms',
+ },
}
export const ROOT_ITEMS = {
@@ -67,12 +64,12 @@ export const ROOT_ITEMS = {
// shows bookmarks entry in a better suited location
// hides it when bookmark folders are supported since
// we show custom component instead of it
- criteria: ['!supportsBookmarkFolders']
+ criteria: ['!supportsBookmarkFolders'],
},
interactions: {
route: 'interactions',
icon: 'bell',
- label: 'nav.interactions'
+ label: 'nav.interactions',
},
chats: {
route: 'chats',
@@ -80,7 +77,7 @@ export const ROOT_ITEMS = {
label: 'nav.chats',
badgeStyle: 'notification',
badgeGetter: 'unreadChatCount',
- criteria: ['chats']
+ criteria: ['chats'],
},
friendRequests: {
route: 'friend-requests',
@@ -88,13 +85,13 @@ export const ROOT_ITEMS = {
label: 'nav.friend_requests',
badgeStyle: 'notification',
criteria: ['lockedUser'],
- badgeGetter: 'followRequestCount'
+ badgeGetter: 'followRequestCount',
},
about: {
route: 'about',
anon: true,
icon: 'info-circle',
- label: 'nav.about'
+ label: 'nav.about',
},
announcements: {
route: 'announcements',
@@ -103,18 +100,18 @@ export const ROOT_ITEMS = {
store: 'announcements',
badgeStyle: 'notification',
badgeGetter: 'unreadAnnouncementCount',
- criteria: ['announcements']
+ criteria: ['announcements'],
},
drafts: {
route: 'drafts',
icon: 'file-pen',
label: 'nav.drafts',
badgeStyle: 'neutral',
- badgeGetter: 'draftCount'
- }
+ badgeGetter: 'draftCount',
+ },
}
-export function routeTo (item, currentUser) {
+export function routeTo(item, currentUser) {
if (!item.route && !item.routeObject) return null
let route
@@ -122,7 +119,7 @@ export function routeTo (item, currentUser) {
if (item.routeObject) {
route = item.routeObject
} else {
- route = { name: (item.anon || currentUser) ? item.route : item.anonRoute }
+ route = { name: item.anon || currentUser ? item.route : item.anonRoute }
}
if (USERNAME_ROUTES.has(route.name)) {
diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js
index 11db1c9e3..7a43000ce 100644
--- a/src/components/navigation/navigation_entry.js
+++ b/src/components/navigation/navigation_entry.js
@@ -1,48 +1,57 @@
+import { mapState as mapPiniaState, mapStores } from 'pinia'
import { mapState } from 'vuex'
+
import { routeTo } from 'src/components/navigation/navigation.js'
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
+
+import { useAnnouncementsStore } from 'src/stores/announcements.js'
+import { useSyncConfigStore } from 'src/stores/sync_config.js'
+
import { library } from '@fortawesome/fontawesome-svg-core'
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
-import { mapStores, mapState as mapPiniaState } from 'pinia'
-
-import { useAnnouncementsStore } from 'src/stores/announcements'
-import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
library.add(faThumbtack)
const NavigationEntry = {
props: ['item', 'showPin'],
components: {
- OptionalRouterLink
+ OptionalRouterLink,
},
methods: {
- isPinned (value) {
+ isPinned(value) {
return this.pinnedItems.has(value)
},
- togglePin (value) {
+ togglePin(value) {
if (this.isPinned(value)) {
- useServerSideStorageStore().removeCollectionPreference({ path: 'collections.pinnedNavItems', value })
+ useSyncConfigStore().removeCollectionPreference({
+ path: 'collections.pinnedNavItems',
+ value,
+ })
} else {
- useServerSideStorageStore().addCollectionPreference({ path: 'collections.pinnedNavItems', value })
+ useSyncConfigStore().addCollectionPreference({
+ path: 'collections.pinnedNavItems',
+ value,
+ })
}
- useServerSideStorageStore().pushServerSideStorage()
- }
+ useSyncConfigStore().pushSyncConfig()
+ },
},
computed: {
- routeTo () {
+ routeTo() {
return routeTo(this.item, this.currentUser)
},
- getters () {
+ getters() {
return this.$store.getters
},
...mapStores(useAnnouncementsStore),
...mapState({
- currentUser: state => state.users.currentUser
+ currentUser: (state) => state.users.currentUser,
}),
- ...mapPiniaState(useServerSideStorageStore, {
- pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
+ ...mapPiniaState(useSyncConfigStore, {
+ pinnedItems: (store) =>
+ new Set(store.prefsStorage.collections.pinnedNavItems),
}),
- }
+ },
}
export default NavigationEntry
diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js
index f9a900fc6..c3649e4b3 100644
--- a/src/components/navigation/navigation_pins.js
+++ b/src/components/navigation/navigation_pins.js
@@ -1,27 +1,37 @@
-import { mapState } from 'vuex'
import { mapState as mapPiniaState } from 'pinia'
-import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js'
-import { getBookmarkFolderEntries, getListEntries, filterNavigation } from 'src/components/navigation/filter.js'
+import { mapState } from 'vuex'
-import StillImage from 'src/components/still-image/still-image.vue'
+import {
+ filterNavigation,
+ getBookmarkFolderEntries,
+ getListEntries,
+} from 'src/components/navigation/filter.js'
+import {
+ ROOT_ITEMS,
+ routeTo,
+ TIMELINES,
+} from 'src/components/navigation/navigation.js'
+
+import { useAnnouncementsStore } from 'src/stores/announcements'
+import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useListsStore } from 'src/stores/lists'
+import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
- faUsers,
- faGlobe,
- faCity,
- faBookmark,
- faEnvelope,
- faComments,
faBell,
+ faBookmark,
+ faCity,
+ faComments,
+ faEnvelope,
+ faGlobe,
faInfoCircle,
+ faList,
faStream,
- faList
+ faUsers,
} from '@fortawesome/free-solid-svg-icons'
-import { useListsStore } from 'src/stores/lists'
-import { useAnnouncementsStore } from 'src/stores/announcements'
-import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
-import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
library.add(
faUsers,
@@ -33,85 +43,85 @@ library.add(
faBell,
faInfoCircle,
faStream,
- faList
+ faList,
)
const NavPanel = {
props: ['limit'],
methods: {
- getRouteTo (item) {
+ getRouteTo(item) {
return routeTo(item, this.currentUser)
- }
- },
- components: {
- StillImage
+ },
},
+ components: {},
computed: {
- getters () {
+ getters() {
return this.$store.getters
},
...mapPiniaState(useListsStore, {
- lists: getListEntries
+ lists: getListEntries,
}),
...mapPiniaState(useAnnouncementsStore, {
- supportsAnnouncements: store => store.supportsAnnouncements
+ supportsAnnouncements: (store) => store.supportsAnnouncements,
}),
...mapPiniaState(useBookmarkFoldersStore, {
bookmarks: getBookmarkFolderEntries,
}),
- ...mapPiniaState(useServerSideStorageStore, {
- pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems)
+ ...mapPiniaState(useSyncConfigStore, {
+ pinnedItems: (store) =>
+ new Set(store.prefsStorage.collections.pinnedNavItems),
}),
+ ...mapPiniaState(useInstanceStore, ['privateMode', 'federating']),
+ ...mapPiniaState(useInstanceCapabilitiesStore, [
+ 'pleromaChatMessagesAvailable',
+ 'localBubble',
+ ]),
...mapState({
- currentUser: state => state.users.currentUser,
- followRequestCount: state => state.api.followRequests.length,
- privateMode: state => state.instance.private,
- federating: state => state.instance.federating,
- pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
- bubbleTimeline: state => state.instance.localBubbleInstances.length > 0
+ currentUser: (state) => state.users.currentUser,
+ followRequestCount: (state) => state.api.followRequests.length,
}),
- pinnedList () {
+ pinnedList() {
if (!this.currentUser) {
- return filterNavigation([
- { ...TIMELINES.public, name: 'public' },
- { ...TIMELINES.twkn, name: 'twkn' },
- { ...ROOT_ITEMS.about, name: 'about' }
- ],
- {
- hasChats: this.pleromaChatMessagesAvailable,
- hasAnnouncements: this.supportsAnnouncements,
- isFederating: this.federating,
- isPrivate: this.privateMode,
- currentUser: this.currentUser,
- supportsBubbleTimeline: this.bubbleTimeline,
- supportsBookmarkFolders: this.bookmarks
- })
+ return filterNavigation(
+ [
+ { ...TIMELINES.public, name: 'public' },
+ { ...TIMELINES.twkn, name: 'twkn' },
+ { ...ROOT_ITEMS.about, name: 'about' },
+ ],
+ {
+ hasChats: this.pleromaChatMessagesAvailable,
+ hasAnnouncements: this.supportsAnnouncements,
+ isFederating: this.federating,
+ isPrivate: this.privateMode,
+ currentUser: this.currentUser,
+ supportsBubbleTimeline: this.localBubble,
+ supportsBookmarkFolders: this.bookmarks,
+ },
+ )
}
return filterNavigation(
[
- ...Object
- .entries({ ...TIMELINES })
+ ...Object.entries({ ...TIMELINES })
.filter(([k]) => this.pinnedItems.has(k))
.map(([k, v]) => ({ ...v, name: k })),
...this.lists.filter((k) => this.pinnedItems.has(k.name)),
...this.bookmarks.filter((k) => this.pinnedItems.has(k.name)),
- ...Object
- .entries({ ...ROOT_ITEMS })
+ ...Object.entries({ ...ROOT_ITEMS })
.filter(([k]) => this.pinnedItems.has(k))
- .map(([k, v]) => ({ ...v, name: k }))
+ .map(([k, v]) => ({ ...v, name: k })),
],
{
hasChats: this.pleromaChatMessagesAvailable,
hasAnnouncements: this.supportsAnnouncements,
- supportsBubbleTimeline: this.bubbleTimeline,
+ supportsBubbleTimeline: this.localBubble,
supportsBookmarkFolders: this.bookmarks,
isFederating: this.federating,
isPrivate: this.privateMode,
- currentUser: this.currentUser
- }
+ currentUser: this.currentUser,
+ },
).slice(0, this.limit)
- }
- }
+ },
+ },
}
export default NavPanel
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 3edf21de7..856850920 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,29 +1,36 @@
-import StatusContent from '../status_content/status_content.vue'
+import { defineAsyncComponent } from 'vue'
import { mapState } from 'vuex'
-import Status from '../status/status.vue'
-import UserAvatar from '../user_avatar/user_avatar.vue'
-import UserCard from '../user_card/user_card.vue'
-import Timeago from '../timeago/timeago.vue'
-import Report from '../report/report.vue'
-import UserLink from '../user_link/user_link.vue'
-import RichContent from 'src/components/rich_content/rich_content.jsx'
-import UserPopover from '../user_popover/user_popover.vue'
-import ConfirmModal from '../confirm_modal/confirm_modal.vue'
+
+import Report from 'src/components/report/report.vue'
+import StatusContent from 'src/components/status_content/status_content.vue'
+import Timeago from 'src/components/timeago/timeago.vue'
+import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
+import UserLink from 'src/components/user_link/user_link.vue'
+import UserPopover from 'src/components/user_popover/user_popover.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
-import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
+import {
+ highlightClass,
+ highlightStyle,
+} from '../../services/user_highlighter/user_highlighter.js'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+import { useUserHighlightStore } from 'src/stores/user_highlight.js'
+
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCheck,
- faTimes,
- faStar,
- faRetweet,
- faUserPlus,
- faEyeSlash,
- faUser,
- faSuitcaseRolling,
+ faCompressAlt,
faExpandAlt,
- faCompressAlt
+ faEyeSlash,
+ faRetweet,
+ faStar,
+ faSuitcaseRolling,
+ faTimes,
+ faUser,
+ faUserPlus,
} from '@fortawesome/free-solid-svg-icons'
library.add(
@@ -36,17 +43,17 @@ library.add(
faEyeSlash,
faSuitcaseRolling,
faExpandAlt,
- faCompressAlt
+ faCompressAlt,
)
const Notification = {
- data () {
+ data() {
return {
selecting: false,
statusExpanded: false,
unmuted: false,
showingApproveConfirmDialog: false,
- showingDenyConfirmDialog: false
+ showingDenyConfirmDialog: false,
}
},
props: ['notification'],
@@ -54,148 +61,166 @@ const Notification = {
components: {
StatusContent,
UserAvatar,
- UserCard,
Timeago,
- Status,
+
Report,
- RichContent,
+
UserPopover,
UserLink,
- ConfirmModal
+ ConfirmModal: defineAsyncComponent(
+ () => import('src/components/confirm_modal/confirm_modal.vue'),
+ ),
},
- mounted () {
+ mounted() {
document.addEventListener('selectionchange', this.onContentSelect)
},
- unmounted () {
+ unmounted() {
document.removeEventListener('selectionchange', this.onContentSelect)
},
methods: {
- toggleStatusExpanded () {
+ toggleStatusExpanded() {
if (!this.expandable) return
this.statusExpanded = !this.statusExpanded
},
- onContentSelect () {
+ onContentSelect() {
const { isCollapsed, anchorNode, offsetNode } = document.getSelection()
if (isCollapsed) {
this.selecting = false
return
}
- const within = this.$refs.root.contains(anchorNode) || this.$refs.root.contains(offsetNode)
+ const within =
+ this.$refs.root.contains(anchorNode) ||
+ this.$refs.root.contains(offsetNode)
if (within) {
this.selecting = true
} else {
this.selecting = false
}
},
- onContentClick (e) {
- if (!this.selecting && !e.target.closest('a') && !e.target.closest('button')) {
+ onContentClick(e) {
+ if (
+ !this.selecting &&
+ !e.target.closest('a') &&
+ !e.target.closest('button')
+ ) {
this.toggleStatusExpanded()
}
},
- generateUserProfileLink (user) {
- return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
+ generateUserProfileLink(user) {
+ return generateProfileLink(
+ user.id,
+ user.screen_name,
+ useInstanceStore().restrictedNicknames,
+ )
},
- getUser (notification) {
+ getUser(notification) {
return this.$store.state.users.usersObject[notification.from_profile.id]
},
- interacted () {
+ interacted() {
this.$emit('interacted')
},
- toggleMute () {
+ toggleMute() {
this.unmuted = !this.unmuted
},
- showApproveConfirmDialog () {
+ showApproveConfirmDialog() {
this.showingApproveConfirmDialog = true
},
- hideApproveConfirmDialog () {
+ hideApproveConfirmDialog() {
this.showingApproveConfirmDialog = false
},
- showDenyConfirmDialog () {
+ showDenyConfirmDialog() {
this.showingDenyConfirmDialog = true
},
- hideDenyConfirmDialog () {
+ hideDenyConfirmDialog() {
this.showingDenyConfirmDialog = false
},
- approveUser () {
+ approveUser() {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
- doApprove () {
- this.$emit('interacted')
+ doApprove() {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
- this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
+ this.$store.dispatch('markSingleNotificationAsSeen', {
+ id: this.notification.id,
+ })
this.$store.dispatch('updateNotification', {
id: this.notification.id,
- updater: notification => {
+ updater: (notification) => {
notification.type = 'follow'
- }
+ },
})
this.hideApproveConfirmDialog()
},
- denyUser () {
+ denyUser() {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
- doDeny () {
- this.$emit('interacted')
- this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
+ doDeny() {
+ this.$store.state.api.backendInteractor
+ .denyUser({ id: this.user.id })
.then(() => {
- this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
+ this.$store.dispatch('dismissNotificationLocal', {
+ id: this.notification.id,
+ })
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog()
- }
+ },
},
computed: {
- userClass () {
+ userClass() {
return highlightClass(this.notification.from_profile)
},
- userStyle () {
- const highlight = this.$store.getters.mergedConfig.highlight
- const user = this.notification.from_profile
- return highlightStyle(highlight[user.screen_name])
+ userStyle() {
+ const user = this.notification.from_profile.screen_name
+ return highlightStyle(useUserHighlightStore().get(user))
},
- expandable () {
- return (new Set(['like', 'pleroma:emoji_reaction', 'repeat'])).has(this.notification.type)
+ expandable() {
+ return new Set(['like', 'pleroma:emoji_reaction', 'repeat', 'poll']).has(
+ this.notification.type,
+ )
},
- user () {
+ user() {
return this.$store.getters.findUser(this.notification.from_profile.id)
},
- userProfileLink () {
+ userProfileLink() {
return this.generateUserProfileLink(this.user)
},
- targetUser () {
+ targetUser() {
return this.$store.getters.findUser(this.notification.target.id)
},
- targetUserProfileLink () {
+ targetUserProfileLink() {
return this.generateUserProfileLink(this.targetUser)
},
- needMute () {
+ needMute() {
return this.$store.getters.relationship(this.user.id).muting
},
- isStatusNotification () {
+ isStatusNotification() {
return isStatusNotification(this.notification.type)
},
- mergedConfig () {
- return this.$store.getters.mergedConfig
+ mergedConfig() {
+ return useMergedConfigStore().mergedConfig
},
- shouldConfirmApprove () {
+ allowNonSquareEmoji() {
+ return this.mergedConfig.nonSquareEmoji
+ },
+ shouldConfirmApprove() {
return this.mergedConfig.modalOnApproveFollow
},
- shouldConfirmDeny () {
+ shouldConfirmDeny() {
return this.mergedConfig.modalOnDenyFollow
},
...mapState({
- currentUser: state => state.users.currentUser
- })
- }
+ currentUser: (state) => state.users.currentUser,
+ }),
+ },
}
export default Notification
diff --git a/src/components/notification/notification.style.js b/src/components/notification/notification.style.js
index 05c1f6f23..49e28cf2e 100644
--- a/src/components/notification/notification.style.js
+++ b/src/components/notification/notification.style.js
@@ -7,7 +7,7 @@ export default {
'Icon',
'Border',
'Avatar',
- 'PollGraph'
+ 'PollGraph',
],
- defaultRules: []
+ defaultRules: [],
}
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 7165289dc..39bd15426 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -7,7 +7,7 @@
class="Notification"
:compact="true"
:statusoid="notification.status"
- @interacted="interacted"
+ @click="interacted"
/>
@@ -136,6 +137,7 @@
:src="notification.emoji_url"
:alt="notification.emoji"
:title="notification.emoji"
+ :class="{ ['-wide']: allowNonSquareEmoji }"
>
-
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
-
-
+
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
-
+
diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue
index 0b740ea0a..27e1bb179 100644
--- a/src/components/notifications/notification_filters.vue
+++ b/src/components/notifications/notification_filters.vue
@@ -106,31 +106,33 @@
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index ced97d57f..49349b127 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -1,28 +1,32 @@
+import { mapState } from 'pinia'
import { computed } from 'vue'
import { mapGetters } from 'vuex'
-import { mapState } from 'pinia'
-import Notification from '../notification/notification.vue'
-import ExtraNotifications from '../extra_notifications/extra_notifications.vue'
-import NotificationFilters from './notification_filters.vue'
-import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
-import {
- notificationsFromStore,
- filteredNotificationsFromStore,
- unseenNotificationsFromStore,
- countExtraNotifications,
- ACTIONABLE_NOTIFICATION_TYPES
-} from '../../services/notification_utils/notification_utils.js'
-import FaviconService from '../../services/favicon_service/favicon_service.js'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import { faCircleNotch, faArrowUp, faMinus } from '@fortawesome/free-solid-svg-icons'
-import { useInterfaceStore } from 'src/stores/interface'
-import { useAnnouncementsStore } from 'src/stores/announcements'
-library.add(
- faCircleNotch,
+import ExtraNotifications from 'src/components/extra_notifications/extra_notifications.vue'
+import Notification from 'src/components/notification/notification.vue'
+import FaviconService from '../../services/favicon_service/favicon_service.js'
+import {
+ ACTIONABLE_NOTIFICATION_TYPES,
+ countExtraNotifications,
+ filteredNotificationsFromStore,
+ notificationsFromStore,
+ unseenNotificationsFromStore,
+} from '../../services/notification_utils/notification_utils.js'
+import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
+import NotificationFilters from './notification_filters.vue'
+
+import { useAnnouncementsStore } from 'src/stores/announcements.js'
+import { useInterfaceStore } from 'src/stores/interface.js'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
faArrowUp,
- faMinus
-)
+ faCircleNotch,
+ faMinus,
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(faCircleNotch, faArrowUp, faMinus)
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
@@ -30,7 +34,7 @@ const Notifications = {
components: {
Notification,
NotificationFilters,
- ExtraNotifications
+ ExtraNotifications,
},
props: {
// Disables panel styles, unread mark, potentially other notification-related actions
@@ -41,93 +45,124 @@ const Notifications = {
// Do not show extra notifications
noExtra: {
type: Boolean,
- default: false
+ default: false,
},
// Disable teleporting (i.e. for /users/user/notifications)
- disableTeleport: Boolean
+ disableTeleport: Boolean,
},
- data () {
+ data() {
return {
showScrollTop: false,
bottomedOut: false,
// How many seen notifications to display in the list. The more there are,
// the heavier the page becomes. This count is increased when loading
// older notifications, and cut back to default whenever hitting "Read!".
- seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
+ seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT,
}
},
- provide () {
+ provide() {
return {
- popoversZLayer: computed(() => this.popoversZLayer)
+ popoversZLayer: computed(() => this.popoversZLayer),
}
},
computed: {
- mainClass () {
+ mainClass() {
return this.minimalMode ? '' : 'panel panel-default'
},
- notifications () {
+ notifications() {
return notificationsFromStore(this.$store)
},
- error () {
+ error() {
return this.$store.state.notifications.error
},
- unseenNotifications () {
- return unseenNotificationsFromStore(this.$store)
+ unseenNotifications() {
+ return unseenNotificationsFromStore(
+ this.$store,
+ useMergedConfigStore().mergedConfig.notificationVisibility,
+ useMergedConfigStore().mergedConfig.ignoreInactionableSeen,
+ )
},
- filteredNotifications () {
+ filteredNotifications() {
if (this.unseenAtTop) {
return [
- ...filteredNotificationsFromStore(this.$store).filter(n => this.shouldShowUnseen(n)),
- ...filteredNotificationsFromStore(this.$store).filter(n => !this.shouldShowUnseen(n))
+ ...filteredNotificationsFromStore(
+ this.$store,
+ useMergedConfigStore().mergedConfig.notificationVisibility,
+ ).filter((n) => this.shouldShowUnseen(n)),
+ ...filteredNotificationsFromStore(
+ this.$store,
+ useMergedConfigStore().mergedConfig.notificationVisibility,
+ ).filter((n) => !this.shouldShowUnseen(n)),
]
} else {
- return filteredNotificationsFromStore(this.$store, this.filterMode)
+ return filteredNotificationsFromStore(
+ this.$store,
+ useMergedConfigStore().mergedConfig.notificationVisibility,
+ this.filterMode,
+ )
}
},
- unseenCountBadgeText () {
+ unseenCountBadgeText() {
return `${this.unseenCount ? this.unseenCount : ''}${this.extraNotificationsCount ? '*' : ''}`
},
- unseenCount () {
+ unseenCount() {
return this.unseenNotifications.length
},
- ignoreInactionableSeen () { return this.$store.getters.mergedConfig.ignoreInactionableSeen },
- extraNotificationsCount () {
- return countExtraNotifications(this.$store)
+ ignoreInactionableSeen() {
+ return useMergedConfigStore().mergedConfig.ignoreInactionableSeen
},
- unseenCountTitle () {
- return this.unseenNotifications.length + (this.unreadChatCount) + this.unreadAnnouncementCount
+ extraNotificationsCount() {
+ return countExtraNotifications(
+ this.$store,
+ useMergedConfigStore().mergedConfig,
+ useAnnouncementsStore().unreadAnnouncementCount,
+ )
},
- loading () {
+ unseenCountTitle() {
+ return (
+ this.unseenNotifications.length +
+ this.unreadChatCount +
+ this.unreadAnnouncementCount
+ )
+ },
+ loading() {
return this.$store.state.notifications.loading
},
- noHeading () {
+ noHeading() {
const { layoutType } = useInterfaceStore()
return this.minimalMode || layoutType === 'mobile'
},
- teleportTarget () {
+ teleportTarget() {
const { layoutType } = useInterfaceStore()
const map = {
wide: '#notifs-column',
- mobile: '#mobile-notifications'
+ mobile: '#mobile-notifications',
}
return map[layoutType] || '#notifs-sidebar'
},
- popoversZLayer () {
+ popoversZLayer() {
const { layoutType } = useInterfaceStore()
return layoutType === 'mobile' ? 'navbar' : null
},
- notificationsToDisplay () {
- return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
+ notificationsToDisplay() {
+ return this.filteredNotifications.slice(
+ 0,
+ this.unseenCount + this.seenToDisplayCount,
+ )
},
- noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
- unseenAtTop () { return this.$store.getters.mergedConfig.unseenAtTop },
- showExtraNotifications () {
+ noSticky() {
+ return useMergedConfigStore().mergedConfig.disableStickyHeaders
+ },
+ unseenAtTop() {
+ return useMergedConfigStore().mergedConfig.unseenAtTop
+ },
+ showExtraNotifications() {
return !this.noExtra
},
...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']),
- ...mapGetters(['unreadChatCount'])
+ ...mapGetters(['unreadChatCount']),
},
- mounted () {
+ mounted() {
this.scrollerRef = this.$refs.root.closest('.column.-scrollable')
if (!this.scrollerRef) {
this.scrollerRef = this.$refs.root.closest('.mobile-notifications')
@@ -137,12 +172,12 @@ const Notifications = {
}
this.scrollerRef.addEventListener('scroll', this.updateScrollPosition)
},
- unmounted () {
+ unmounted() {
if (!this.scrollerRef) return
this.scrollerRef.removeEventListener('scroll', this.updateScrollPosition)
},
watch: {
- unseenCountTitle (count) {
+ unseenCountTitle(count) {
if (count > 0) {
FaviconService.drawFaviconBadge()
useInterfaceStore().setPageTitle(`(${count})`)
@@ -151,10 +186,13 @@ const Notifications = {
useInterfaceStore().setPageTitle('')
}
},
- teleportTarget () {
+ teleportTarget() {
// handle scroller change
this.$nextTick(() => {
- this.scrollerRef.removeEventListener('scroll', this.updateScrollPosition)
+ this.scrollerRef.removeEventListener(
+ 'scroll',
+ this.updateScrollPosition,
+ )
this.scrollerRef = this.$refs.root.closest('.column.-scrollable')
if (!this.scrollerRef) {
this.scrollerRef = this.$refs.root.closest('.mobile-notifications')
@@ -162,17 +200,18 @@ const Notifications = {
this.scrollerRef.addEventListener('scroll', this.updateScrollPosition)
this.updateScrollPosition()
})
- }
+ },
},
methods: {
- scrollToTop () {
+ scrollToTop() {
const scrollable = this.scrollerRef
scrollable.scrollTo({ top: this.$refs.root.offsetTop })
},
- updateScrollPosition () {
- this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop
+ updateScrollPosition() {
+ this.showScrollTop =
+ this.$refs.root.offsetTop < this.scrollerRef.scrollTop
},
- shouldShowUnseen (notification) {
+ shouldShowUnseen(notification) {
if (notification.seen) return false
const actionable = ACTIONABLE_NOTIFICATION_TYPES.has(notification.type)
@@ -182,26 +221,29 @@ const Notifications = {
* everything else (likes/repeats/reacts) cannot be acted and therefore we just clear
* the "seen" status upon any clicks on them
*/
- notificationClicked (notification) {
+ notificationClicked(notification) {
const { id } = notification
this.$store.dispatch('notificationClicked', { id })
},
- notificationInteracted (notification) {
+ notificationInteracted(notification) {
const { id } = notification
this.$store.dispatch('markSingleNotificationAsSeen', { id })
},
- markAsSeen () {
+ markAsSeen() {
this.$store.dispatch('markNotificationsAsSeen')
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT
},
- fetchOlderNotifications () {
+ fetchOlderNotifications() {
if (this.loading) {
return
}
const seenCount = this.filteredNotifications.length - this.unseenCount
if (this.seenToDisplayCount < seenCount) {
- this.seenToDisplayCount = Math.min(this.seenToDisplayCount + 20, seenCount)
+ this.seenToDisplayCount = Math.min(
+ this.seenToDisplayCount + 20,
+ seenCount,
+ )
return
} else if (this.seenToDisplayCount > seenCount) {
this.seenToDisplayCount = seenCount
@@ -210,19 +252,21 @@ const Notifications = {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true })
- notificationsFetcher.fetchAndUpdate({
- store,
- credentials,
- older: true
- }).then(notifs => {
- store.commit('setNotificationsLoading', { value: false })
- if (notifs.length === 0) {
- this.bottomedOut = true
- }
- this.seenToDisplayCount += notifs.length
- })
- }
- }
+ notificationsFetcher
+ .fetchAndUpdate({
+ store,
+ credentials,
+ older: true,
+ })
+ .then((notifs) => {
+ store.commit('setNotificationsLoading', { value: false })
+ if (notifs.length === 0) {
+ this.bottomedOut = true
+ }
+ this.seenToDisplayCount += notifs.length
+ })
+ },
+ },
}
export default Notifications
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 79ef5b300..32839ebbf 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -119,6 +119,10 @@
max-width: 1.25em;
height: 1.25em;
width: auto;
+
+ &.-wide {
+ max-width: 3.75em;
+ }
}
.emoji-reaction-emoji-image {
diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js
index 02e4c9ffc..4bcb9803e 100644
--- a/src/components/oauth_callback/oauth_callback.js
+++ b/src/components/oauth_callback/oauth_callback.js
@@ -1,25 +1,29 @@
import oauth from '../../services/new_api/oauth.js'
+
+import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js'
const oac = {
props: ['code'],
- mounted () {
+ mounted() {
if (this.code) {
const oauthStore = useOAuthStore()
const { clientId, clientSecret } = oauthStore
- oauth.getToken({
- clientId,
- clientSecret,
- instance: this.$store.state.instance.server,
- code: this.code
- }).then((result) => {
- oauthStore.setToken(result.access_token)
- this.$store.dispatch('loginUser', result.access_token)
- this.$router.push({ name: 'friends' })
- })
+ oauth
+ .getToken({
+ clientId,
+ clientSecret,
+ instance: useInstanceStore().server,
+ code: this.code,
+ })
+ .then((result) => {
+ oauthStore.setToken(result.access_token)
+ this.$store.dispatch('loginUser', result.access_token)
+ this.$router.push({ name: 'friends' })
+ })
}
- }
+ },
}
export default oac
diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue
index 041219feb..761aeb216 100644
--- a/src/components/opacity_input/opacity_input.vue
+++ b/src/components/opacity_input/opacity_input.vue
@@ -8,7 +8,7 @@
class="label"
:class="{ faint: !present || disabled }"
>
- {{ label }}
+ {{ label || $t('settings.style.themes3.editor.opacity') }}
diff --git a/src/components/optional_router_link/optional_router_link.vue b/src/components/optional_router_link/optional_router_link.vue
index d56ad268a..b0d553801 100644
--- a/src/components/optional_router_link/optional_router_link.vue
+++ b/src/components/optional_router_link/optional_router_link.vue
@@ -18,6 +18,6 @@
diff --git a/src/components/page_list/page_list.js b/src/components/page_list/page_list.js
index 4e1184844..3030b0a4a 100644
--- a/src/components/page_list/page_list.js
+++ b/src/components/page_list/page_list.js
@@ -2,7 +2,7 @@ import SelectableList from 'src/components/selectable_list/selectable_list.vue'
const PageList = {
components: {
- SelectableList
+ SelectableList,
},
props: {
/**
@@ -10,43 +10,43 @@ const PageList = {
*/
boxOnly: {
type: Boolean,
- default: false
+ default: false,
},
/**
* how many entries to fetch at once
*/
pageSize: {
type: Number,
- default: 50
+ default: 50,
},
/**
* the function/callback used to fetch new entries (one page)
*/
fetchPage: {
type: Function,
- default: async () => []
+ default: async () => [],
},
/**
* wether or not this is a single page list (so it won't allow fetching more pages)
*/
singlePage: {
type: Boolean,
- default: false
- }
+ default: false,
+ },
},
- data () {
+ data() {
return {
pageIndex: 1,
items: [],
canLoadMore: true,
- isLoading: false
+ isLoading: false,
}
},
methods: {
/**
* reset and load first page
*/
- reset () {
+ reset() {
this.canLoadMore = true
this.pageIndex = 1
this.items = []
@@ -56,14 +56,14 @@ const PageList = {
/**
* load another page
*/
- loadMore () {
+ loadMore() {
if (!this.isLoading && this.canLoadMore) {
this.isLoading = true
- console.log("is loading = true")
+ console.log('is loading = true')
this.fetchPage(this.$store, {
page: this.pageIndex++,
- pageSize: this.pageSize
- }).then(items => {
+ pageSize: this.pageSize,
+ }).then((items) => {
this.items = [...this.items, ...items]
this.isLoading = false
})
@@ -73,15 +73,15 @@ const PageList = {
* get currently selected elements
* @returns {Array}
*/
- getSelected () {
+ getSelected() {
return this.$refs.list.selected
- }
+ },
},
/**
* auto-load first page when mounted
*/
- mounted () {
+ mounted() {
this.loadMore()
- }
+ },
}
export default PageList
diff --git a/src/components/palette_editor/palette_editor.vue b/src/components/palette_editor/palette_editor.vue
index 252928b9e..eec9fe361 100644
--- a/src/components/palette_editor/palette_editor.vue
+++ b/src/components/palette_editor/palette_editor.vue
@@ -1,60 +1,65 @@
-
updatePalette(key, value)"
- />
-
-
- {{ $t('settings.style.themes3.palette.import') }}
-
-
-
- {{ $t('settings.style.themes3.palette.export') }}
-
-
- {{ $t('settings.style.themes3.palette.apply') }}
-
+
+ updatePalette(key, value)"
+ />
+
+
+
+
+ {{ $t('settings.style.themes3.palette.import') }}
+
+
+
+ {{ $t('settings.style.themes3.palette.export') }}
+
+
+
+
+ {{ $t('settings.style.themes3.palette.apply') }}
+
+
diff --git a/src/components/quote/quote_form.js b/src/components/quote/quote_form.js
new file mode 100644
index 000000000..cffae123b
--- /dev/null
+++ b/src/components/quote/quote_form.js
@@ -0,0 +1,120 @@
+import { debounce } from 'lodash'
+
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import Quote from './quote.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+
+export default {
+ components: {
+ Quote,
+ Checkbox,
+ },
+ name: 'QuoteForm',
+ props: {
+ visible: {
+ type: Boolean,
+ },
+ url: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ id: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ text: this.url,
+ loading: false,
+ error: false,
+ debounceSetQuote: debounce((value) => {
+ this.fetchStatus(value)
+ }, 1000),
+ }
+ },
+ created() {
+ if (this.url && !this.id) {
+ this.fetchStatus(this.url)
+ } else if (this.id) {
+ this.text =
+ window.location.protocol +
+ '//' +
+ this.instanceHost +
+ '/notice/' +
+ this.id
+ }
+ },
+ computed: {
+ instanceHost() {
+ return new URL(useInstanceStore().server).host
+ },
+ noticeRegex() {
+ return new RegExp(
+ `^([^/:]*:?//|)(${window.location.host}|${this.instanceHost})/notice/(.*)$`,
+ )
+ },
+ quoteVisible() {
+ return (!!this.id || this.loading) && !this.error
+ },
+ },
+ watch: {
+ text(value) {
+ this.debounceSetQuote(value)
+ this.$emit('update:url', value)
+ },
+ visible(value) {
+ if (value && this.url) {
+ this.fetchStatus(this.url)
+ }
+ },
+ },
+ methods: {
+ clear() {
+ this.text = this.url
+ this.loading = false
+ this.error = false
+ },
+ setLoading(value) {
+ this.loading = value
+ },
+ handleError(error) {
+ this.id = null
+ this.error = !!error
+ },
+ fetchStatus(value) {
+ this.error = false
+
+ const notice = this.noticeRegex.exec(value)
+ if (notice && notice.length === 4) {
+ this.$emit('update:id', notice[3])
+ } else if (value) {
+ this.loading = true
+ this.$store
+ .dispatch('search', {
+ q: value,
+ resolve: true,
+ offset: 0,
+ limit: 1,
+ type: 'statuses',
+ })
+ .then((data) => {
+ if (data && data.statuses && data.statuses.length === 1) {
+ this.$emit('update:id', data.statuses[0].id)
+ } else {
+ this.handleError(true)
+ }
+ })
+ .catch(this.handleError)
+ .finally(() => {
+ this.loading = false
+ })
+ } else {
+ this.$emit('update:id', null)
+ }
+ },
+ },
+}
diff --git a/src/components/quote/quote_form.vue b/src/components/quote/quote_form.vue
new file mode 100644
index 000000000..2e7200d7f
--- /dev/null
+++ b/src/components/quote/quote_form.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
diff --git a/src/components/quotes_timeline/quotes_timeline.js b/src/components/quotes_timeline/quotes_timeline.js
index a5f42da56..f92f109be 100644
--- a/src/components/quotes_timeline/quotes_timeline.js
+++ b/src/components/quotes_timeline/quotes_timeline.js
@@ -1,26 +1,36 @@
-import Timeline from '../timeline/timeline.vue'
+import Timeline from 'src/components/timeline/timeline.vue'
const QuotesTimeline = {
- created () {
+ created() {
this.$store.commit('clearTimeline', { timeline: 'quotes' })
- this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId })
+ this.$store.dispatch('startFetchingTimeline', {
+ timeline: 'quotes',
+ statusId: this.statusId,
+ })
},
components: {
- Timeline
+ Timeline,
},
computed: {
- statusId () { return this.$route.params.id },
- timeline () { return this.$store.state.statuses.timelines.quotes }
+ statusId() {
+ return this.$route.params.id
+ },
+ timeline() {
+ return this.$store.state.statuses.timelines.quotes
+ },
},
watch: {
- statusId () {
+ statusId() {
this.$store.commit('clearTimeline', { timeline: 'quotes' })
- this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId })
- }
+ this.$store.dispatch('startFetchingTimeline', {
+ timeline: 'quotes',
+ statusId: this.statusId,
+ })
+ },
},
- unmounted () {
+ unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'quotes')
- }
+ },
}
export default QuotesTimeline
diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue
index 2f8645c0b..91d3dcc3b 100644
--- a/src/components/range_input/range_input.vue
+++ b/src/components/range_input/range_input.vue
@@ -54,13 +54,22 @@
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 53a678680..60f0fd16b 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,13 +1,20 @@
import useVuelidate from '@vuelidate/core'
import { required, requiredIf, sameAs } from '@vuelidate/validators'
+import { mapState as mapPiniaState } from 'pinia'
import { mapActions, mapState } from 'vuex'
-import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
+
+import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
+import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue'
import localeService from '../../services/locale/locale.service.js'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+
import { DAY } from 'src/services/date_utils/date_utils.js'
-import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
const registration = {
- setup () { return { v$: useVuelidate() } },
+ setup() {
+ return { v$: useVuelidate() }
+ },
data: () => ({
user: {
email: '',
@@ -17,15 +24,15 @@ const registration = {
confirm: '',
birthday: '',
reason: '',
- language: ['']
+ language: [''],
},
- captcha: {}
+ captcha: {},
}),
components: {
InterfaceLanguageSwitcher,
- TermsOfServicePanel
+ TermsOfServicePanel,
},
- validations () {
+ validations() {
return {
user: {
email: { required: requiredIf(() => this.accountActivationRequired) },
@@ -34,20 +41,23 @@ const registration = {
password: { required },
confirm: {
required,
- sameAs: sameAs(this.user.password)
+ sameAs: sameAs(this.user.password),
},
birthday: {
required: requiredIf(() => this.birthdayRequired),
- maxValue: value => {
- return !this.birthdayRequired || new Date(value).getTime() <= this.birthdayMin.getTime()
- }
+ maxValue: (value) => {
+ return (
+ !this.birthdayRequired ||
+ new Date(value).getTime() <= this.birthdayMin.getTime()
+ )
+ },
},
reason: { required: requiredIf(() => this.accountApprovalRequired) },
- language: {}
- }
+ language: {},
+ },
}
},
- created () {
+ created() {
if ((!this.registrationOpen && !this.token) || this.signedIn) {
this.$router.push({ name: 'root' })
}
@@ -55,14 +65,16 @@ const registration = {
this.setCaptcha()
},
computed: {
- token () { return this.$route.params.token },
- bioPlaceholder () {
+ token() {
+ return this.$route.params.token
+ },
+ bioPlaceholder() {
return this.replaceNewlines(this.$t('registration.bio_placeholder'))
},
- reasonPlaceholder () {
+ reasonPlaceholder() {
return this.replaceNewlines(this.$t('registration.reason_placeholder'))
},
- birthdayMin () {
+ birthdayMin() {
const minAge = this.birthdayMinAge
const today = new Date()
today.setUTCMilliseconds(0)
@@ -73,31 +85,41 @@ const registration = {
minDate.setTime(today.getTime() - minAge * DAY)
return minDate
},
- birthdayMinAttr () {
+ birthdayMinAttr() {
return this.birthdayMin.toJSON().replace(/T.+$/, '')
},
- birthdayMinFormatted () {
- const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
- return this.user.birthday && new Date(Date.parse(this.birthdayMin)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' })
+ birthdayMinFormatted() {
+ const browserLocale = localeService.internalToBrowserLocale(
+ this.$i18n.locale,
+ )
+ return (
+ this.user.birthday &&
+ new Date(Date.parse(this.birthdayMin)).toLocaleDateString(
+ browserLocale,
+ { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' },
+ )
+ )
},
+ ...mapPiniaState(useInstanceStore, {
+ registrationOpen: (store) => store.registrationOpen,
+ embeddedToS: (store) => store.embeddedToS,
+ termsOfService: (store) => store.tos,
+ accountActivationRequired: (store) => store.accountActivationRequired,
+ accountApprovalRequired: (store) => store.accountApprovalRequired,
+ birthdayRequired: (store) => store.birthdayRequired,
+ birthdayMinAge: (store) => store.birthdayMinAge,
+ }),
...mapState({
- registrationOpen: (state) => state.instance.registrationOpen,
signedIn: (state) => !!state.users.currentUser,
isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors,
signUpNotice: (state) => state.users.signUpNotice,
hasSignUpNotice: (state) => !!state.users.signUpNotice.message,
- termsOfService: (state) => state.instance.tos,
- embeddedToS: (state) => state.instance.embeddedToS,
- accountActivationRequired: (state) => state.instance.accountActivationRequired,
- accountApprovalRequired: (state) => state.instance.accountApprovalRequired,
- birthdayRequired: (state) => state.instance.birthdayRequired,
- birthdayMinAge: (state) => state.instance.birthdayMinAge
- })
+ }),
},
methods: {
...mapActions(['signUp', 'getCaptcha']),
- async submit () {
+ async submit() {
this.user.nickname = this.user.username
this.user.token = this.token
@@ -105,7 +127,9 @@ const registration = {
this.user.captcha_token = this.captcha.token
this.user.captcha_answer_data = this.captcha.answer_data
if (this.user.language) {
- this.user.language = localeService.internalToBackendLocaleMulti(this.user.language.filter(k => k))
+ this.user.language = localeService.internalToBackendLocaleMulti(
+ this.user.language.filter((k) => k),
+ )
}
this.v$.$touch()
@@ -124,13 +148,15 @@ const registration = {
}
}
},
- setCaptcha () {
- this.getCaptcha().then(cpt => { this.captcha = cpt })
+ setCaptcha() {
+ this.getCaptcha().then((cpt) => {
+ this.captcha = cpt
+ })
},
- replaceNewlines (str) {
+ replaceNewlines(str) {
return str.replace(/\s*\n\s*/g, ' \n')
- }
- }
+ },
+ },
}
export default registration
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index f6b056e6b..b969a416e 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -395,7 +395,7 @@
}
form textarea {
- line-height: 16px;
+ line-height: 1;
resize: vertical;
}
diff --git a/src/components/remote_follow/remote_follow.js b/src/components/remote_follow/remote_follow.js
index 951b59419..a583b6f09 100644
--- a/src/components/remote_follow/remote_follow.js
+++ b/src/components/remote_follow/remote_follow.js
@@ -1,9 +1,9 @@
export default {
props: ['user'],
computed: {
- subscribeUrl () {
+ subscribeUrl() {
const serverUrl = new URL(this.user.statusnet_profile_url)
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
- }
- }
+ },
+ },
}
diff --git a/src/components/remote_user_resolver/remote_user_resolver.js b/src/components/remote_user_resolver/remote_user_resolver.js
index 9b5e511e0..430f56c84 100644
--- a/src/components/remote_user_resolver/remote_user_resolver.js
+++ b/src/components/remote_user_resolver/remote_user_resolver.js
@@ -1,14 +1,16 @@
const RemoteUserResolver = {
data: () => ({
- error: false
+ error: false,
}),
- mounted () {
+ mounted() {
this.redirect()
},
methods: {
- redirect () {
- const acct = this.$route.params.username + '@' + this.$route.params.hostname
- this.$store.state.api.backendInteractor.fetchUser({ id: acct })
+ redirect() {
+ const acct =
+ this.$route.params.username + '@' + this.$route.params.hostname
+ this.$store.state.api.backendInteractor
+ .fetchUser({ id: acct })
.then((externalUser) => {
if (externalUser.error) {
this.error = true
@@ -17,15 +19,15 @@ const RemoteUserResolver = {
const id = externalUser.id
this.$router.replace({
name: 'external-user-profile',
- params: { id }
+ params: { id },
})
}
})
.catch(() => {
this.error = true
})
- }
- }
+ },
+ },
}
export default RemoteUserResolver
diff --git a/src/components/remove_follower_button/remove_follower_button.js b/src/components/remove_follower_button/remove_follower_button.js
index 052a519ff..cbe92dce1 100644
--- a/src/components/remove_follower_button/remove_follower_button.js
+++ b/src/components/remove_follower_button/remove_follower_button.js
@@ -1,48 +1,54 @@
-import ConfirmModal from '../confirm_modal/confirm_modal.vue'
+import { defineAsyncComponent } from 'vue'
+
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
export default {
props: ['user', 'relationship'],
- data () {
+ data() {
return {
inProgress: false,
- showingConfirmRemoveFollower: false
+ showingConfirmRemoveFollower: false,
}
},
components: {
- ConfirmModal
+ ConfirmModal: defineAsyncComponent(
+ () => import('src/components/confirm_modal/confirm_modal.vue'),
+ ),
},
computed: {
- label () {
+ label() {
if (this.inProgress) {
return this.$t('user_card.follow_progress')
} else {
return this.$t('user_card.remove_follower')
}
},
- shouldConfirmRemoveUserFromFollowers () {
- return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
- }
+ shouldConfirmRemoveUserFromFollowers() {
+ return useMergedConfigStore().mergedConfig.modalOnRemoveUserFromFollowers
+ },
},
methods: {
- showConfirmRemoveUserFromFollowers () {
+ showConfirmRemoveUserFromFollowers() {
this.showingConfirmRemoveFollower = true
},
- hideConfirmRemoveUserFromFollowers () {
+ hideConfirmRemoveUserFromFollowers() {
this.showingConfirmRemoveFollower = false
},
- onClick () {
+ onClick() {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
- doRemoveUserFromFollowers () {
+ doRemoveUserFromFollowers() {
this.inProgress = true
- this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => {
- this.inProgress = false
- })
+ this.$store
+ .dispatch('removeUserFromFollowers', this.relationship.id)
+ .then(() => {
+ this.inProgress = false
+ })
this.hideConfirmRemoveUserFromFollowers()
- }
- }
+ },
+ },
}
diff --git a/src/components/remove_follower_button/remove_follower_button.vue b/src/components/remove_follower_button/remove_follower_button.vue
index 3054770d9..93eaf66c4 100644
--- a/src/components/remove_follower_button/remove_follower_button.vue
+++ b/src/components/remove_follower_button/remove_follower_button.vue
@@ -8,7 +8,7 @@
>
{{ label }}
-
-
+
diff --git a/src/components/report/report.js b/src/components/report/report.js
index 6e9f21eea..e9d14c515 100644
--- a/src/components/report/report.js
+++ b/src/components/report/report.js
@@ -1,37 +1,44 @@
-import { useReportsStore } from 'src/stores/reports'
-import Select from '../select/select.vue'
-import StatusContent from '../status_content/status_content.vue'
-import Timeago from '../timeago/timeago.vue'
-import RichContent from 'src/components/rich_content/rich_content.jsx'
+import Select from 'src/components/select/select.vue'
+import StatusContent from 'src/components/status_content/status_content.vue'
+import Timeago from 'src/components/timeago/timeago.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useReportsStore } from 'src/stores/reports.js'
+
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const Report = {
- props: [
- 'reportId'
- ],
+ props: ['reportId'],
components: {
Select,
StatusContent,
Timeago,
- RichContent
},
computed: {
- report () {
+ report() {
return useReportsStore().reports[this.reportId] || {}
},
state: {
- get: function () { return this.report.state },
- set: function (val) { this.setReportState(val) }
- }
+ get: function () {
+ return this.report.state
+ },
+ set: function (val) {
+ this.setReportState(val)
+ },
+ },
},
methods: {
- generateUserProfileLink (user) {
- return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
+ generateUserProfileLink(user) {
+ return generateProfileLink(
+ user.id,
+ user.screen_name,
+ useInstanceStore().restrictedNicknames,
+ )
},
- setReportState (state) {
+ setReportState(state) {
return useReportsStore().setReportState({ id: this.report.id, state })
- }
- }
+ },
+ },
}
export default Report
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index 5adc2443a..fc17a529f 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -1,11 +1,18 @@
-import { unescape, flattenDeep } from 'lodash'
-import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
-import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
-import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
-import StillImageEmojiPopover from 'src/components/still-image/still-image-emoji-popover.vue'
-import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
-import { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.js'
+import { flattenDeep, unescape as ldUnescape } from 'lodash'
+
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
+import { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.js'
+import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
+import StillImage from 'src/components/still-image/still-image.vue'
+import StillImageEmojiPopover from 'src/components/still-image/still-image-emoji-popover.vue'
+
+import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
+import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
+import {
+ getAttrs,
+ getTagName,
+ processTextForEmoji,
+} from 'src/services/html_converter/utility.service.js'
import './rich_content.scss'
@@ -27,7 +34,7 @@ const MAYBE_LINE_BREAKING_ELEMENTS = [
'h2',
'h3',
'h4',
- 'h5'
+ 'h5',
]
/**
@@ -52,46 +59,46 @@ export default {
name: 'RichContent',
components: {
MentionsLine,
- HashtagLink
+ HashtagLink,
},
props: {
// Original html content
html: {
required: true,
- type: String
+ type: String,
},
attentions: {
required: false,
- default: () => []
+ default: () => [],
},
// Emoji object, as in status.emojis, note the "s" at the end...
emoji: {
required: true,
- type: Array
+ type: Array,
},
// Whether to handle links or not (posts: yes, everything else: no)
handleLinks: {
required: false,
type: Boolean,
- default: false
+ default: false,
},
// Meme arrows
greentext: {
required: false,
type: Boolean,
- default: false
+ default: false,
},
// Faint style (for notifs)
faint: {
required: false,
type: Boolean,
- default: false
+ default: false,
},
// Collapse newlines
collapse: {
required: false,
type: Boolean,
- default: false
+ default: false,
},
/* Content comes from current instance
*
@@ -103,11 +110,17 @@ export default {
isLocal: {
required: false,
type: Boolean,
- default: true
- }
+ default: true,
+ },
+ // Allow wide emoji (max 3:1 ratio)
+ allowNonSquareEmoji: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
},
// NEVER EVER TOUCH DATA INSIDE RENDER
- render () {
+ render() {
// Pre-process HTML
const { newHtml: html } = preProcessPerLine(this.html, this.greentext)
let currentMentions = null // Current chain of mentions, we group all mentions together
@@ -124,10 +137,7 @@ export default {
let tagsIndex = 0
const renderImage = (tag) => {
- return
+ return
}
const renderHashtag = (attrs, children, encounteredTextReverse) => {
@@ -137,12 +147,14 @@ export default {
lastTags.push(linkData)
}
const { url, tag, content } = linkData
- return
+ return
}
const renderMention = (attrs, children) => {
const linkData = getLinkData(attrs, children, mentionIndex++)
- linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url)
+ linkData.notifying = this.attentions.some(
+ (a) => a.statusnet_profile_url === linkData.url,
+ )
writtenMentions.push(linkData)
if (currentMentions === null) {
currentMentions = []
@@ -152,7 +164,7 @@ export default {
invisibleMentions.push(linkData)
}
if (currentMentions.length === 1) {
- return
+ return
} else {
return ''
}
@@ -171,26 +183,28 @@ export default {
// in MentionsLine
lastSpacing = item
// Don't remove last space in a container (fixes poast mentions)
- return (index !== array.length - 1) && (currentMentions !== null) ? item.trim() : item
+ return index !== array.length - 1 && currentMentions !== null
+ ? item.trim()
+ : item
}
currentMentions = null
if (item.includes(':')) {
- item = ['', processTextForEmoji(
- item,
- this.emoji,
- ({ shortcode, url }) => {
- return
- }
- )]
+ item = [
+ '',
+ processTextForEmoji(item, this.emoji, ({ shortcode, url }) => {
+ return (
+
+ )
+ }),
+ ]
}
return item
}
@@ -209,18 +223,24 @@ export default {
* we have a tag right next to mentions
*/
const mentionsLinePadding =
- // Padding is only needed if we just finished parsing mentions
- previouslyMentions &&
- // Don't add padding if content is string and has padding already
- !(children && typeof children[0] === 'string' && children[0].match(/^\s/))
- ? lastSpacing
- : ''
+ // Padding is only needed if we just finished parsing mentions
+ previouslyMentions &&
+ // Don't add padding if content is string and has padding already
+ !(
+ children &&
+ typeof children[0] === 'string' &&
+ children[0].match(/^\s/)
+ )
+ ? lastSpacing
+ : ''
if (MAYBE_LINE_BREAKING_ELEMENTS.includes(Tag)) {
// all the elements that can cause a line change
currentMentions = null
- } else if (Tag === 'img') { // replace images with StillImage
+ } else if (Tag === 'img') {
+ // replace images with StillImage
return ['', [mentionsLinePadding, renderImage(opener)], '']
- } else if (Tag === 'a' && this.handleLinks) { // replace mentions with MentionLink
+ } else if (Tag === 'a' && this.handleLinks) {
+ // replace mentions with MentionLink
if (fullAttrs.class && fullAttrs.class.includes('mention')) {
// Handling mentions here
return renderMention(attrs, children)
@@ -228,7 +248,11 @@ export default {
currentMentions = null
}
} else if (Tag === 'span') {
- if (this.handleLinks && fullAttrs.class && fullAttrs.class.includes('h-card')) {
+ if (
+ this.handleLinks &&
+ fullAttrs.class &&
+ fullAttrs.class.includes('h-card')
+ ) {
return ['', children.map(processItem), '']
}
}
@@ -236,11 +260,8 @@ export default {
if (children !== undefined) {
return [
'',
- [
- mentionsLinePadding,
- [opener, children.map(processItem), closer]
- ],
- ''
+ [mentionsLinePadding, [opener, children.map(processItem), closer]],
+ '',
]
} else {
return ['', [mentionsLinePadding, item], '']
@@ -256,29 +277,31 @@ export default {
const emptyText = item.trim() === ''
if (emptyText) return item
if (!encounteredTextReverse) encounteredTextReverse = true
- return unescape(item)
+ return ldUnescape(item)
} else if (Array.isArray(item)) {
// Handle tag nodes
const [opener, children] = item
const Tag = opener === '' ? '' : getTagName(opener)
switch (Tag) {
- case 'a': { // replace mentions with MentionLink
+ case 'a': {
+ // replace mentions with MentionLink
if (!this.handleLinks) break
const fullAttrs = getAttrs(opener, () => true)
const attrs = getAttrs(opener, () => true)
// should only be this
if (
(fullAttrs.class && fullAttrs.class.includes('hashtag')) || // Pleroma style
- (fullAttrs.rel === 'tag') // Mastodon style
+ fullAttrs.rel === 'tag' // Mastodon style
) {
return renderHashtag(attrs, children, encounteredTextReverse)
} else {
attrs.target = '_blank'
- const newChildren = [...children].reverse().map(processItemReverse).reverse()
+ const newChildren = [...children]
+ .reverse()
+ .map(processItemReverse)
+ .reverse()
- return
- { newChildren }
-
+ return {newChildren}
}
}
case '':
@@ -290,11 +313,9 @@ export default {
const newChildren = Array.isArray(children)
? [...children].reverse().map(processItemReverse).reverse()
: children
- return
- { newChildren }
-
+ return {newChildren}
} else {
- return
+ return
}
}
return item
@@ -306,30 +327,35 @@ export default {
// DO NOT USE SLOTS they cause a re-render feedback loop here.
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
// at least until vue3?
- const result =
-
- {
- this.collapse
- ? pass2.map(x => {
- if (!Array.isArray(x)) return x.replace(/\n/g, ' ')
- return x.map(y => y.type === 'br' ? ' ' : y)
- })
- : pass2
- }
-
+ const result = (
+
+ {this.collapse
+ ? pass2.map((x) => {
+ if (!Array.isArray(x)) return x.replace(/\n/g, ' ')
+ return x.map((y) => (y.type === 'br' ? ' ' : y))
+ })
+ : pass2}
+
+ )
const event = {
lastTags,
writtenMentions,
writtenTags,
- invisibleMentions
+ invisibleMentions,
}
// DO NOT MOVE TO UPDATE. BAD IDEA.
this.$emit('parseReady', event)
return result
- }
+ },
}
const getLinkData = (attrs, children, index) => {
@@ -346,7 +372,7 @@ const getLinkData = (attrs, children, index) => {
url: attrs.href,
tag: attrs['data-tag'],
content: flattenDeep(children).join(''),
- textContent
+ textContent,
}
}
@@ -362,31 +388,36 @@ export const preProcessPerLine = (html, greentext) => {
const greentextHandle = new Set(['p', 'div'])
const lines = convertHtmlToLines(html)
- const newHtml = lines.reverse().map((item, index, array) => {
- if (!item.text) return item
- const string = item.text
+ const newHtml = lines
+ .reverse()
+ .map((item, index, array) => {
+ if (!item.text) return item
+ const string = item.text
- // Greentext stuff
- if (
- // Only if greentext is engaged
- greentext &&
+ // Greentext stuff
+ if (
+ // Only if greentext is engaged
+ greentext &&
// Only handle p's and divs. Don't want to affect blockquotes, code etc
- item.level.every(l => greentextHandle.has(l)) &&
+ item.level.every((l) => greentextHandle.has(l)) &&
// Only if line begins with '>' or '<'
(string.includes('>') || string.includes('<'))
- ) {
- const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags
- .replace(/@\w+/gi, '') // remove mentions (even failed ones)
- .trim()
- if (cleanedString.startsWith('>')) {
- return `${string} `
- } else if (cleanedString.startsWith('<')) {
- return `${string} `
+ ) {
+ const cleanedString = string
+ .replace(/<[^>]+?>/gi, '') // remove all tags
+ .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+ .trim()
+ if (cleanedString.startsWith('>')) {
+ return `${string} `
+ } else if (cleanedString.startsWith('<')) {
+ return `${string} `
+ }
}
- }
- return string
- }).reverse().join('')
+ return string
+ })
+ .reverse()
+ .join('')
return { newHtml }
}
diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss
index d2ec77dcb..33effc623 100644
--- a/src/components/rich_content/rich_content.scss
+++ b/src/components/rich_content/rich_content.scss
@@ -77,6 +77,14 @@
height: var(--emoji-size, 32px);
}
+ &.-allow-non-square-emoji {
+ .emoji {
+ width: auto;
+ max-width: calc(var(--emoji-size, 32px) * 3);
+ min-width: var(--emoji-size, 32px);
+ }
+ }
+
.img,
video {
max-width: 100%;
diff --git a/src/components/root.style.js b/src/components/root.style.js
index 5075e33c8..54c4c6095 100644
--- a/src/components/root.style.js
+++ b/src/components/root.style.js
@@ -27,8 +27,8 @@ export default {
// Selection colors
'--selectionBackground': 'color | --accent',
- '--selectionText': 'color | $textColor(--accent --text no-preserve)'
- }
- }
- ]
+ '--selectionText': 'color | $textColor(--accent --text no-preserve)',
+ },
+ },
+ ],
}
diff --git a/src/components/roundness_input/roundness_input.vue b/src/components/roundness_input/roundness_input.vue
index 1da71ca79..caf21763b 100644
--- a/src/components/roundness_input/roundness_input.vue
+++ b/src/components/roundness_input/roundness_input.vue
@@ -33,19 +33,17 @@
diff --git a/src/components/scope_selector/scope_selector.js b/src/components/scope_selector/scope_selector.js
index 52cda368b..9d3a3a718 100644
--- a/src/components/scope_selector/scope_selector.js
+++ b/src/components/scope_selector/scope_selector.js
@@ -1,69 +1,92 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import {
- faEnvelope,
- faLock,
- faLockOpen,
- faGlobe
-} from '@fortawesome/free-solid-svg-icons'
-
-library.add(
faEnvelope,
faGlobe,
faLock,
- faLockOpen
-)
+ faLockOpen,
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(faEnvelope, faGlobe, faLock, faLockOpen)
const ScopeSelector = {
- props: [
- 'showAll',
- 'userDefault',
- 'originalScope',
- 'initialScope',
- 'onScopeChange'
- ],
- data () {
+ props: {
+ showAll: {
+ required: true,
+ type: Boolean,
+ },
+ userDefault: {
+ required: true,
+ type: String,
+ },
+ originalScope: {
+ required: false,
+ type: String,
+ },
+ initialScope: {
+ required: false,
+ type: String,
+ },
+ onScopeChange: {
+ required: true,
+ type: Function,
+ },
+ unstyled: {
+ required: false,
+ type: Boolean,
+ default: true,
+ },
+ },
+ data() {
return {
- currentScope: this.initialScope
+ currentScope: this.initialScope,
}
},
computed: {
- showNothing () {
- return !this.showPublic && !this.showUnlisted && !this.showPrivate && !this.showDirect
+ showNothing() {
+ return (
+ !this.showPublic &&
+ !this.showUnlisted &&
+ !this.showPrivate &&
+ !this.showDirect
+ )
},
- showPublic () {
+ showPublic() {
return this.originalScope !== 'direct' && this.shouldShow('public')
},
- showUnlisted () {
+ showUnlisted() {
return this.originalScope !== 'direct' && this.shouldShow('unlisted')
},
- showPrivate () {
+ showPrivate() {
return this.originalScope !== 'direct' && this.shouldShow('private')
},
- showDirect () {
+ showDirect() {
return this.shouldShow('direct')
},
- css () {
+ css() {
+ const style = this.unstyled ? 'button-unstyled' : 'button-default'
return {
- public: { toggled: this.currentScope === 'public' },
- unlisted: { toggled: this.currentScope === 'unlisted' },
- private: { toggled: this.currentScope === 'private' },
- direct: { toggled: this.currentScope === 'direct' }
+ public: [style, { toggled: this.currentScope === 'public' }],
+ unlisted: [style, { toggled: this.currentScope === 'unlisted' }],
+ private: [style, { toggled: this.currentScope === 'private' }],
+ direct: [style, { toggled: this.currentScope === 'direct' }],
}
- }
+ },
},
methods: {
- shouldShow (scope) {
- return this.showAll ||
+ shouldShow(scope) {
+ return (
+ this.showAll ||
this.currentScope === scope ||
this.originalScope === scope ||
this.userDefault === scope ||
scope === 'direct'
+ )
},
- changeVis (scope) {
+ changeVis(scope) {
this.currentScope = scope
this.onScopeChange && this.onScopeChange(scope)
- }
- }
+ },
+ },
}
export default ScopeSelector
diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue
index b90ae0205..cbe51a07f 100644
--- a/src/components/scope_selector/scope_selector.vue
+++ b/src/components/scope_selector/scope_selector.vue
@@ -1,11 +1,11 @@
.ScopeSelector {
+ display: inline-block;
+
.scope {
display: inline-block;
- cursor: pointer;
min-width: 1.3em;
min-height: 1.3em;
text-align: center;
+ padding: 0.5em 0.25em
}
}
diff --git a/src/components/screen_reader_notice/screen_reader_notice.js b/src/components/screen_reader_notice/screen_reader_notice.js
index 794b855ac..b834be531 100644
--- a/src/components/screen_reader_notice/screen_reader_notice.js
+++ b/src/components/screen_reader_notice/screen_reader_notice.js
@@ -2,20 +2,22 @@ const ScreenReaderNotice = {
props: {
ariaLive: {
type: String,
- default: 'assertive'
- }
+ default: 'assertive',
+ },
},
- data () {
+ data() {
return {
- currentText: ''
+ currentText: '',
}
},
methods: {
- announce (text) {
+ announce(text) {
this.currentText = text
- setTimeout(() => { this.currentText = '' }, 1000)
- }
- }
+ setTimeout(() => {
+ this.currentText = ''
+ }, 1000)
+ },
+ },
}
export default ScreenReaderNotice
diff --git a/src/components/scroll_top_button/scroll_top_button.js b/src/components/scroll_top_button/scroll_top_button.js
index bdc45b9b4..e4b908f0b 100644
--- a/src/components/scroll_top_button/scroll_top_button.js
+++ b/src/components/scroll_top_button/scroll_top_button.js
@@ -3,16 +3,16 @@ const ScrollTopButton = {
fast: {
type: Boolean,
required: false,
- default: false
- }
+ default: false,
+ },
},
methods: {
scrollToTop() {
- const speed = this.fast ? 'instant' : 'smooth';
+ const speed = this.fast ? 'instant' : 'smooth'
window.scrollTo({ top: 0, behavior: speed })
- }
- }
+ },
+ },
}
export default ScrollTopButton
diff --git a/src/components/scrollbar.style.js b/src/components/scrollbar.style.js
index 1168f67d8..fbe952076 100644
--- a/src/components/scrollbar.style.js
+++ b/src/components/scrollbar.style.js
@@ -1,12 +1,16 @@
export default {
name: 'Scrollbar',
- selector: ['::-webkit-scrollbar-button', '::-webkit-scrollbar-thumb', '::-webkit-resizer'],
+ selector: [
+ '::-webkit-scrollbar-button',
+ '::-webkit-scrollbar-thumb',
+ '::-webkit-resizer',
+ ],
notEditable: true, // for now
defaultRules: [
{
directives: {
- background: '--wallpaper'
- }
- }
- ]
+ background: '--wallpaper',
+ },
+ },
+ ],
}
diff --git a/src/components/scrollbar_element.style.js b/src/components/scrollbar_element.style.js
index ef1ea8136..6f238e1f8 100644
--- a/src/components/scrollbar_element.style.js
+++ b/src/components/scrollbar_element.style.js
@@ -5,7 +5,7 @@ const border = (top, shadow) => ({
spread: 0,
color: shadow ? '#000000' : '#FFFFFF',
alpha: 0.2,
- inset: true
+ inset: true,
})
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
@@ -16,7 +16,7 @@ const buttonOuterShadow = {
blur: 2,
spread: 0,
color: '#000000',
- alpha: 1
+ alpha: 1,
}
const hoverGlow = {
@@ -25,7 +25,7 @@ const hoverGlow = {
blur: 4,
spread: 0,
color: '--text',
- alpha: 1
+ alpha: 1,
}
export default {
@@ -35,68 +35,66 @@ export default {
states: {
pressed: ':active',
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(:disabled)',
- disabled: ':disabled'
+ disabled: ':disabled',
},
- validInnerComponents: [
- 'Text'
- ],
+ validInnerComponents: ['Text'],
defaultRules: [
{
directives: {
background: '--fg',
shadow: [buttonOuterShadow, ...buttonInsetFakeBorders],
- roundness: 3
- }
+ roundness: 3,
+ },
},
{
state: ['hover'],
directives: {
- shadow: [hoverGlow, ...buttonInsetFakeBorders]
- }
+ shadow: [hoverGlow, ...buttonInsetFakeBorders],
+ },
},
{
state: ['pressed'],
directives: {
- shadow: [buttonOuterShadow, ...inputInsetFakeBorders]
- }
+ shadow: [buttonOuterShadow, ...inputInsetFakeBorders],
+ },
},
{
state: ['hover', 'pressed'],
directives: {
- shadow: [hoverGlow, ...inputInsetFakeBorders]
- }
+ shadow: [hoverGlow, ...inputInsetFakeBorders],
+ },
},
{
state: ['toggled'],
directives: {
background: '--accent,-24.2',
- shadow: [buttonOuterShadow, ...inputInsetFakeBorders]
- }
+ shadow: [buttonOuterShadow, ...inputInsetFakeBorders],
+ },
},
{
state: ['toggled', 'hover'],
directives: {
background: '--accent,-24.2',
- shadow: [hoverGlow, ...inputInsetFakeBorders]
- }
+ shadow: [hoverGlow, ...inputInsetFakeBorders],
+ },
},
{
state: ['disabled'],
directives: {
background: '$blend(--inheritedBackground 0.25 --parent)',
- shadow: [...buttonInsetFakeBorders]
- }
+ shadow: [...buttonInsetFakeBorders],
+ },
},
{
component: 'Text',
parent: {
component: 'Button',
- state: ['disabled']
+ state: ['disabled'],
},
directives: {
textOpacity: 0.25,
- textOpacityMode: 'blend'
- }
- }
- ]
+ textOpacityMode: 'blend',
+ },
+ },
+ ],
}
diff --git a/src/components/search/search.js b/src/components/search/search.js
index 877d6f300..0a1f779bb 100644
--- a/src/components/search/search.js
+++ b/src/components/search/search.js
@@ -1,31 +1,23 @@
-import FollowCard from '../follow_card/follow_card.vue'
-import Conversation from '../conversation/conversation.vue'
-import Status from '../status/status.vue'
-import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
-import map from 'lodash/map'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faCircleNotch,
- faSearch
-} from '@fortawesome/free-solid-svg-icons'
-import { uniqBy } from 'lodash'
+import { map, uniqBy } from 'lodash'
-library.add(
- faCircleNotch,
- faSearch
-)
+import Conversation from 'src/components/conversation/conversation.vue'
+import FollowCard from 'src/components/follow_card/follow_card.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faCircleNotch, faSearch } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faCircleNotch, faSearch)
const Search = {
components: {
FollowCard,
Conversation,
- Status,
- TabSwitcher
+
+ TabSwitcher,
},
- props: [
- 'query'
- ],
- data () {
+ props: ['query'],
+ data() {
return {
loaded: false,
loading: false,
@@ -37,36 +29,37 @@ const Search = {
statusesOffset: 0,
lastStatusFetchCount: 0,
- lastQuery: ''
+ lastQuery: '',
}
},
computed: {
- users () {
- return this.userIds.map(userId => this.$store.getters.findUser(userId))
+ users() {
+ return this.userIds.map((userId) => this.$store.getters.findUser(userId))
},
- visibleStatuses () {
+ visibleStatuses() {
const allStatusesObject = this.$store.state.statuses.allStatusesObject
- return this.statuses.filter(status =>
- allStatusesObject[status.id] && !allStatusesObject[status.id].deleted
+ return this.statuses.filter(
+ (status) =>
+ allStatusesObject[status.id] && !allStatusesObject[status.id].deleted,
)
- }
+ },
},
- mounted () {
+ mounted() {
this.search(this.query)
},
watch: {
- query (newValue) {
+ query(newValue) {
this.searchTerm = newValue
this.search(newValue)
- }
+ },
},
methods: {
- newQuery (query) {
+ newQuery(query) {
this.$router.push({ name: 'search', query: { query } })
this.$refs.searchInput.focus()
},
- search (query, searchType = null) {
+ search(query, searchType = null) {
if (!query) {
this.loading = false
return
@@ -83,8 +76,14 @@ const Search = {
this.lastStatusFetchCount = 0
}
- this.$store.dispatch('search', { q: query, resolve: true, offset: this.statusesOffset, type: searchType })
- .then(data => {
+ this.$store
+ .dispatch('search', {
+ q: query,
+ resolve: true,
+ offset: this.statusesOffset,
+ type: searchType,
+ })
+ .then((data) => {
this.loading = false
const oldLength = this.statuses.length
@@ -104,14 +103,14 @@ const Search = {
this.lastQuery = query
})
},
- resultCount (tabName) {
+ resultCount(tabName) {
const length = this[tabName].length
return length === 0 ? '' : ` (${length})`
},
- onResultTabSwitch (key) {
+ onResultTabSwitch(key) {
this.currenResultTab = key
},
- getActiveTab () {
+ getActiveTab() {
if (this.visibleStatuses.length > 0) {
return 'statuses'
} else if (this.users.length > 0) {
@@ -122,10 +121,10 @@ const Search = {
return 'statuses'
},
- lastHistoryRecord (hashtag) {
+ lastHistoryRecord(hashtag) {
return hashtag.history && hashtag.history[0]
- }
- }
+ },
+ },
}
export default Search
diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js
index 3b297f098..ebe30d303 100644
--- a/src/components/search_bar/search_bar.js
+++ b/src/components/search_bar/search_bar.js
@@ -1,33 +1,27 @@
import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faTimes,
- faSearch
-} from '@fortawesome/free-solid-svg-icons'
+import { faSearch, faTimes } from '@fortawesome/free-solid-svg-icons'
-library.add(
- faTimes,
- faSearch
-)
+library.add(faTimes, faSearch)
const SearchBar = {
data: () => ({
searchTerm: undefined,
hidden: true,
- error: false
+ error: false,
}),
watch: {
$route: function (route) {
if (route.name === 'search') {
this.searchTerm = route.query.query
}
- }
+ },
},
methods: {
- find (searchTerm) {
+ find(searchTerm) {
this.$router.push({ name: 'search', query: { query: searchTerm } })
this.$refs.searchInput.focus()
},
- toggleHidden () {
+ toggleHidden() {
this.hidden = !this.hidden
this.$emit('toggled', this.hidden)
this.$nextTick(() => {
@@ -35,8 +29,8 @@ const SearchBar = {
this.$refs.searchInput.focus()
}
})
- }
- }
+ },
+ },
}
export default SearchBar
diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue
index d06b4e77d..9a9c971fb 100644
--- a/src/components/search_bar/search_bar.vue
+++ b/src/components/search_bar/search_bar.vue
@@ -51,8 +51,6 @@
class="cancel-icon fa-scale-110 fa-old-padding"
/>
-
-
@@ -61,18 +59,14 @@
-->
+
+
diff --git a/src/components/settings_modal/admin_tabs/instance_tab.js b/src/components/settings_modal/admin_tabs/instance_tab.js
index b07bafe8f..67d04c303 100644
--- a/src/components/settings_modal/admin_tabs/instance_tab.js
+++ b/src/components/settings_modal/admin_tabs/instance_tab.js
@@ -1,25 +1,22 @@
+import { get } from 'lodash'
+
+import AttachmentSetting from '../helpers/attachment_setting.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
-import IntegerSetting from '../helpers/integer_setting.vue'
-import StringSetting from '../helpers/string_setting.vue'
+import ColorSetting from '../helpers/color_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
-import AttachmentSetting from '../helpers/attachment_setting.vue'
-
+import IntegerSetting from '../helpers/integer_setting.vue'
+import ListSetting from '../helpers/list_setting.vue'
+import MapSetting from '../helpers/map_setting.vue'
+import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faGlobe
-} from '@fortawesome/free-solid-svg-icons'
-
-library.add(
- faGlobe
-)
+import StringSetting from '../helpers/string_setting.vue'
const InstanceTab = {
- provide () {
+ provide() {
return {
defaultDraftMode: true,
- defaultSource: 'admin'
+ defaultSource: 'admin',
}
},
components: {
@@ -27,12 +24,45 @@ const InstanceTab = {
ChoiceSetting,
IntegerSetting,
StringSetting,
+ ColorSetting,
AttachmentSetting,
- GroupSetting
+ ListSetting,
+ PWAManifestIconsSetting,
+ MapSetting,
+ GroupSetting,
},
computed: {
- ...SharedComputedObject()
- }
+ ...SharedComputedObject(),
+ providersOptions() {
+ const desc = get(this.$store.state.adminSettings.descriptions, [
+ ':pleroma',
+ 'Pleroma.Web.Metadata',
+ ':providers',
+ ])
+ return new Set(
+ desc.suggestions.map((option) => ({
+ label: option.replace('Pleroma.Web.Metadata.Providers.', ''),
+ value: option,
+ })),
+ )
+ },
+ limitLocalContentOptions() {
+ const desc = get(this.$store.state.adminSettings.descriptions, [
+ ':pleroma',
+ ':instance',
+ ':limit_to_local_content',
+ ])
+ return new Set(
+ desc.suggestions.map((option) => ({
+ label:
+ option !== 'false'
+ ? this.$t('admin_dash.instance.' + option)
+ : this.$t('general.no'),
+ value: option,
+ })),
+ )
+ },
+ },
}
export default InstanceTab
diff --git a/src/components/settings_modal/admin_tabs/instance_tab.vue b/src/components/settings_modal/admin_tabs/instance_tab.vue
index 32e8df259..144958fa4 100644
--- a/src/components/settings_modal/admin_tabs/instance_tab.vue
+++ b/src/components/settings_modal/admin_tabs/instance_tab.vue
@@ -1,17 +1,13 @@
-
-
{{ $t('admin_dash.instance.instance') }}
+
+
{{ $t('admin_dash.instance.instance') }}
-
-
-
+
+
@@ -22,87 +18,75 @@
+
+
+
+
+
+
+
+
{{ $t('admin_dash.instance.branding') }}
+
+
+
+
+
+ {{ $t('admin_dash.instance.pwa.manifest') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('admin_dash.instance.misc_brand') }}
-
-
-
{{ $t('admin_dash.instance.registrations') }}
+
{{ $t('admin_dash.instance.rich_metadata') }}
-
-
+
-
-
-
-
-
-
-
-
-
-
- {{ $t('admin_dash.instance.captcha_header') }}
-
-
-
-
-
-
-
-
-
- {{ $t('admin_dash.instance.kocaptcha') }}
-
-
-
-
-
+
-
-
{{ $t('admin_dash.instance.access') }}
+
+
{{ $t('admin_dash.instance.access') }}
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+
+
+
+
+
-
-
diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js
index 7fbb0a5cd..68d2b923a 100644
--- a/src/components/settings_modal/tabs/appearance_tab.js
+++ b/src/components/settings_modal/tabs/appearance_tab.js
@@ -1,40 +1,30 @@
+import { mapActions } from 'pinia'
+
+import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
+import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
-import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import SharedComputedObject from '../helpers/shared_computed_object.js'
import UnitSetting from '../helpers/unit_setting.vue'
-import { defaultHorizontalUnits } from '../helpers/unit_setting.js'
-import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
-import Preview from './theme_tab/theme_preview.vue'
-import FontControl from 'src/components/font_control/font_control.vue'
+import Preview from './old_theme_tab/theme_preview.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { normalizeThemeData, useInterfaceStore } from 'src/stores/interface.js'
import { newImporter } from 'src/services/export_import/export_import.js'
-import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
-import { init } from 'src/services/theme_data/theme_data_3.service.js'
import {
- getCssRules
-} from 'src/services/theme_data/css_utils.js'
+ adoptStyleSheets,
+ createStyleSheet,
+} from 'src/services/style_setter/style_setter.js'
+import { getCssRules } from 'src/services/theme_data/css_utils.js'
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
-import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
-import fileSizeFormatService from 'src/components/../services/file_size_format/file_size_format.js'
-
-import SharedComputedObject from '../helpers/shared_computed_object.js'
-import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
-
-import { mapActions } from 'pinia'
-import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface'
-
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faGlobe
-} from '@fortawesome/free-solid-svg-icons'
-
-library.add(
- faGlobe
-)
+import { init } from 'src/services/theme_data/theme_data_3.service.js'
+import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
const AppearanceTab = {
- data () {
+ data() {
return {
availableThemesV3: [],
availableThemesV2: [],
@@ -45,7 +35,7 @@ const AppearanceTab = {
validator: this.importValidator,
onImport: this.onImport,
parser: this.importParser,
- onImportFailure: this.onImportFailure
+ onImportFailure: this.onImportFailure,
}),
palettesKeys: [
'bg',
@@ -55,27 +45,29 @@ const AppearanceTab = {
'cRed',
'cGreen',
'cBlue',
- 'cOrange'
+ 'cOrange',
],
userPalette: {},
intersectionObserver: null,
- thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
- key: mode,
- value: mode,
- label: this.$t(`settings.third_column_mode_${mode}`)
- })),
- forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({
- key: mode,
- value: i - 1,
- label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`)
- })),
+ forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map(
+ (mode, i) => ({
+ key: mode,
+ value: i - 1,
+ label: this.$t(
+ `settings.style.themes3.hacks.forced_roundness_mode_${mode}`,
+ ),
+ }),
+ ),
underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode) => ({
key: mode,
value: mode,
- label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`)
+ label: this.$t(
+ `settings.style.themes3.hacks.underlay_override_mode_${mode}`,
+ ),
})),
backgroundUploading: false,
background: null,
+ backgroundError: null,
backgroundPreview: null,
}
},
@@ -85,17 +77,15 @@ const AppearanceTab = {
IntegerSetting,
FloatSetting,
UnitSetting,
- ProfileSettingIndicator,
- FontControl,
Preview,
- PaletteEditor
+ PaletteEditor,
},
- mounted () {
+ mounted() {
useInterfaceStore().getThemeData()
const updateIndex = (resource) => {
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
- const currentIndex = this.$store.state.instance[`${resource}sIndex`]
+ const currentIndex = useInstanceStore()[`${resource}sIndex`]
let promise
if (currentIndex) {
@@ -104,120 +94,151 @@ const AppearanceTab = {
promise = useInterfaceStore()[`fetch${capitalizedResource}sIndex`]()
}
- return promise.then(index => {
- return Object
- .entries(index)
- .map(([k, func]) => [k, func()])
+ return promise.then((index) => {
+ return Object.entries(index).map(([k, func]) => [k, func()])
})
}
- updateIndex('style').then(styles => {
- styles.forEach(([key, stylePromise]) => stylePromise.then(data => {
- const meta = data.find(x => x.component === '@meta')
- this.availableThemesV3.push({ key, data, name: meta.directives.name, version: 'v3' })
- }))
+ updateIndex('style').then((styles) => {
+ styles.forEach(([key, stylePromise]) =>
+ stylePromise.then((data) => {
+ const meta = data.find((x) => x.component === '@meta')
+ this.availableThemesV3.push({
+ key,
+ data,
+ name: meta.directives.name,
+ version: 'v3',
+ })
+ }),
+ )
})
- updateIndex('theme').then(themes => {
- themes.forEach(([key, themePromise]) => themePromise.then(data => {
- if (!data) {
- console.warn(`Theme with key ${key} is empty or malformed`)
- } else if (Array.isArray(data)) {
- console.warn(`Theme with key ${key} is a v1 theme and should be moved to static/palettes/index.json`)
- } else if (!data.source && !data.theme) {
- console.warn(`Theme with key ${key} is malformed`)
- } else {
- this.availableThemesV2.push({ key, data, name: data.name, version: 'v2' })
- }
- }))
+ updateIndex('theme').then((themes) => {
+ themes.forEach(([key, themePromise]) =>
+ themePromise.then((data) => {
+ if (!data) {
+ console.warn(`Theme with key ${key} is empty or malformed`)
+ } else if (Array.isArray(data)) {
+ console.warn(
+ `Theme with key ${key} is a v1 theme and should be moved to static/palettes/index.json`,
+ )
+ } else if (!data.source && !data.theme) {
+ console.warn(`Theme with key ${key} is malformed`)
+ } else {
+ this.availableThemesV2.push({
+ key,
+ data,
+ name: data.name,
+ version: 'v2',
+ })
+ }
+ }),
+ )
})
this.userPalette = useInterfaceStore().paletteDataUsed || {}
- updateIndex('palette').then(bundledPalettes => {
- bundledPalettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
- let palette
- if (Array.isArray(v)) {
- const [
- name,
- bg,
- fg,
- text,
- link,
- cRed = '#FF0000',
- cGreen = '#00FF00',
- cBlue = '#0000FF',
- cOrange = '#E3FF00'
- ] = v
- palette = { key, name, bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
- } else {
- palette = { key, ...v }
- }
- if (!palette.key.startsWith('style.')) {
- this.bundledPalettes.push(palette)
- }
- }))
+ updateIndex('palette').then((bundledPalettes) => {
+ bundledPalettes.forEach(([key, palettePromise]) =>
+ palettePromise.then((v) => {
+ let palette
+ if (Array.isArray(v)) {
+ const [
+ name,
+ bg,
+ fg,
+ text,
+ link,
+ cRed = '#FF0000',
+ cGreen = '#00FF00',
+ cBlue = '#0000FF',
+ cOrange = '#E3FF00',
+ ] = v
+ palette = {
+ key,
+ name,
+ bg,
+ fg,
+ text,
+ link,
+ cRed,
+ cBlue,
+ cGreen,
+ cOrange,
+ }
+ } else {
+ palette = { key, ...v }
+ }
+ if (!palette.key.startsWith('style.')) {
+ this.bundledPalettes.push(palette)
+ }
+ }),
+ )
})
this.previewTheme('stock', 'v3')
if (window.IntersectionObserver) {
- this.intersectionObserver = new IntersectionObserver((entries, observer) => {
- entries.forEach(({ target, isIntersecting }) => {
- if (!isIntersecting) return
- const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey)
- this.$nextTick(() => {
- if (theme) this.previewTheme(theme.key, theme.version, theme.data)
+ this.intersectionObserver = new IntersectionObserver(
+ (entries, observer) => {
+ entries.forEach(({ target, isIntersecting }) => {
+ if (!isIntersecting) return
+ const theme = this.availableStyles.find(
+ (x) => x.key === target.dataset.themeKey,
+ )
+ this.$nextTick(() => {
+ if (theme) this.previewTheme(theme.key, theme.version, theme.data)
+ })
+ observer.unobserve(target)
})
- observer.unobserve(target)
- })
- }, {
- root: this.$refs.themeList
- })
+ },
+ {
+ root: this.$refs.themeList,
+ },
+ )
} else {
- this.availableStyles.forEach(theme => this.previewTheme(theme.key, theme.version, theme.data))
+ this.availableStyles.forEach((theme) =>
+ this.previewTheme(theme.key, theme.version, theme.data),
+ )
}
},
- updated () {
+ updated() {
this.$nextTick(() => {
- this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => {
- this.intersectionObserver.observe(node)
- })
+ this.$refs.themeList
+ .querySelectorAll('.theme-preview')
+ .forEach((node) => {
+ this.intersectionObserver.observe(node)
+ })
})
},
watch: {
- paletteDataUsed () {
+ paletteDataUsed() {
this.userPalette = this.paletteDataUsed || {}
- }
+ },
},
computed: {
- isDefaultBackground () {
- return !(this.$store.state.users.currentUser.background_image)
+ isDefaultBackground() {
+ return !this.$store.state.users.currentUser.background_image
},
- switchInProgress () {
+ switchInProgress() {
return useInterfaceStore().themeChangeInProgress
},
- paletteDataUsed () {
+ paletteDataUsed() {
return useInterfaceStore().paletteDataUsed
},
- availableStyles () {
- return [
- ...this.availableThemesV3,
- ...this.availableThemesV2
- ]
+ availableStyles() {
+ return [...this.availableThemesV3, ...this.availableThemesV2]
},
- availablePalettes () {
- return [
- ...this.bundledPalettes,
- ...this.stylePalettes
- ]
+ availablePalettes() {
+ return [...this.bundledPalettes, ...this.stylePalettes]
},
- stylePalettes () {
+ stylePalettes() {
const ruleset = useInterfaceStore().styleDataUsed || []
if (!ruleset && ruleset.length === 0) return
- const meta = ruleset.find(x => x.component === '@meta')
- const result = ruleset.filter(x => x.component.startsWith('@palette'))
- .map(x => {
+ const meta = ruleset.find((x) => x.component === '@meta')
+ const result = ruleset
+ .filter((x) => x.component.startsWith('@palette'))
+ .map((x) => {
const { variant, directives } = x
const {
bg,
@@ -229,7 +250,7 @@ const AppearanceTab = {
cBlue,
cGreen,
cOrange,
- wallpaper
+ wallpaper,
} = directives
const result = {
@@ -244,126 +265,106 @@ const AppearanceTab = {
cBlue,
cGreen,
cOrange,
- wallpaper
+ wallpaper,
}
return Object.fromEntries(Object.entries(result).filter(([, v]) => v))
})
return result
},
- noIntersectionObserver () {
+ noIntersectionObserver() {
return !window.IntersectionObserver
},
- horizontalUnits () {
- return defaultHorizontalUnits
+ instanceWallpaper() {
+ useInstanceStore().instanceIdentity.background
},
- fontsOverride () {
- return this.$store.getters.mergedConfig.fontsOverride
- },
- columns () {
- const mode = this.$store.getters.mergedConfig.thirdColumnMode
-
- const notif = mode === 'none' ? [] : ['notifs']
-
- if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
- return [...notif, 'content', 'sidebar']
- } else {
- return ['sidebar', 'content', ...notif]
- }
- },
- instanceWallpaperUsed () {
- return this.$store.state.instance.background &&
+ instanceWallpaperUsed() {
+ return (
+ useInstanceStore().instanceIdentity.background &&
!this.$store.state.users.currentUser.background_image
+ )
},
- language: {
- get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
- set: function (val) {
- this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
- }
- },
- customThemeVersion () {
+ customThemeVersion() {
const { themeVersion } = useInterfaceStore()
return themeVersion
},
- isCustomThemeUsed () {
+ isCustomThemeUsed() {
const { customTheme, customThemeSource } = this.mergedConfig
return customTheme != null || customThemeSource != null
},
- isCustomStyleUsed () {
+ isCustomStyleUsed() {
const { styleCustomData } = this.mergedConfig
return styleCustomData != null
},
- ...SharedComputedObject()
+ ...SharedComputedObject(),
},
methods: {
- updateFont (key, value) {
- this.$store.dispatch('setOption', {
- name: 'theme3hacks',
- value: {
- ...this.mergedConfig.theme3hacks,
- fonts: {
- ...this.mergedConfig.theme3hacks.fonts,
- [key]: value
- }
- }
- })
- },
- importFile () {
+ importFile() {
this.fileImporter.importData()
},
- importParser (file, filename) {
+ importParser(file, filename) {
if (filename.endsWith('.json')) {
return JSON.parse(file)
} else if (filename.endsWith('.iss')) {
return deserialize(file)
}
},
- onImport (parsed, filename) {
+ onImport(parsed, filename) {
if (filename.endsWith('.json')) {
useInterfaceStore().setThemeCustom(parsed.source || parsed.theme)
} else if (filename.endsWith('.iss')) {
useInterfaceStore().setStyleCustom(parsed)
}
},
- onImportFailure (result) {
+ onImportFailure(result) {
console.error('Failure importing theme:', result)
- useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
+ useInterfaceStore().pushGlobalNotice({
+ messageKey: 'settings.invalid_theme_imported',
+ level: 'error',
+ })
},
- importValidator (parsed, filename) {
+ importValidator(parsed, filename) {
if (filename.endsWith('.json')) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
} else if (filename.endsWith('.iss')) {
if (!Array.isArray(parsed)) return false
if (parsed.length < 1) return false
- if (parsed.find(x => x.component === '@meta') == null) return false
+ if (parsed.find((x) => x.component === '@meta') == null) return false
return true
}
},
- isThemeActive (key) {
- return key === (this.mergedConfig.theme || this.$store.state.instance.theme)
+ isThemeActive(key) {
+ return (
+ key ===
+ (this.mergedConfig.theme || useInstanceStore().instanceIdentity.theme)
+ )
},
- isStyleActive (key) {
- return key === (this.mergedConfig.style || this.$store.state.instance.style)
+ isStyleActive(key) {
+ return (
+ key ===
+ (this.mergedConfig.style || useInstanceStore().instanceIdentity.style)
+ )
},
- isPaletteActive (key) {
- return key === (this.mergedConfig.palette || this.$store.state.instance.palette)
+ isPaletteActive(key) {
+ return (
+ key ===
+ (this.mergedConfig.palette ||
+ useInstanceStore().instanceIdentity.palette)
+ )
},
- ...mapActions(useInterfaceStore, [
- 'setStyle',
- 'setTheme'
- ]),
- setPalette (name, data) {
+ ...mapActions(useInterfaceStore, ['setStyle', 'setTheme']),
+ setPalette(name, data) {
useInterfaceStore().setPalette(name)
this.userPalette = data
},
- setPaletteCustom (data) {
+ setPaletteCustom(data) {
useInterfaceStore().setPaletteCustom(data)
this.userPalette = data
},
- resetTheming () {
+ resetTheming() {
useInterfaceStore().setStyle('stock')
},
- previewTheme (key, version, input) {
+ previewTheme(key, version, input) {
let theme3
if (this.compilationCache[key]) {
theme3 = this.compilationCache[key]
@@ -376,10 +377,10 @@ const AppearanceTab = {
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true,
- onlyNormalState: true
+ onlyNormalState: true,
})
} else if (version === 'v3') {
- const palette = input.find(x => x.component === '@palette')
+ const palette = input.find((x) => x.component === '@palette')
let paletteRule
if (palette) {
const { directives } = palette
@@ -388,21 +389,20 @@ const AppearanceTab = {
paletteRule = {
component: 'Root',
directives: Object.fromEntries(
- Object
- .entries(directives)
+ Object.entries(directives)
.filter(([k]) => k && k !== 'name')
- .map(([k, v]) => ['--' + k, 'color | ' + v])
- )
+ .map(([k, v]) => ['--' + k, 'color | ' + v]),
+ ),
}
} else {
paletteRule = null
}
theme3 = init({
- inputRuleset: [...input, paletteRule].filter(x => x),
+ inputRuleset: [...input, paletteRule].filter((x) => x),
ultimateBackgroundColor: '#000000',
liteMode: true,
- onlyNormalState: true
+ onlyNormalState: true,
})
}
} else {
@@ -410,7 +410,7 @@ const AppearanceTab = {
inputRuleset: [],
ultimateBackgroundColor: '#000000',
liteMode: true,
- onlyNormalState: true
+ onlyNormalState: true,
})
}
@@ -418,22 +418,29 @@ const AppearanceTab = {
this.compilationCache[key] = theme3
}
-
const sheet = createStyleSheet('appearance-tab-previews', 90)
- sheet.addRule([
- '#theme-preview-', key, ' {\n',
- getCssRules(theme3.eager).join('\n'),
- '\n}'
- ].join(''))
+ sheet.addRule(
+ [
+ '#theme-preview-',
+ key,
+ ' {\n',
+ getCssRules(theme3.eager).join('\n'),
+ '\n}',
+ ].join(''),
+ )
sheet.ready = true
adoptStyleSheets()
},
- uploadFile (slot, e) {
+ uploadFile(slot, e) {
const file = e.target.files[0]
- if (!file) { return }
- if (file.size > this.$store.state.instance[slot + 'limit']) {
+ if (!file) {
+ return
+ }
+ if (file.size > useInstanceStore()[slot + 'limit']) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
- const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
+ const allowedsize = fileSizeFormatService.fileSizeFormat(
+ useInstanceStore()[slot + 'limit'],
+ )
useInterfaceStore().pushGlobalNotice({
messageKey: 'upload.error.message',
messageArgs: [
@@ -441,10 +448,10 @@ const AppearanceTab = {
filesize: filesize.num,
filesizeunit: filesize.unit,
allowedsize: allowedsize.num,
- allowedsizeunit: allowedsize.unit
- })
+ allowedsizeunit: allowedsize.unit,
+ }),
],
- level: 'error'
+ level: 'error',
})
return
}
@@ -457,26 +464,42 @@ const AppearanceTab = {
}
reader.readAsDataURL(file)
},
- resetBackground () {
- const confirmed = window.confirm(this.$t('settings.reset_background_confirm'))
+ resetBackground() {
+ const confirmed = window.confirm(
+ this.$t('settings.reset_background_confirm'),
+ )
if (confirmed) {
this.submitBackground('')
}
},
- submitBackground (background) {
- if (!this.backgroundPreview && background !== '') { return }
+ resetUploadedBackground() {
+ this.backgroundPreview = null
+ },
+ clearBackgroundError() {
+ this.backgroundError = null
+ },
+ submitBackground(background) {
+ if (!this.backgroundPreview && background !== '') {
+ return
+ }
this.backgroundUploading = true
- this.$store.state.api.backendInteractor.updateProfileImages({ background })
+ this.$store.state.api.backendInteractor
+ .updateProfileImages({ background })
.then((data) => {
this.$store.commit('addNewUsers', [data])
this.$store.commit('setCurrentUser', data)
this.backgroundPreview = null
+ this.backgroundError = null
+ })
+ .catch((e) => {
+ this.backgroundError = e
+ })
+ .finally(() => {
+ this.backgroundUploading = false
})
- .catch(this.displayUploadError)
- .finally(() => { this.backgroundUploading = false })
},
- }
+ },
}
export default AppearanceTab
diff --git a/src/components/settings_modal/tabs/appearance_tab.scss b/src/components/settings_modal/tabs/appearance_tab.scss
index d786cfa38..eadfa090d 100644
--- a/src/components/settings_modal/tabs/appearance_tab.scss
+++ b/src/components/settings_modal/tabs/appearance_tab.scss
@@ -1,27 +1,18 @@
.appearance-tab {
+ margin: 1em;
+
+ h3 {
+ border: none
+ }
+
.palette,
.theme-notice {
padding: 0.5em;
- margin: 1em;
}
- .setting-item {
- padding-bottom: 0;
-
- &.heading {
- display: grid;
- align-items: baseline;
- grid-template-columns: 1fr auto auto auto;
- grid-gap: 0.5em;
-
- h2 {
- flex: 1 0 auto;
- }
- }
- }
-
- h4 {
- margin: 0.5em 0;
+ .theme-name {
+ font-weight: 900;
+ padding-bottom: 0.5em;
}
input[type="file"] {
@@ -29,7 +20,31 @@
height: auto;
}
+ .banner-background {
+ display: flex;
+ gap: 1em;
+ flex-wrap: wrap;
+
+ h4 {
+ margin: 0;
+ }
+ }
+
+ .banner-background-input {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5em;
+
+ .custom-bg-control {
+ display: grid;
+ gap: 0.5em;
+ grid-template-columns: 1fr 1fr;
+ }
+ }
+
.banner-background-preview {
+ display: flex;
max-width: 100%;
width: 300px;
position: relative;
@@ -37,40 +52,95 @@
img {
width: 100%;
}
- }
- .reset-button {
- position: absolute;
- top: 0.2em;
- right: 0.2em;
- border-radius: var(--roundness);
- background-color: rgb(0 0 0 / 60%);
- opacity: 0.7;
- width: 1.5em;
- height: 1.5em;
- text-align: center;
- line-height: 1.5em;
- font-size: 1.5em;
- cursor: pointer;
+ .fun-monitor {
+ position: relative;
+ pointer-events: none;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
- &:hover {
- opacity: 1;
- }
+ * {
+ line-height: 1;
+ }
- svg {
- color: white;
+ &-display-bezel,
+ &-display-screen {
+ aspect-ratio: 16 / 9;
+ width: 16em;
+ }
+
+ img {
+ object-fit: cover;
+ }
+
+ .wallpaper {
+ position: absolute;
+ inset: 0;
+ background-color: var(--wallpaper);
+ }
+
+ &-display-uploading {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ display: flex;
+ place-items: center;
+ place-content: center;
+ background-color: rgb(0 0 0 / 60%);
+ font-size: 4em;
+ }
+
+ &-display-screen {
+ padding: 0;
+ overflow: hidden;
+ position: relative;
+
+ &-overlay {
+ background: transparent;
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ }
+
+ &-image {
+ aspect-ratio: 16 / 9
+ }
+ }
+
+ &-display-bezel {
+ padding: 1em;
+ margin: 0;
+ order: 1;
+ z-index: 3;
+ }
+
+ &-neck {
+ width: 5em;
+ height: 3em;
+ margin-top: -1em;
+ margin-bottom: -0.5em;
+ order: 2
+ }
+
+ &-stand {
+ width: 8em;
+ height: 1em;
+ order: 3;
+ z-index: 1
+ }
}
}
-
-
.palettes-container {
height: 15em;
overflow: hidden auto;
scrollbar-gutter: stable;
border-radius: var(--roundness);
border: 1px solid var(--border);
- margin: -0.5em;
+ margin-bottom: 0.5em;
+ margin-top: 0;
+ padding: 0.5em;
}
.palettes {
@@ -80,9 +150,9 @@
padding: 0.5em;
width: 100%;
- h4 {
- margin: 0;
+ h5 {
grid-column: 1 / span 2;
+ margin-bottom: 0;
}
}
@@ -160,7 +230,7 @@
.theme-preview {
font-size: 1rem; // fix for firefox
- width: 19rem;
+ width: 14rem;
display: flex;
flex-direction: column;
align-items: center;
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index cd54e2c82..1343a36d5 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -1,10 +1,15 @@
-
-
{{ $t('settings.theme') }}
+
+
{{ $t('settings.style.style_section') }}
-
+
{{ $t('settings.style.stock_theme_used') }}
v3
-
+
-
+
{{ $t('settings.style.custom_theme_used') }}
v2
-
+
-
+
{{ $t('settings.style.custom_style_used') }}
v3
-
+
-
+
{{ style.name }}
{{ style.version }}
-
+
@@ -70,16 +75,14 @@
{{ $t('settings.style.themes3.editor.load_style') }}
-
-
-
{{ $t('settings.style.themes3.palette.label') }}
+
{{ $t('settings.style.themes3.palette.label') }}
-
+
{{ $t('settings.style.themes3.palette.style') }}
-
+
-
{{ $t('settings.style.themes3.palette.bundled') }}
+
{{ $t('settings.style.themes3.palette.bundled') }}
-
+
{{ $t('settings.style.themes3.palette.user') }}
-
+
-
-
-
{{ $t('settings.background') }}
-
-
-
-
-
-
-
{{ $t('settings.set_new_background') }}
-
-
-
-
-
-
- {{ $t('settings.save') }}
-
-
-
-
{{ $t('settings.scale_and_layout') }}
-
- {{ $t("settings.style.appearance_tab_note") }}
-
-
-
-
- {{ $t('settings.text_size') }}
-
-
-
-
- px
- rem
-
-
-
- 14px
-
-
+
{{ $t('settings.background') }}
+
+
+
-
-
-
+
-
-
{{ $t('settings.visual_tweaks') }}
+
+ {{ backgroundError }}
+
+
+
+
+
+
+ {{ $t('settings.reset_profile_background') }}
+
+
+
+
{{ $t('settings.visual_tweaks') }}
+
+ {{ $t("settings.style.visual_tweaks_section_note") }}
+
-
-
- {{ $t('settings.mobile_center_dialog') }}
-
-
{{ $t('settings.style.themes3.hacks.underlay_overrides') }}
@@ -404,19 +264,13 @@
-
- {{ $t('settings.force_theme_recompilation_debug') }}
+
+ {{ $t('settings.foreign_user_background') }}
-
- {{ $t('settings.theme_debug') }}
+
+ {{ $t('settings.compact_profiles') }}
diff --git a/src/components/settings_modal/tabs/clutter_tab.js b/src/components/settings_modal/tabs/clutter_tab.js
new file mode 100644
index 000000000..85749ab3f
--- /dev/null
+++ b/src/components/settings_modal/tabs/clutter_tab.js
@@ -0,0 +1,159 @@
+import { mapActions, mapState } from 'pinia'
+import { v4 as uuidv4 } from 'uuid'
+
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import Select from 'src/components/select/select.vue'
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import HelpIndicator from '../helpers/help_indicator.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import UnitSetting from '../helpers/unit_setting.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useSyncConfigStore } from 'src/stores/sync_config.js'
+
+const ClutterTab = {
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ UnitSetting,
+ IntegerSetting,
+ Checkbox,
+ Select,
+ HelpIndicator,
+ },
+ computed: {
+ ...SharedComputedObject(),
+ ...mapState(useInstanceCapabilitiesStore, ['shoutAvailable']),
+ ...mapState(useInstanceStore, {
+ showFeaturesPanel: (store) => store.instanceIdentity.showFeaturesPanel,
+ instanceSpecificPanelPresent: (store) =>
+ store.instanceIdentity.showInstanceSpecificPanel &&
+ store.instanceIdentity.instanceSpecificPanelContent,
+ }),
+ ...mapState(useSyncConfigStore, {
+ muteFilters: (store) =>
+ Object.entries(store.prefsStorage.simple.muteFilters),
+ muteFiltersObject: (store) => store.prefsStorage.simple.muteFilters,
+ }),
+ },
+ methods: {
+ ...mapActions(useSyncConfigStore, [
+ 'setSimplePrefAndSave',
+ 'unsetSimplePrefAndSave',
+ 'pushSyncConfig',
+ ]),
+ getDatetimeLocal(timestamp) {
+ const date = new Date(timestamp)
+ const fmt = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 2 })
+ const datetime = [
+ date.getFullYear(),
+ '-',
+ fmt.format(date.getMonth() + 1),
+ '-',
+ fmt.format(date.getDate()),
+ 'T',
+ fmt.format(date.getHours()),
+ ':',
+ fmt.format(date.getMinutes()),
+ ].join('')
+ return datetime
+ },
+ checkRegexValid(id) {
+ const filter = this.muteFiltersObject[id]
+ if (filter.type !== 'regexp') return true
+ if (filter.type !== 'user_regexp') return true
+ const { value } = filter
+ let valid = true
+ try {
+ new RegExp(value)
+ } catch {
+ valid = false
+ console.error('Invalid RegExp: ' + value)
+ }
+ return valid
+ },
+ createFilter(
+ filter = {
+ type: 'word',
+ value: '',
+ name: 'New Filter',
+ enabled: true,
+ expires: null,
+ hide: false,
+ },
+ ) {
+ const newId = uuidv4()
+
+ filter.order = this.muteFilters.length + 2
+ this.muteFiltersDraftObject[newId] = filter
+ this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
+ this.pushSyncConfig()
+ },
+ exportFilter(id) {
+ this.exportedFilter = { ...this.muteFiltersDraftObject[id] }
+ delete this.exportedFilter.order
+ this.filterExporter.exportData()
+ },
+ importFilter() {
+ this.filterImporter.importData()
+ },
+ copyFilter(id) {
+ const filter = { ...this.muteFiltersDraftObject[id] }
+ const newId = uuidv4()
+
+ this.muteFiltersDraftObject[newId] = filter
+ this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
+ this.pushSyncConfig()
+ },
+ deleteFilter(id) {
+ delete this.muteFiltersDraftObject[id]
+ this.unsetSimplePrefAndSave({ path: 'muteFilters.' + id, value: null })
+ this.pushSyncConfig()
+ },
+ purgeExpiredFilters() {
+ this.muteFiltersExpired.forEach(([id]) => {
+ delete this.muteFiltersDraftObject[id]
+ this.unsetSimplePrefAndSave({ path: 'muteFilters.' + id, value: null })
+ })
+ this.pushSyncConfig()
+ },
+ updateFilter(id, field, value) {
+ const filter = { ...this.muteFiltersDraftObject[id] }
+ if (field === 'expires-never') {
+ if (!value) {
+ const offset = 1000 * 60 * 60 * 24 * 14 // 2 weeks
+ const date = Date.now() + offset
+ filter.expires = date
+ } else {
+ filter.expires = null
+ }
+ } else if (field === 'expires') {
+ const parsed = Date.parse(value)
+ filter.expires = parsed.valueOf()
+ } else {
+ filter[field] = value
+ }
+ this.muteFiltersDraftObject[id] = filter
+ this.muteFiltersDraftDirty[id] = true
+ },
+ saveFilter(id) {
+ this.setSimplePrefAndSave({
+ path: 'muteFilters.' + id,
+ value: this.muteFiltersDraftObject[id],
+ })
+ this.pushSyncConfig()
+ this.muteFiltersDraftDirty[id] = false
+ },
+ },
+ // Updating nested properties
+ watch: {
+ replyVisibility() {
+ this.$store.dispatch('queueFlushAll')
+ },
+ },
+}
+
+export default ClutterTab
diff --git a/src/components/settings_modal/tabs/clutter_tab.vue b/src/components/settings_modal/tabs/clutter_tab.vue
new file mode 100644
index 000000000..c0e6008f8
--- /dev/null
+++ b/src/components/settings_modal/tabs/clutter_tab.vue
@@ -0,0 +1,95 @@
+
+
+
+
{{ $t('settings.interface') }}
+
+
+
+ {{ $t('settings.subject_input_always_show') }}
+
+
+
+
+ {{ $t('settings.minimal_scopes_mode') }}
+
+
+
+
+ {{ $t('settings.hide_post_stats') }}
+
+
+
+
+ {{ $t('settings.hide_user_stats') }}
+
+
+
+
+ {{ $t('settings.hide_actor_type_indication') }}
+
+
+
+
+ {{ $t('settings.hide_scrobbles') }}
+
+
+
+
+ {{ $t('settings.hide_scrobbles_after') }}
+
+
+
+
+ {{ $t('settings.hide_isp') }}
+
+
+
+
+
+
+ {{ $t('settings.user_card_hide_personal_marks') }}
+
+
+
+
+ {{ $t('settings.hide_shoutbox') }}
+
+
+
+
{{ $t('settings.attachments') }}
+
+
+
+ {{ $t('settings.max_thumbnails') }}
+
+
+
+
+ {{ $t('settings.hide_attachments_in_tl') }}
+
+
+
+
+ {{ $t('settings.hide_attachments_in_convo') }}
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/composing_tab.js b/src/components/settings_modal/tabs/composing_tab.js
new file mode 100644
index 000000000..ab8d0101d
--- /dev/null
+++ b/src/components/settings_modal/tabs/composing_tab.js
@@ -0,0 +1,189 @@
+import { mapState } from 'pinia'
+
+import FontControl from 'src/components/font_control/font_control.vue'
+import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
+import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
+import Select from 'src/components/select/select.vue'
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import FloatSetting from '../helpers/float_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import UnitSetting from '../helpers/unit_setting.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+import { useSyncConfigStore } from 'src/stores/sync_config.js'
+
+import localeService from 'src/services/locale/locale.service.js'
+import { cacheKey, clearCache, emojiCacheKey } from 'src/services/sw/sw.js'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faDatabase,
+ faGlobe,
+ faMessage,
+ faPenAlt,
+ faSliders,
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(faGlobe, faMessage, faPenAlt, faDatabase, faSliders)
+
+const ComposingTab = {
+ data() {
+ return {
+ subjectLineOptions: ['email', 'noop', 'masto'].map((mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(
+ `settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`,
+ ),
+ })),
+ conversationDisplayOptions: ['tree', 'linear'].map((mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.conversation_display_${mode}`),
+ })),
+ absoluteTime12hOptions: ['24h', '12h'].map((mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.absolute_time_format_12h_${mode}`),
+ })),
+ conversationOtherRepliesButtonOptions: ['below', 'inside'].map(
+ (mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.conversation_other_replies_button_${mode}`),
+ }),
+ ),
+ mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(
+ (mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.mention_link_display_${mode}`),
+ }),
+ ),
+ userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map((mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.user_popover_avatar_action_${mode}`),
+ })),
+ unsavedPostActionOptions: ['save', 'discard', 'confirm'].map((mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.unsaved_post_action_${mode}`),
+ })),
+ loopSilentAvailable:
+ // Firefox
+ Object.getOwnPropertyDescriptor(
+ HTMLVideoElement.prototype,
+ 'mozHasAudio',
+ ) ||
+ // Chrome-likes
+ Object.getOwnPropertyDescriptor(
+ HTMLMediaElement.prototype,
+ 'webkitAudioDecodedByteCount',
+ ) ||
+ // Future spec, still not supported in Nightly 63 as of 08/2018
+ Object.getOwnPropertyDescriptor(
+ HTMLMediaElement.prototype,
+ 'audioTracks',
+ ),
+ emailLanguage: this.$store.state.users.currentUser.language || [''],
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ FloatSetting,
+ UnitSetting,
+ InterfaceLanguageSwitcher,
+ ScopeSelector,
+ Select,
+ FontControl,
+ },
+ computed: {
+ postFormats() {
+ return useInstanceCapabilitiesStore().postFormats
+ },
+ postContentOptions() {
+ return this.postFormats.map((format) => ({
+ key: format,
+ value: format,
+ label: this.$t(`post_status.content_type["${format}"]`),
+ }))
+ },
+ language: {
+ get: function () {
+ return useMergedConfigStore().mergedConfig.interfaceLanguage
+ },
+ set: function (val) {
+ useSyncConfigStore().setSimplePrefAndSave({
+ path: 'interfaceLanguage',
+ value: val,
+ })
+ },
+ },
+ ...SharedComputedObject(),
+ ...mapState(useInstanceStore, ['blockExpiration']),
+ },
+ methods: {
+ changeDefaultScope(value) {
+ this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
+ },
+ clearCache(key) {
+ clearCache(key)
+ .then(() => {
+ this.$store.dispatch('settingsSaved', { success: true })
+ })
+ .catch((error) => {
+ this.$store.dispatch('settingsSaved', { error })
+ })
+ },
+ tooSmall() {
+ this.$emit('tooSmall')
+ },
+ tooBig() {
+ this.$emit('tooBig')
+ },
+ getNavMode() {
+ return this.$refs.tabSwitcher.getNavMode()
+ },
+ clearAssetCache() {
+ this.clearCache(cacheKey)
+ },
+ clearEmojiCache() {
+ this.clearCache(emojiCacheKey)
+ },
+ updateProfile() {
+ const params = {
+ language: localeService.internalToBackendLocaleMulti(
+ this.emailLanguage,
+ ),
+ }
+
+ this.$store.state.api.backendInteractor
+ .updateProfile({ params })
+ .then((user) => {
+ this.$store.commit('addNewUsers', [user])
+ this.$store.commit('setCurrentUser', user)
+ })
+ },
+ updateFont(key, value) {
+ useSyncConfigStore().setSimplePrefAndSave({
+ path: 'theme3hacks',
+ value: {
+ ...this.mergedConfig.theme3hacks,
+ fonts: {
+ ...this.mergedConfig.theme3hacks.fonts,
+ [key]: value,
+ },
+ },
+ })
+ },
+ },
+}
+
+export default ComposingTab
diff --git a/src/components/settings_modal/tabs/composing_tab.vue b/src/components/settings_modal/tabs/composing_tab.vue
new file mode 100644
index 000000000..755539096
--- /dev/null
+++ b/src/components/settings_modal/tabs/composing_tab.vue
@@ -0,0 +1,117 @@
+
+
+
+
{{ $t('settings.general') }}
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.sensitive_by_default') }}
+
+
+
+
+ {{ $t('settings.default_post_status_content_type') }}
+
+
+
+
+ {{ $t('settings.pad_emoji') }}
+
+
+
+
+ {{ $t('settings.autocomplete_select_first') }}
+
+
+
+
+ {{ $t('settings.auto_save_draft') }}
+
+
+
+
+ {{ $t('settings.unsaved_post_action') }}
+
+
+
+
{{ $t('settings.replies') }}
+
+
+
+ {{ $t('settings.scope_copy') }}
+
+
+
+
+ {{ $t('settings.subject_line_behavior') }}
+
+
+
+
+ {{ $t('settings.attachments') }}
+
+
+
+
+ {{ $t('settings.image_compression') }}
+
+
+
+
+ {{ $t('settings.always_use_jpeg') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js
index 4304e59c0..cc41302c7 100644
--- a/src/components/settings_modal/tabs/data_import_export_tab.js
+++ b/src/components/settings_modal/tabs/data_import_export_tab.js
@@ -1,85 +1,90 @@
-import Importer from 'src/components/importer/importer.vue'
-import Exporter from 'src/components/exporter/exporter.vue'
-import Checkbox from 'src/components/checkbox/checkbox.vue'
import { mapState } from 'vuex'
-import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
+
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import Exporter from 'src/components/exporter/exporter.vue'
+import Importer from 'src/components/importer/importer.vue'
+
+import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js'
const DataImportExportTab = {
- data () {
+ data() {
return {
activeTab: 'profile',
newDomainToMute: '',
listBackupsError: false,
addBackupError: false,
addedBackup: false,
- backups: []
+ backups: [],
}
},
- created () {
+ created() {
useOAuthTokensStore().fetchTokens()
this.fetchBackups()
},
components: {
Importer,
Exporter,
- Checkbox
+ Checkbox,
},
computed: {
...mapState({
backendInteractor: (state) => state.api.backendInteractor,
- user: (state) => state.users.currentUser
- })
+ user: (state) => state.users.currentUser,
+ }),
},
methods: {
- getFollowsContent () {
- return this.backendInteractor.exportFriends({ id: this.user.id })
+ getFollowsContent() {
+ return this.backendInteractor
+ .exportFriends({ id: this.user.id })
.then(this.generateExportableUsersContent)
},
- getBlocksContent () {
- return this.backendInteractor.fetchBlocks()
+ getBlocksContent() {
+ return this.backendInteractor
+ .fetchBlocks()
.then(this.generateExportableUsersContent)
},
- getMutesContent () {
- return this.backendInteractor.fetchMutes()
+ getMutesContent() {
+ return this.backendInteractor
+ .fetchMutes()
.then(this.generateExportableUsersContent)
},
- importFollows (file) {
- return this.backendInteractor.importFollows({ file })
- .then((status) => {
- if (!status) {
- throw new Error('failed')
- }
- })
- },
- importBlocks (file) {
- return this.backendInteractor.importBlocks({ file })
- .then((status) => {
- if (!status) {
- throw new Error('failed')
- }
- })
- },
- importMutes (file) {
- return this.backendInteractor.importMutes({ file })
- .then((status) => {
- if (!status) {
- throw new Error('failed')
- }
- })
- },
- generateExportableUsersContent (users) {
- // Get addresses
- return users.map((user) => {
- // check is it's a local user
- if (user && user.is_local) {
- // append the instance address
- return user.screen_name + '@' + location.hostname
+ importFollows(file) {
+ return this.backendInteractor.importFollows({ file }).then((status) => {
+ if (!status) {
+ throw new Error('failed')
}
- return user.screen_name
- }).join('\n')
+ })
},
- addBackup () {
- this.$store.state.api.backendInteractor.addBackup()
+ importBlocks(file) {
+ return this.backendInteractor.importBlocks({ file }).then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ importMutes(file) {
+ return this.backendInteractor.importMutes({ file }).then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ generateExportableUsersContent(users) {
+ // Get addresses
+ return users
+ .map((user) => {
+ // check is it's a local user
+ if (user && user.is_local) {
+ // append the instance address
+ return user.screen_name + '@' + location.hostname
+ }
+ return user.screen_name
+ })
+ .join('\n')
+ },
+ addBackup() {
+ this.$store.state.api.backendInteractor
+ .addBackup()
.then(() => {
this.addedBackup = true
this.addBackupError = false
@@ -90,8 +95,9 @@ const DataImportExportTab = {
})
.then(() => this.fetchBackups())
},
- fetchBackups () {
- this.$store.state.api.backendInteractor.listBackups()
+ fetchBackups() {
+ this.$store.state.api.backendInteractor
+ .listBackups()
.then((res) => {
this.backups = res
this.listBackupsError = false
@@ -99,8 +105,8 @@ const DataImportExportTab = {
.catch((error) => {
this.listBackupsError = error.error
})
- }
- }
+ },
+ },
}
export default DataImportExportTab
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.scss b/src/components/settings_modal/tabs/data_import_export_tab.scss
new file mode 100644
index 000000000..477dab66c
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export_tab.scss
@@ -0,0 +1,21 @@
+.data-import-export-tab {
+ .importer-exporter {
+ display: inline-flex;
+ flex-direction: column;
+ gap: 0.5em;
+ }
+
+ table {
+ td, th {
+ line-height: 1.5;
+ }
+
+ th {
+ padding: 0 0.5em;
+ }
+
+ td {
+ padding: 0.5em;
+ }
+ }
+}
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue
index eb3c86425..d5e540b8b 100644
--- a/src/components/settings_modal/tabs/data_import_export_tab.vue
+++ b/src/components/settings_modal/tabs/data_import_export_tab.vue
@@ -1,62 +1,77 @@
-
-
{{ $t('settings.follow_import') }}
-
{{ $t('settings.import_followers_from_a_csv_file') }}
-
-
-
-
{{ $t('settings.follow_export') }}
-
-
-
-
{{ $t('settings.block_import') }}
-
{{ $t('settings.import_blocks_from_a_csv_file') }}
-
-
-
-
{{ $t('settings.block_export') }}
-
-
-
-
{{ $t('settings.mute_import') }}
-
{{ $t('settings.import_mutes_from_a_csv_file') }}
-
-
-
-
{{ $t('settings.mute_export') }}
-
-
-
-
{{ $t('settings.account_backup') }}
-
{{ $t('settings.account_backup_description') }}
-
+
+
{{ $t('settings.import_export.title') }}
+
+
+ {{ $t('settings.import_export.follows') }}
+ {{ $t('settings.import_followers_from_a_csv_file') }}
+
+
+
+
+
+
+ {{ $t('settings.import_export.mutes') }}
+ {{ $t('settings.import_mutes_from_a_csv_file') }}
+
+
+
+
+
+
+ {{ $t('settings.import_export.blocks') }}
+ {{ $t('settings.import_blocks_from_a_csv_file') }}
+
+
+
+
+
+
+
{{ $t('settings.account_backup') }}
+
+
{{ $t('settings.account_backup_description') }}
+
+ {{ $t('settings.add_backup') }}
+
+
+ {{ $t('settings.added_backup') }}
+
+
+ {{ $t('settings.add_backup_error', { error: addBackupError }) }}
+
+
+
{{ $t('settings.account_backup_table_head') }}
@@ -111,21 +126,9 @@
/>
-
- {{ $t('settings.add_backup') }}
-
-
- {{ $t('settings.added_backup') }}
-
-
- {{ $t('settings.add_backup_error', { error: addBackupError }) }}
-
-
+
diff --git a/src/components/settings_modal/tabs/developer_tab.js b/src/components/settings_modal/tabs/developer_tab.js
new file mode 100644
index 000000000..9a6258638
--- /dev/null
+++ b/src/components/settings_modal/tabs/developer_tab.js
@@ -0,0 +1,47 @@
+import { mapState } from 'pinia'
+
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+
+import { cacheKey, clearCache, emojiCacheKey } from 'src/services/sw/sw.js'
+
+const pleromaFeCommitUrl =
+ 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
+
+const VersionTab = {
+ components: {
+ BooleanSetting,
+ },
+ computed: {
+ frontendVersionLink() {
+ return pleromaFeCommitUrl + this.frontendVersion
+ },
+ ...mapState(useInstanceStore, [
+ 'backendVersion',
+ 'backendRepository',
+ 'frontendVersion',
+ ]),
+ ...SharedComputedObject(),
+ },
+ methods: {
+ clearAssetCache() {
+ this.clearCache(cacheKey)
+ },
+ clearEmojiCache() {
+ this.clearCache(emojiCacheKey)
+ },
+ clearCache(key) {
+ clearCache(key)
+ .then(() => {
+ this.$store.dispatch('settingsSaved', { success: true })
+ })
+ .catch((error) => {
+ this.$store.dispatch('settingsSaved', { error })
+ })
+ },
+ },
+}
+
+export default VersionTab
diff --git a/src/components/settings_modal/tabs/developer_tab.scss b/src/components/settings_modal/tabs/developer_tab.scss
new file mode 100644
index 000000000..a94b5c122
--- /dev/null
+++ b/src/components/settings_modal/tabs/developer_tab.scss
@@ -0,0 +1,30 @@
+.developer-tab {
+ .setting-list {
+ margin-left: 2em;
+ }
+
+ .cache-buttons {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-column: 1 / 3;
+ column-gap: 0.5em;
+ margin: 0 5em;
+
+ .-mobile & {
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr 1fr;
+ row-gap: 0.5em;
+ margin: 0;
+ }
+ }
+
+
+ dt {
+ font-weight: 900;
+ }
+
+ dd {
+ margin-top: 0.5em;
+ margin-bottom: 1em;
+ }
+}
diff --git a/src/components/settings_modal/tabs/developer_tab.vue b/src/components/settings_modal/tabs/developer_tab.vue
new file mode 100644
index 000000000..3e0fe71cb
--- /dev/null
+++ b/src/components/settings_modal/tabs/developer_tab.vue
@@ -0,0 +1,80 @@
+
+
+
+
{{ $t('settings.version.title') }}
+
+ {{ $t('settings.version.backend_version') }}
+
+
+ {{ backendVersion }}
+
+
+ {{ $t('settings.version.frontend_version') }}
+
+
+ {{ frontendVersion }}
+
+
+
+
{{ $t('settings.debug') }}
+
+
+
+ {{ $t('settings.virtual_scrolling') }}
+
+
+
+
+ {{ $t('settings.theme_debug') }}
+
+
+
+
+ {{ $t('settings.force_theme_recompilation_debug') }}
+
+
+ {{ $t('settings.cache') }}
+
+
+
+
+ {{ $t('settings.clear_asset_cache') }}
+
+
+ {{ $t('settings.clear_emoji_cache') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 9a4ed814d..e0d0f6879 100644
--- a/src/components/settings_modal/tabs/filtering_tab.js
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -1,50 +1,52 @@
import { cloneDeep } from 'lodash'
-import { mapState, mapActions } from 'pinia'
-import { mapState as mapVuexState } from 'vuex'
-import { v4 as uuidv4 } from 'uuid';
+import { mapActions, mapState } from 'pinia'
+import { v4 as uuidv4 } from 'uuid'
-import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
-import { useInterfaceStore } from 'src/stores/interface'
-
-import {
- newImporter,
- newExporter
-} from 'src/services/export_import/export_import.js'
-
-import BooleanSetting from '../helpers/boolean_setting.vue'
-import ChoiceSetting from '../helpers/choice_setting.vue'
-import UnitSetting from '../helpers/unit_setting.vue'
-import IntegerSetting from '../helpers/integer_setting.vue'
-import HelpIndicator from '../helpers/help_indicator.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'
-
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import HelpIndicator from '../helpers/help_indicator.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
+import UnitSetting from '../helpers/unit_setting.vue'
+
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useInterfaceStore } from 'src/stores/interface'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+import { useSyncConfigStore } from 'src/stores/sync_config.js'
+
+import {
+ newExporter,
+ newImporter,
+} from 'src/services/export_import/export_import.js'
const SUPPORTED_TYPES = new Set(['word', 'regexp', 'user', 'user_regexp'])
const FilteringTab = {
- data () {
+ data() {
return {
- replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({
+ replyVisibilityOptions: ['all', 'following', 'self'].map((mode) => ({
key: mode,
value: mode,
- label: this.$t(`settings.reply_visibility_${mode}`)
+ label: this.$t(`settings.reply_visibility_${mode}`),
})),
- muteBlockLv1Options: ['ask', 'forever', 'temporarily'].map(mode => ({
+ muteBlockLv1Options: ['ask', 'forever', 'temporarily'].map((mode) => ({
key: mode,
value: mode,
- label: this.$t(`user_card.mute_block_${mode}`)
+ label: this.$t(`user_card.mute_block_${mode}`),
})),
- muteFiltersDraftObject: cloneDeep(useServerSideStorageStore().prefsStorage.simple.muteFilters),
+ muteFiltersDraftObject: cloneDeep(
+ useSyncConfigStore().prefsStorage.simple.muteFilters,
+ ),
muteFiltersDraftDirty: Object.fromEntries(
Object.entries(
- useServerSideStorageStore().prefsStorage.simple.muteFilters
- ).map(([k]) => [k, false])
+ useSyncConfigStore().prefsStorage.simple.muteFilters,
+ ).map(([k]) => [k, false]),
),
exportedFilter: null,
filterImporter: newImporter({
- validator (parsed) {
+ validator(parsed) {
if (Array.isArray(parsed)) return false
if (!SUPPORTED_TYPES.has(parsed.type)) return false
return true
@@ -55,7 +57,8 @@ const FilteringTab = {
expires = null,
hide = false,
name = '',
- value = ''
+ value = '',
+ caseSensitive = false,
} = data
this.createFilter({
@@ -63,22 +66,22 @@ const FilteringTab = {
expires,
hide,
name,
- value
+ value,
+ caseSensitive,
})
},
- onImportFailure (result) {
+ onImportFailure(result) {
console.error('Failure importing filter:', result)
- useInterfaceStore()
- .pushGlobalNotice({
- messageKey: 'settings.filter.import_failure',
- level: 'error'
- })
- }
+ useInterfaceStore().pushGlobalNotice({
+ messageKey: 'settings.filter.import_failure',
+ level: 'error',
+ })
+ },
}),
filterExporter: newExporter({
filename: 'pleromafe_mute-filter',
- getExportedObject: () => this.exportedFilter
- })
+ getExportedObject: () => this.exportedFilter,
+ }),
}
},
components: {
@@ -88,69 +91,78 @@ const FilteringTab = {
IntegerSetting,
Checkbox,
Select,
- HelpIndicator
+ HelpIndicator,
},
computed: {
...SharedComputedObject(),
- ...mapState(
- useServerSideStorageStore,
- {
- muteFilters: store => Object.entries(store.prefsStorage.simple.muteFilters),
- muteFiltersObject: store => store.prefsStorage.simple.muteFilters
- }
- ),
- ...mapVuexState({
- blockExpirationSupported: state => state.instance.blockExpiration
+ ...mapState(useSyncConfigStore, {
+ muteFilters: (store) =>
+ Object.entries(store.prefsStorage.simple.muteFilters),
+ muteFiltersObject: (store) => store.prefsStorage.simple.muteFilters,
}),
+ ...mapState(useInstanceCapabilitiesStore, ['blockExpiration']),
onMuteDefaultActionLv1: {
- get () {
- const value = this.$store.state.config.onMuteDefaultAction
+ get() {
+ const value = useMergedConfigStore().mergedConfig.onMuteDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
- set (value) {
+ set(value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
- this.$store.dispatch('setOption', { name: 'onMuteDefaultAction', value: realValue })
- }
+ this.setPreference({
+ path: 'simple.onMuteDefaultAction',
+ value: realValue,
+ })
+ },
},
onBlockDefaultActionLv1: {
- get () {
- const value = this.$store.state.config.onBlockDefaultAction
+ get() {
+ const value = useMergedConfigStore().mergedConfig.onBlockDefaultAction
if (value === 'ask' || value === 'forever') {
return value
} else {
return 'temporarily'
}
},
- set (value) {
+ set(value) {
let realValue = value
if (value !== 'ask' && value !== 'forever') {
realValue = '14d'
}
- this.$store.dispatch('setOption', { name: 'onBlockDefaultAction', value: realValue })
- }
+ this.setPreference({
+ path: 'simple.onBlockDefaultAction',
+ value: realValue,
+ })
+ },
},
- muteFiltersDraft () {
+ muteFiltersDraft() {
return Object.entries(this.muteFiltersDraftObject)
},
- muteFiltersExpired () {
+ muteFiltersExpired() {
const now = Date.now()
- return Object
- .entries(this.muteFiltersDraftObject)
- .filter(([, { expires }]) => expires != null && expires <= now)
- }
+ return Object.entries(this.muteFiltersDraftObject).filter(
+ ([, { expires }]) => expires != null && expires <= now,
+ )
+ },
},
methods: {
- ...mapActions(useServerSideStorageStore, ['setPreference', 'unsetPreference', 'pushServerSideStorage']),
- getDatetimeLocal (timestamp) {
+ ...mapActions(useSyncConfigStore, [
+ 'setPreference',
+ 'setSimplePrefAndSave',
+ 'unsetSimplePrefAndSave',
+ 'unsetPreference',
+ 'unsetPrefAndSave',
+ 'pushSyncConfig',
+ ]),
+ getDatetimeLocal(timestamp) {
const date = new Date(timestamp)
- const fmt = new Intl.NumberFormat("en-US", {minimumIntegerDigits: 2})
+ const fmt = new Intl.NumberFormat('en-US', { minimumIntegerDigits: 2 })
const datetime = [
date.getFullYear(),
'-',
@@ -160,11 +172,11 @@ const FilteringTab = {
'T',
fmt.format(date.getHours()),
':',
- fmt.format(date.getMinutes())
+ fmt.format(date.getMinutes()),
].join('')
return datetime
},
- checkRegexValid (id) {
+ checkRegexValid(id) {
const filter = this.muteFiltersObject[id]
if (filter.type !== 'regexp') return true
if (filter.type !== 'user_regexp') return true
@@ -178,20 +190,21 @@ const FilteringTab = {
}
return valid
},
- createFilter (filter = {
- type: 'word',
- value: '',
- name: 'New Filter',
- enabled: true,
- expires: null,
- hide: false,
- }) {
+ createFilter(
+ filter = {
+ type: 'word',
+ value: '',
+ name: 'New Filter',
+ enabled: true,
+ expires: null,
+ hide: false,
+ },
+ ) {
const newId = uuidv4()
filter.order = this.muteFilters.length + 2
this.muteFiltersDraftObject[newId] = filter
- this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
- this.pushServerSideStorage()
+ this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
},
exportFilter(id) {
this.exportedFilter = { ...this.muteFiltersDraftObject[id] }
@@ -201,26 +214,23 @@ const FilteringTab = {
importFilter() {
this.filterImporter.importData()
},
- copyFilter (id) {
+ copyFilter(id) {
const filter = { ...this.muteFiltersDraftObject[id] }
const newId = uuidv4()
this.muteFiltersDraftObject[newId] = filter
- this.setPreference({ path: 'simple.muteFilters.' + newId , value: filter })
- this.pushServerSideStorage()
+ this.setSimplePrefAndSave({ path: 'muteFilters.' + newId, value: filter })
},
- deleteFilter (id) {
+ deleteFilter(id) {
delete this.muteFiltersDraftObject[id]
- this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
- this.pushServerSideStorage()
+ this.unsetSimplePrefAndSave({ path: 'muteFilters.' + id, value: null })
},
- purgeExpiredFilters () {
+ purgeExpiredFilters() {
this.muteFiltersExpired.forEach(([id]) => {
- console.log(id)
delete this.muteFiltersDraftObject[id]
- this.unsetPreference({ path: 'simple.muteFilters.' + id , value: null })
+ this.unsetPreference({ path: 'simple.muteFilters.' + id, value: null })
})
- this.pushServerSideStorage()
+ this.pushSyncConfig()
},
updateFilter(id, field, value) {
const filter = { ...this.muteFiltersDraftObject[id] }
@@ -242,17 +252,24 @@ const FilteringTab = {
this.muteFiltersDraftDirty[id] = true
},
saveFilter(id) {
- this.setPreference({ path: 'simple.muteFilters.' + id , value: this.muteFiltersDraftObject[id] })
- this.pushServerSideStorage()
+ this.setSimplePrefAndSave({
+ path: 'muteFilters.' + id,
+ value: this.muteFiltersDraftObject[id],
+ })
this.muteFiltersDraftDirty[id] = false
},
},
// Updating nested properties
watch: {
- replyVisibility () {
+ replyVisibility() {
this.$store.dispatch('queueFlushAll')
- }
- }
+ },
+ muteFiltersObject() {
+ this.muteFiltersDraftObject = cloneDeep(
+ useMergedConfigStore().mergedConfig.muteFilters,
+ )
+ },
+ },
}
export default FilteringTab
diff --git a/src/components/settings_modal/tabs/filtering_tab.scss b/src/components/settings_modal/tabs/filtering_tab.scss
index 5b51c5e4d..9550ef7bf 100644
--- a/src/components/settings_modal/tabs/filtering_tab.scss
+++ b/src/components/settings_modal/tabs/filtering_tab.scss
@@ -51,7 +51,16 @@
text-align: right;
}
+ > label.checkbox {
+ display: grid;
+ grid-template-columns: subgrid;
+ grid-template-rows: subgrid;
+ grid-column: 1 / span 2;
+ text-align: right;
+ }
+
.filter-field-value {
+ display: flex;
grid-column: 2 / span 2;
}
}
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index a699653f0..eaf1665d7 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -1,10 +1,7 @@
-
-
-
{{ $t('settings.filter.clutter') }}
+
+
+
{{ $t('settings.filter.mute_filter') }}
-
- {{ $t('settings.hide_post_stats') }}
-
-
-
-
- {{ $t('settings.hide_user_stats') }}
-
-
-
-
- {{ $t('settings.hide_actor_type_indication') }}
-
-
-
-
- {{ $t('settings.hide_scrobbles') }}
-
-
-
-
- {{ $t('settings.hide_scrobbles_after') }}
-
-
-
-
- {{ $t('settings.attachments') }}
-
-
- {{ $t('settings.max_thumbnails') }}
-
-
-
-
- {{ $t('settings.hide_attachments_in_tl') }}
-
-
-
-
- {{ $t('settings.hide_attachments_in_convo') }}
-
-
-
-
-
-
{{ $t('settings.filter.mute_filter') }}
-
-
- {{ $t('user_card.default_mute_expiration') }}
-
-
+
+ {{ $t('user_card.default_mute_expiration') }}
+
+
- {{ option.label }}
-
-
+
+ {{ option.label }}
+
+
+
-
- {{ $t('user_card.default_block_expiration') }}
-
-
+
+
+ {{ $t('user_card.default_block_expiration') }}
+
+
- {{ option.label }}
-
-
+
+ {{ option.label }}
+
+
+
+
+
+
+ {{ $t('settings.filter.case_sensitive') }}
+
+
({
+ absoluteTime12hOptions: ['24h', '12h'].map((mode) => ({
key: mode,
value: mode,
- label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
+ label: this.$t(`settings.absolute_time_format_12h_${mode}`),
})),
- conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
- key: mode,
- value: mode,
- label: this.$t(`settings.conversation_display_${mode}`)
- })),
- absoluteTime12hOptions: ['24h', '12h'].map(mode => ({
- key: mode,
- value: mode,
- label: this.$t(`settings.absolute_time_format_12h_${mode}`)
- })),
- conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
- key: mode,
- value: mode,
- label: this.$t(`settings.conversation_other_replies_button_${mode}`)
- })),
- mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
- key: mode,
- value: mode,
- label: this.$t(`settings.mention_link_display_${mode}`)
- })),
- userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({
- key: mode,
- value: mode,
- label: this.$t(`settings.user_popover_avatar_action_${mode}`)
- })),
- unsavedPostActionOptions: ['save', 'discard', 'confirm'].map(mode => ({
- key: mode,
- value: mode,
- label: this.$t(`settings.unsaved_post_action_${mode}`)
- })),
- loopSilentAvailable:
- // Firefox
- Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
- // Chrome-likes
- Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
- // Future spec, still not supported in Nightly 63 as of 08/2018
- Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
- emailLanguage: this.$store.state.users.currentUser.language || ['']
+ emailLanguage: this.$store.state.users.currentUser.language || [''],
}
},
components: {
BooleanSetting,
ChoiceSetting,
- IntegerSetting,
- FloatSetting,
UnitSetting,
+ FloatSetting,
+ FontControl,
InterfaceLanguageSwitcher,
- ProfileSettingIndicator,
- ScopeSelector,
- Select
},
computed: {
- postFormats () {
- return this.$store.state.instance.postFormats || []
- },
- postContentOptions () {
- return this.postFormats.map(format => ({
- key: format,
- value: format,
- label: this.$t(`post_status.content_type["${format}"]`)
- }))
- },
language: {
- get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
+ get: function () {
+ return useMergedConfigStore().mergedConfig.interfaceLanguage
+ },
set: function (val) {
- this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
- }
+ useSyncConfigStore().setSimplePrefAndSave({
+ path: 'interfaceLanguage',
+ value: val,
+ })
+ },
},
- instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
- instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
...SharedComputedObject(),
- ...mapState({
- blockExpirationSupported: state => state.instance.blockExpiration,
- })
+ ...mapState(useInstanceCapabilitiesStore, ['blockExpiration']),
},
methods: {
- changeDefaultScope (value) {
- this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
- },
- clearCache (key) {
- clearCache(key)
- .then(() => {
- this.$store.dispatch('settingsSaved', { success: true })
- })
- .catch(error => {
- this.$store.dispatch('settingsSaved', { error })
- })
- },
- clearAssetCache () {
- this.clearCache(cacheKey)
- },
- clearEmojiCache () {
- this.clearCache(emojiCacheKey)
- },
- updateProfile () {
+ updateProfile() {
const params = {
- language: localeService.internalToBackendLocaleMulti(this.emailLanguage)
+ language: localeService.internalToBackendLocaleMulti(
+ this.emailLanguage,
+ ),
}
this.$store.state.api.backendInteractor
@@ -137,7 +65,10 @@ const GeneralTab = {
this.$store.commit('setCurrentUser', user)
})
},
- }
+ updateFont(path, value) {
+ useLocalConfigStore().set({ path, value })
+ },
+ },
}
export default GeneralTab
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 49fc57c79..76710bec9 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -1,35 +1,114 @@
-
-
-
{{ $t('settings.interface') }}
+
+
+
{{ $t('settings.format_and_language') }}
+
+ {{ $t('settings.interfaceLanguage') }}
+ language = val"
+ />
+ {{ $t('settings.email_language') }}
+
+
+
+ {{ $t('settings.absolute_time_format') }}
+
+
+
+
+ {{ $t('settings.absolute_time_format_12h') }}
+
+
+
+
{{ $t('settings.scale_and_font') }}
+
{{ $t('settings.timelines') }}
+
{{ $t('settings.streaming') }}
@@ -48,79 +127,24 @@
{{ $t('settings.useStreamingApi') }}
-
-
- {{ $t('settings.virtual_scrolling') }}
-
-
-
-
- {{ $t('settings.user_popover_avatar_action') }}
-
-
-
-
- {{ $t('settings.user_popover_avatar_overlay') }}
-
-
-
-
- {{ $t('settings.user_card_left_justify') }}
-
-
-
-
- {{ $t('settings.user_card_hide_personal_marks') }}
-
-
-
-
- {{ $t('settings.always_show_post_button') }}
-
-
-
-
- {{ $t('settings.autohide_floating_post_button') }}
-
-
-
-
- {{ $t('settings.hide_shoutbox') }}
-
-
+
+
+ {{ $t('settings.confirmations') }}
+
+
- {{ $t('settings.confirm_dialogs') }}
+
+ {{ $t('settings.confirm_dialogs') }}
+
@@ -132,9 +156,10 @@
{{ $t('settings.confirm_dialogs_unfollow') }}
-
+
{{ $t('settings.confirm_dialogs_block') }}
@@ -179,383 +204,6 @@
-
-
{{ $t('settings.post_look_feel') }}
-
-
-
- {{ $t('settings.conversation_display') }}
-
-
-
-
-
- {{ $t('settings.tree_advanced') }}
-
-
-
-
- {{ $t('settings.tree_fade_ancestors') }}
-
-
-
-
- {{ $t('settings.max_depth_in_thread') }}
-
-
-
-
- {{ $t('settings.conversation_other_replies_button') }}
-
-
-
-
-
- {{ $t('settings.collapse_subject') }}
-
-
-
-
- {{ $t('settings.emoji_reactions_on_timeline') }}
-
-
-
-
- {{ $t('settings.no_rich_text_description') }}
-
-
-
-
- {{ $t('settings.absolute_time_format') }}
-
-
-
-
-
- {{ $t('settings.absolute_time_format_min_age') }}
-
-
-
-
- {{ $t('settings.absolute_time_format_12h') }}
-
-
-
- {{ $t('settings.attachments') }}
-
-
- {{ $t('settings.image_compression') }}
-
-
-
-
-
- {{ $t('settings.always_use_jpeg') }}
-
-
-
-
-
- {{ $t('settings.use_contain_fit') }}
-
-
-
-
- {{ $t('settings.nsfw_clickthrough') }}
-
-
-
-
-
- {{ $t('settings.preload_images') }}
-
-
-
-
- {{ $t('settings.use_one_click_nsfw') }}
-
-
-
-
-
- {{ $t('settings.loop_video') }}
-
-
-
-
-
- {{ $t('settings.play_videos_in_modal') }}
-
-
- {{ $t('settings.mention_links') }}
-
-
- {{ $t('settings.mention_link_display') }}
-
-
-
-
- {{ $t('settings.mention_link_use_tooltip') }}
-
-
-
-
- {{ $t('settings.mention_link_show_avatar') }}
-
-
-
-
- {{ $t('settings.mention_link_fade_domain') }}
-
-
-
-
- {{ $t('settings.mention_link_bolden_you') }}
-
-
-
- {{ $t('settings.fun') }}
-
-
-
- {{ $t('settings.greentext') }}
-
-
-
-
- {{ $t('settings.show_yous') }}
-
-
-
-
-
-
-
{{ $t('settings.composing') }}
-
-
-
- {{ $t('settings.default_vis') }}
-
-
-
-
-
-
- {{ $t('settings.sensitive_by_default') }}
-
-
-
-
- {{ $t('settings.scope_copy') }}
-
-
-
-
- {{ $t('settings.subject_input_always_show') }}
-
-
-
-
- {{ $t('settings.subject_line_behavior') }}
-
-
-
-
- {{ $t('settings.post_status_content_type') }}
-
-
-
-
- {{ $t('settings.minimal_scopes_mode') }}
-
-
-
-
- {{ $t('settings.pad_emoji') }}
-
-
-
-
- {{ $t('settings.autocomplete_select_first') }}
-
-
-
-
- {{ $t('settings.auto_save_draft') }}
-
-
-
-
- {{ $t('settings.unsaved_post_action') }}
-
-
-
-
-
-
{{ $t('settings.cache') }}
-
-
-
- {{ $t('settings.clear_asset_cache') }}
-
-
-
-
- {{ $t('settings.clear_emoji_cache') }}
-
-
-
-
diff --git a/src/components/settings_modal/tabs/layout_tab.js b/src/components/settings_modal/tabs/layout_tab.js
new file mode 100644
index 000000000..fc56e1392
--- /dev/null
+++ b/src/components/settings_modal/tabs/layout_tab.js
@@ -0,0 +1,51 @@
+import { mapState } from 'pinia'
+
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import UnitSetting from '../helpers/unit_setting.vue'
+
+import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
+const GeneralTab = {
+ data() {
+ return {
+ thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(
+ (mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.third_column_mode_${mode}`),
+ }),
+ ),
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ UnitSetting,
+ },
+ computed: {
+ ...mapState(useInstanceCapabilitiesStore, [
+ 'postFormats',
+ 'suggestionsEnabled',
+ ]),
+ columns() {
+ const mode = useMergedConfigStore().mergedConfig.thirdColumnMode
+
+ const notif = mode === 'none' ? [] : ['notifs']
+
+ if (
+ useMergedConfigStore().mergedConfig.sidebarRight ||
+ mode === 'postform'
+ ) {
+ return [...notif, 'content', 'sidebar']
+ } else {
+ return ['sidebar', 'content', ...notif]
+ }
+ },
+ ...SharedComputedObject(),
+ },
+}
+
+export default GeneralTab
diff --git a/src/components/settings_modal/tabs/layout_tab.vue b/src/components/settings_modal/tabs/layout_tab.vue
new file mode 100644
index 000000000..0035f3a11
--- /dev/null
+++ b/src/components/settings_modal/tabs/layout_tab.vue
@@ -0,0 +1,131 @@
+
+
+
+
{{ $t('settings.general') }}
+
+
+
+ {{ $t('settings.mobile_center_dialog') }}
+
+
+
+
+ {{ $t('settings.always_show_post_button') }}
+
+
+
+
+ {{ $t('settings.autohide_floating_post_button') }}
+
+
+
+
+ {{ $t('settings.user_popover_avatar_overlay') }}
+
+
+
+
+ {{ $t('settings.user_card_left_justify') }}
+
+
+
+
+ {{ $t('settings.theme_editor_min_width') }}
+
+
+
+
+ {{ $t('settings.navbar_size') }}
+
+
+
+
{{ $t('settings.columns') }}
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
index 36dc8a090..3b82bbb4c 100644
--- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
+++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js
@@ -1,47 +1,54 @@
-import get from 'lodash/get'
-import map from 'lodash/map'
-import reject from 'lodash/reject'
-import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
-import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
-import BlockCard from 'src/components/block_card/block_card.vue'
-import MuteCard from 'src/components/mute_card/mute_card.vue'
-import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
-import SelectableList from 'src/components/selectable_list/selectable_list.vue'
-import ProgressButton from 'src/components/progress_button/progress_button.vue'
-import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
+import { get, map, reject } from 'lodash'
+
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
+import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
+import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
+import BlockCard from 'src/components/block_card/block_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
-import { useOAuthTokensStore } from 'src/stores/oauth_tokens'
+import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
+import MuteCard from 'src/components/mute_card/mute_card.vue'
+import ProgressButton from 'src/components/progress_button/progress_button.vue'
+import SelectableList from 'src/components/selectable_list/selectable_list.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js'
const BlockList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
- select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
- destroy: () => {},
- childPropName: 'items'
+ select: (props, $store) =>
+ get($store.state.users.currentUser, 'blockIds', []),
+ destroy: () => {
+ /* no-op */
+ },
+ childPropName: 'items',
})(SelectableList)
const MuteList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
- destroy: () => {},
- childPropName: 'items'
+ destroy: () => {
+ /* no-op */
+ },
+ childPropName: 'items',
})(SelectableList)
const DomainMuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
- select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
- childPropName: 'items'
+ select: (props, $store) =>
+ get($store.state.users.currentUser, 'domainMutes', []),
+ childPropName: 'items',
})(SelectableList)
const MutesAndBlocks = {
- data () {
+ data() {
return {
- activeTab: 'profile'
+ activeTab: 'profile',
}
},
- created () {
+ created() {
useOAuthTokensStore().fetchTokens()
- this.$store.dispatch('getKnownDomains')
+ useInstanceStore().getKnownDomains()
},
components: {
TabSwitcher,
@@ -53,87 +60,94 @@ const MutesAndBlocks = {
DomainMuteCard,
ProgressButton,
Autosuggest,
- Checkbox
+ Checkbox,
},
computed: {
- knownDomains () {
- return this.$store.state.instance.knownDomains
+ knownDomains() {
+ return useInstanceStore().knownDomains
},
- user () {
+ user() {
return this.$store.state.users.currentUser
- }
+ },
},
methods: {
- importFollows (file) {
- return this.$store.state.api.backendInteractor.importFollows({ file })
+ importFollows(file) {
+ return this.$store.state.api.backendInteractor
+ .importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
- importBlocks (file) {
- return this.$store.state.api.backendInteractor.importBlocks({ file })
+ importBlocks(file) {
+ return this.$store.state.api.backendInteractor
+ .importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
- generateExportableUsersContent (users) {
+ generateExportableUsersContent(users) {
// Get addresses
- return users.map((user) => {
- // check is it's a local user
- if (user && user.is_local) {
- // append the instance address
- return user.screen_name + '@' + location.hostname
- }
- return user.screen_name
- }).join('\n')
+ return users
+ .map((user) => {
+ // check is it's a local user
+ if (user && user.is_local) {
+ // append the instance address
+ return user.screen_name + '@' + location.hostname
+ }
+ return user.screen_name
+ })
+ .join('\n')
},
- activateTab (tabName) {
+ activateTab(tabName) {
this.activeTab = tabName
},
- filterUnblockedUsers (userIds) {
+ filterUnblockedUsers(userIds) {
return reject(userIds, (userId) => {
const relationship = this.$store.getters.relationship(this.userId)
return relationship.blocking || userId === this.user.id
})
},
- filterUnMutedUsers (userIds) {
+ filterUnMutedUsers(userIds) {
return reject(userIds, (userId) => {
const relationship = this.$store.getters.relationship(this.userId)
return relationship.muting || userId === this.user.id
})
},
- queryUserIds (query) {
- return this.$store.dispatch('searchUsers', { query })
+ queryUserIds(query) {
+ return this.$store
+ .dispatch('searchUsers', { query })
.then((users) => map(users, 'id'))
},
- blockUsers (ids) {
+ blockUsers(ids) {
return this.$store.dispatch('blockUsers', ids)
},
- unblockUsers (ids) {
+ unblockUsers(ids) {
return this.$store.dispatch('unblockUsers', ids)
},
- muteUsers (ids) {
+ muteUsers(ids) {
return this.$store.dispatch('muteUsers', ids)
},
- unmuteUsers (ids) {
+ unmuteUsers(ids) {
return this.$store.dispatch('unmuteUsers', ids)
},
- filterUnMutedDomains (urls) {
- return urls.filter(url => !this.user.domainMutes.includes(url))
+ filterUnMutedDomains(urls) {
+ return urls.filter((url) => !this.user.domainMutes.includes(url))
},
- queryKnownDomains (query) {
+ queryKnownDomains(query) {
return new Promise((resolve) => {
- resolve(this.knownDomains.filter(url => url.toLowerCase().includes(query)))
+ resolve(
+ this.knownDomains.filter((url) => url.toLowerCase().includes(query)),
+ )
})
},
- unmuteDomains (domains) {
+ unmuteDomains(domains) {
return this.$store.dispatch('unmuteDomains', domains)
- }
- }
+ },
+ },
}
export default MutesAndBlocks
diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js
index c53b5889d..c863a7b9f 100644
--- a/src/components/settings_modal/tabs/notifications_tab.js
+++ b/src/components/settings_modal/tabs/notifications_tab.js
@@ -2,32 +2,36 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const NotificationsTab = {
- data () {
+ data() {
return {
activeTab: 'profile',
- notificationSettings: this.$store.state.users.currentUser.notification_settings,
- newDomainToMute: ''
+ notificationSettings:
+ this.$store.state.users.currentUser.notification_settings,
+ newDomainToMute: '',
}
},
components: {
- BooleanSetting
+ BooleanSetting,
},
computed: {
- user () {
+ user() {
return this.$store.state.users.currentUser
},
- canReceiveReports () {
- if (!this.user) { return false }
+ canReceiveReports() {
+ if (!this.user) {
+ return false
+ }
return this.user.privileges.includes('reports_manage_reports')
},
- ...SharedComputedObject()
+ ...SharedComputedObject(),
},
methods: {
- updateNotificationSettings () {
- this.$store.state.api.backendInteractor
- .updateNotificationSettings({ settings: this.notificationSettings })
- }
- }
+ updateNotificationSettings() {
+ this.$store.state.api.backendInteractor.updateNotificationSettings({
+ settings: this.notificationSettings,
+ })
+ },
+ },
}
export default NotificationsTab
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index 10228888c..ba50f5b21 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -1,7 +1,7 @@
-
-
{{ $t('settings.notification_setting_annoyance') }}
+
+
{{ $t('settings.notification_setting_annoyance') }}
@@ -12,11 +12,9 @@
{{ $t('settings.notification_setting_ignore_inactionable_seen') }}
-
-
- {{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }}
-
-
+
+ {{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }}
+
-
-
{{ $t('settings.notification_setting_filters') }}
+
+
{{ $t('settings.notification_setting_filters') }}
-
-
{{ $t('settings.notification_mutes') }}
-
{{ $t('settings.notification_blocks') }}
-
+
+
+ {{ $t('settings.notification_mutes') }}
+ {{ $t('settings.notification_blocks') }}
+
+
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.js
similarity index 65%
rename from src/components/settings_modal/tabs/theme_tab/theme_tab.js
rename to src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.js
index 4b558471f..83f993c86 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.js
@@ -1,50 +1,52 @@
+import Checkbox from 'src/components/checkbox/checkbox.vue'
+import ColorInput from 'src/components/color_input/color_input.vue'
+import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
+import FontControl from 'src/components/font_control/font_control.vue'
+import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
+import RangeInput from 'src/components/range_input/range_input.vue'
+import Select from 'src/components/select/select.vue'
+import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+import Preview from './theme_preview.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+import { useInterfaceStore } from 'src/stores/interface.js'
+import { useMergedConfigStore } from 'src/stores/merged_config.js'
+
import {
- rgb2hex,
- hex2rgb,
getContrastRatioLayers,
- relativeLuminance
+ hex2rgb,
+ relativeLuminance,
+ rgb2hex,
} from 'src/services/color_convert/color_convert.js'
import {
+ newExporter,
newImporter,
- newExporter
} from 'src/services/export_import/export_import.js'
import {
- SLOT_INHERITANCE
-} from 'src/services/theme_data/pleromafe.js'
-import {
- CURRENT_VERSION,
- OPACITIES,
- getLayers,
- getOpacitySlot,
- DEFAULT_SHADOWS,
- generateColors,
- generateShadows,
- generateRadii,
- generateFonts,
- shadows2to3,
- colors2to3
-} from 'src/services/theme_data/theme_data.service.js'
-
-import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
-import { init } from 'src/services/theme_data/theme_data_3.service.js'
+ adoptStyleSheets,
+ createStyleSheet,
+} from 'src/services/style_setter/style_setter.js'
import {
getCssRules,
- getScopedVersion
+ getScopedVersion,
} from 'src/services/theme_data/css_utils.js'
-import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
-
-import ColorInput from 'src/components/color_input/color_input.vue'
-import RangeInput from 'src/components/range_input/range_input.vue'
-import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
-import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
-import FontControl from 'src/components/font_control/font_control.vue'
-import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
-import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
-import Checkbox from 'src/components/checkbox/checkbox.vue'
-import Select from 'src/components/select/select.vue'
-
-import Preview from './theme_preview.vue'
-import { useInterfaceStore } from 'src/stores/interface'
+import { SLOT_INHERITANCE } from 'src/services/theme_data/pleromafe.js'
+import {
+ CURRENT_VERSION,
+ colors2to3,
+ DEFAULT_SHADOWS,
+ generateColors,
+ generateFonts,
+ generateRadii,
+ generateShadows,
+ getLayers,
+ getOpacitySlot,
+ OPACITIES,
+ shadows2to3,
+} from 'src/services/theme_data/theme_data.service.js'
+import { init } from 'src/services/theme_data/theme_data_3.service.js'
+import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
// List of color values used in v1
const v1OnlyNames = [
@@ -55,8 +57,8 @@ const v1OnlyNames = [
'cRed',
'cGreen',
'cBlue',
- 'cOrange'
-].map(_ => _ + 'ColorLocal')
+ 'cOrange',
+].map((_) => _ + 'ColorLocal')
const colorConvert = (color) => {
if (color.startsWith('--') || color === 'transparent') {
@@ -67,20 +69,20 @@ const colorConvert = (color) => {
}
export default {
- data () {
+ data() {
return {
themeImporter: newImporter({
validator: this.importValidator,
onImport: this.onImport,
- onImportFailure: this.onImportFailure
+ onImportFailure: this.onImportFailure,
}),
themeExporter: newExporter({
filename: 'pleroma_theme',
- getExportedObject: () => this.exportedTheme
+ getExportedObject: () => this.exportedTheme,
}),
availableStyles: [],
selected: '',
- selectedTheme: this.$store.getters.mergedConfig.theme,
+ selectedTheme: useMergedConfigStore().mergedConfig.theme,
themeWarning: undefined,
tempImportFile: undefined,
engineVersion: 0,
@@ -98,12 +100,18 @@ export default {
keepFonts: false,
...Object.keys(SLOT_INHERITANCE)
- .map(key => [key, ''])
- .reduce((acc, [key, val]) => ({ ...acc, [key + 'ColorLocal']: val }), {}),
+ .map((key) => [key, ''])
+ .reduce(
+ (acc, [key, val]) => ({ ...acc, [key + 'ColorLocal']: val }),
+ {},
+ ),
...Object.keys(OPACITIES)
- .map(key => [key, ''])
- .reduce((acc, [key, val]) => ({ ...acc, [key + 'OpacityLocal']: val }), {}),
+ .map((key) => [key, ''])
+ .reduce(
+ (acc, [key, val]) => ({ ...acc, [key + 'OpacityLocal']: val }),
+ {},
+ ),
shadowSelected: undefined,
shadowsLocal: {},
@@ -117,11 +125,11 @@ export default {
avatarAltRadiusLocal: '',
attachmentRadiusLocal: '',
tooltipRadiusLocal: '',
- chatMessageRadiusLocal: ''
+ chatMessageRadiusLocal: '',
}
},
- created () {
- const currentIndex = this.$store.state.instance.themesIndex
+ created() {
+ const currentIndex = useInstanceStore().themesIndex
let promise
if (currentIndex) {
@@ -130,50 +138,48 @@ export default {
promise = useInterfaceStore().fetchThemesIndex()
}
- promise.then(themesIndex => {
- Object
- .values(themesIndex)
- .forEach(themeFunc => {
- themeFunc().then(themeData => themeData && this.availableStyles.push(themeData))
- })
+ promise.then((themesIndex) => {
+ Object.values(themesIndex).forEach((themeFunc) => {
+ themeFunc().then(
+ (themeData) => themeData && this.availableStyles.push(themeData),
+ )
+ })
})
},
- mounted () {
+ mounted() {
if (typeof this.shadowSelected === 'undefined') {
this.shadowSelected = this.shadowsAvailable[0]
}
},
computed: {
- themeWarningHelp () {
+ themeWarningHelp() {
if (!this.themeWarning) return
const t = this.$t
const pre = 'settings.style.switcher.help.'
- const {
- origin,
- themeEngineVersion,
- type,
- noActionsPossible
- } = this.themeWarning
+ const { origin, themeEngineVersion, type, noActionsPossible } =
+ this.themeWarning
if (origin === 'file') {
// Loaded v2 theme from file
if (themeEngineVersion === 2 && type === 'wrong_version') {
return t(pre + 'v2_imported')
}
if (themeEngineVersion > CURRENT_VERSION) {
- return t(pre + 'future_version_imported') + ' ' +
- (
- noActionsPossible
- ? t(pre + 'snapshot_missing')
- : t(pre + 'snapshot_present')
- )
+ return (
+ t(pre + 'future_version_imported') +
+ ' ' +
+ (noActionsPossible
+ ? t(pre + 'snapshot_missing')
+ : t(pre + 'snapshot_present'))
+ )
}
if (themeEngineVersion < CURRENT_VERSION) {
- return t(pre + 'future_version_imported') + ' ' +
- (
- noActionsPossible
- ? t(pre + 'snapshot_missing')
- : t(pre + 'snapshot_present')
- )
+ return (
+ t(pre + 'future_version_imported') +
+ ' ' +
+ (noActionsPossible
+ ? t(pre + 'snapshot_missing')
+ : t(pre + 'snapshot_present'))
+ )
}
} else if (origin === 'localStorage') {
if (type === 'snapshot_source_mismatch') {
@@ -185,38 +191,40 @@ export default {
}
// Admin downgraded FE
if (themeEngineVersion > CURRENT_VERSION) {
- return t(pre + 'fe_downgraded') + ' ' +
- (
- noActionsPossible
- ? t(pre + 'migration_snapshot_ok')
- : t(pre + 'migration_snapshot_gone')
- )
+ return (
+ t(pre + 'fe_downgraded') +
+ ' ' +
+ (noActionsPossible
+ ? t(pre + 'migration_snapshot_ok')
+ : t(pre + 'migration_snapshot_gone'))
+ )
}
// Admin upgraded FE
if (themeEngineVersion < CURRENT_VERSION) {
- return t(pre + 'fe_upgraded') + ' ' +
- (
- noActionsPossible
- ? t(pre + 'migration_snapshot_ok')
- : t(pre + 'migration_snapshot_gone')
- )
+ return (
+ t(pre + 'fe_upgraded') +
+ ' ' +
+ (noActionsPossible
+ ? t(pre + 'migration_snapshot_ok')
+ : t(pre + 'migration_snapshot_gone'))
+ )
}
}
},
- selectedVersion () {
+ selectedVersion() {
return Array.isArray(this.selectedTheme) ? 1 : 2
},
- currentColors () {
+ currentColors() {
return Object.keys(SLOT_INHERITANCE)
- .map(key => [key, this[key + 'ColorLocal']])
+ .map((key) => [key, this[key + 'ColorLocal']])
.reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
},
- currentOpacity () {
+ currentOpacity() {
return Object.keys(OPACITIES)
- .map(key => [key, this[key + 'OpacityLocal']])
+ .map((key) => [key, this[key + 'OpacityLocal']])
.reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
},
- currentRadii () {
+ currentRadii() {
return {
btn: this.btnRadiusLocal,
input: this.inputRadiusLocal,
@@ -226,11 +234,11 @@ export default {
avatarAlt: this.avatarAltRadiusLocal,
tooltip: this.tooltipRadiusLocal,
attachment: this.attachmentRadiusLocal,
- chatMessage: this.chatMessageRadiusLocal
+ chatMessage: this.chatMessageRadiusLocal,
}
},
// This needs optimization maybe
- previewContrast () {
+ previewContrast() {
try {
if (!this.previewTheme.colors.bg) return {}
const colors = this.previewTheme.colors
@@ -243,113 +251,124 @@ export default {
aaa: ratio >= 7,
// same but for 18pt+ texts
laa: ratio >= 3,
- laaa: ratio >= 4.5
+ laaa: ratio >= 4.5,
})
- const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})
+ const colorsConverted = Object.entries(colors).reduce(
+ (acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }),
+ {},
+ )
- const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
- const slotIsBaseText = key === 'text' || key === 'link'
- const slotIsText = slotIsBaseText || (
- typeof value === 'object' && value !== null && value.textColor
- )
- if (!slotIsText) return acc
- const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
- const background = variant || layer
- const opacitySlot = getOpacitySlot(background)
- const textColors = [
- key,
- ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
- ]
+ const ratios = Object.entries(SLOT_INHERITANCE).reduce(
+ (acc, [key, value]) => {
+ const slotIsBaseText = key === 'text' || key === 'link'
+ const slotIsText =
+ slotIsBaseText ||
+ (typeof value === 'object' && value !== null && value.textColor)
+ if (!slotIsText) return acc
+ const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
+ const background = variant || layer
+ const opacitySlot = getOpacitySlot(background)
+ const textColors = [
+ key,
+ ...(background === 'bg'
+ ? ['cRed', 'cGreen', 'cBlue', 'cOrange']
+ : []),
+ ]
- const layers = getLayers(
- layer,
- variant || layer,
- opacitySlot,
- colorsConverted,
- opacity
- )
+ const layers = getLayers(
+ layer,
+ variant || layer,
+ opacitySlot,
+ colorsConverted,
+ opacity,
+ )
- // Temporary patch for null-y value errors
- if (layers.flat().some(v => v == null)) return acc
+ // Temporary patch for null-y value errors
+ if (layers.flat().some((v) => v == null)) return acc
- return {
- ...acc,
- ...textColors.reduce((acc, textColorKey) => {
- const newKey = slotIsBaseText
- ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
- : textColorKey
- return {
- ...acc,
- [newKey]: getContrastRatioLayers(
- colorsConverted[textColorKey],
- layers,
- colorsConverted[textColorKey]
- )
- }
- }, {})
- }
+ return {
+ ...acc,
+ ...textColors.reduce((acc, textColorKey) => {
+ const newKey = slotIsBaseText
+ ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
+ : textColorKey
+ return {
+ ...acc,
+ [newKey]: getContrastRatioLayers(
+ colorsConverted[textColorKey],
+ layers,
+ colorsConverted[textColorKey],
+ ),
+ }
+ }, {}),
+ }
+ },
+ {},
+ )
+
+ return Object.entries(ratios).reduce((acc, [k, v]) => {
+ acc[k] = hints(v)
+ return acc
}, {})
-
- return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
} catch (e) {
console.warn('Failure computing contrasts', e)
return {}
}
},
- themeDataUsed () {
+ themeDataUsed() {
return useInterfaceStore().themeDataUsed
},
- shadowsAvailable () {
+ shadowsAvailable() {
return Object.keys(DEFAULT_SHADOWS).sort()
},
currentShadowOverriden: {
- get () {
+ get() {
return !!this.currentShadow
},
- set (val) {
+ set(val) {
if (val) {
- this.shadowsLocal[this.shadowSelected] = (this.currentShadowFallback || [])
- .map(s => ({
- name: null,
- x: 0,
- y: 0,
- blur: 0,
- spread: 0,
- inset: false,
- color: '#000000',
- alpha: 1,
- ...s
- }))
+ this.shadowsLocal[this.shadowSelected] = (
+ this.currentShadowFallback || []
+ ).map((s) => ({
+ name: null,
+ x: 0,
+ y: 0,
+ blur: 0,
+ spread: 0,
+ inset: false,
+ color: '#000000',
+ alpha: 1,
+ ...s,
+ }))
} else {
delete this.shadowsLocal[this.shadowSelected]
}
- }
+ },
},
- currentShadowFallback () {
+ currentShadowFallback() {
return (this.previewTheme.shadows || {})[this.shadowSelected]
},
currentShadow: {
- get () {
+ get() {
return this.shadowsLocal[this.shadowSelected]
},
- set (v) {
+ set(v) {
this.shadowsLocal[this.shadowSelected] = v
- }
+ },
},
- themeValid () {
+ themeValid() {
return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
},
- exportedTheme () {
- const saveEverything = (
+ exportedTheme() {
+ const saveEverything =
!this.keepFonts &&
!this.keepShadows &&
!this.keepOpacity &&
!this.keepRoundness &&
!this.keepColor
- )
const source = {
- themeEngineVersion: CURRENT_VERSION
+ themeEngineVersion: CURRENT_VERSION,
}
if (this.keepFonts || saveEverything) {
@@ -370,18 +389,20 @@ export default {
const theme = {
themeEngineVersion: CURRENT_VERSION,
- ...this.previewTheme
+ ...this.previewTheme,
}
return {
// To separate from other random JSON files and possible future source formats
- _pleroma_theme_version: 2, theme, source
+ _pleroma_theme_version: 2,
+ theme,
+ source,
}
},
- isActive () {
+ isActive() {
const tabSwitcher = this.$parent
return tabSwitcher ? tabSwitcher.isActive('theme') : false
- }
+ },
},
components: {
ColorInput,
@@ -393,70 +414,65 @@ export default {
TabSwitcher,
Preview,
Checkbox,
- Select
+ Select,
},
methods: {
- loadTheme (
- {
- theme,
- source,
- _pleroma_theme_version: fileVersion
- },
+ loadTheme(
+ { theme, source, _pleroma_theme_version: fileVersion },
origin,
- forceUseSource = false
+ forceUseSource = false,
) {
this.dismissWarning()
- const version = (origin === 'localStorage' && !theme.colors)
- ? 'l1'
- : fileVersion
+ const version =
+ origin === 'localStorage' && !theme.colors ? 'l1' : fileVersion
const snapshotEngineVersion = (theme || {}).themeEngineVersion
const themeEngineVersion = (source || {}).themeEngineVersion || 2
const versionsMatch = themeEngineVersion === CURRENT_VERSION
- const sourceSnapshotMismatch = (
+ const sourceSnapshotMismatch =
theme !== undefined &&
- source !== undefined &&
- themeEngineVersion !== snapshotEngineVersion
- )
+ source !== undefined &&
+ themeEngineVersion !== snapshotEngineVersion
// Force loading of source if user requested it or if snapshot
// is unavailable
const forcedSourceLoad = (source && forceUseSource) || !theme
- if (!(versionsMatch && !sourceSnapshotMismatch) &&
- !forcedSourceLoad &&
- version !== 'l1' &&
- origin !== 'defaults'
+ if (
+ !(versionsMatch && !sourceSnapshotMismatch) &&
+ !forcedSourceLoad &&
+ version !== 'l1' &&
+ origin !== 'defaults'
) {
if (sourceSnapshotMismatch && origin === 'localStorage') {
this.themeWarning = {
origin,
themeEngineVersion,
- type: 'snapshot_source_mismatch'
+ type: 'snapshot_source_mismatch',
}
} else if (!theme) {
this.themeWarning = {
origin,
noActionsPossible: true,
themeEngineVersion,
- type: 'no_snapshot_old_version'
+ type: 'no_snapshot_old_version',
}
} else if (!versionsMatch) {
this.themeWarning = {
origin,
noActionsPossible: !source,
themeEngineVersion,
- type: 'wrong_version'
+ type: 'wrong_version',
}
}
}
this.normalizeLocalState(theme, version, source, forcedSourceLoad)
},
- forceLoadLocalStorage () {
+ forceLoadLocalStorage() {
this.loadThemeFromLocalStorage(true)
},
- dismissWarning () {
+ dismissWarning() {
this.themeWarning = undefined
this.tempImportFile = undefined
},
- forceLoad () {
+ forceLoad() {
const { origin } = this.themeWarning
switch (origin) {
case 'localStorage':
@@ -468,7 +484,7 @@ export default {
}
this.dismissWarning()
},
- forceSnapshot () {
+ forceSnapshot() {
const { origin } = this.themeWarning
switch (origin) {
case 'localStorage':
@@ -480,104 +496,103 @@ export default {
}
this.dismissWarning()
},
- loadThemeFromLocalStorage (confirmLoadSource = false) {
+ loadThemeFromLocalStorage(confirmLoadSource = false) {
const theme = this.themeDataUsed?.source
if (theme) {
this.loadTheme(
{
- theme
+ theme,
},
'localStorage',
- confirmLoadSource
+ confirmLoadSource,
)
}
},
- setCustomTheme () {
- useInterfaceStore().setThemeV2({
- customTheme: {
- ignore: true,
- themeFileVersion: this.selectedVersion,
- themeEngineVersion: CURRENT_VERSION,
- ...this.previewTheme
- },
- customThemeSource: {
- themeFileVersion: this.selectedVersion,
- themeEngineVersion: CURRENT_VERSION,
- shadows: this.shadowsLocal,
- fonts: this.fontsLocal,
- opacity: this.currentOpacity,
- colors: this.currentColors,
- radii: this.currentRadii
- }
+ setCustomTheme() {
+ useInterfaceStore().setTheme({
+ themeFileVersion: this.selectedVersion,
+ themeEngineVersion: CURRENT_VERSION,
+ shadows: this.shadowsLocal,
+ fonts: this.fontsLocal,
+ opacity: this.currentOpacity,
+ colors: this.currentColors,
+ radii: this.currentRadii,
})
},
- updatePreviewColors () {
+ updatePreviewColors() {
const result = generateColors({
opacity: this.currentOpacity,
- colors: this.currentColors
+ colors: this.currentColors,
})
this.previewTheme.colors = result.theme.colors
this.previewTheme.opacity = result.theme.opacity
},
- updatePreviewShadows () {
+ updatePreviewShadows() {
this.previewTheme.shadows = generateShadows(
{
shadows: this.shadowsLocal,
opacity: this.previewTheme.opacity,
- themeEngineVersion: this.engineVersion
+ themeEngineVersion: this.engineVersion,
},
this.previewTheme.colors,
- relativeLuminance(this.previewTheme.colors.bg) < 0.5 ? 1 : -1
+ relativeLuminance(this.previewTheme.colors.bg) < 0.5 ? 1 : -1,
).theme.shadows
},
- importTheme () { this.themeImporter.importData() },
- exportTheme () { this.themeExporter.exportData() },
- onImport (parsed, forceSource = false) {
+ importTheme() {
+ this.themeImporter.importData()
+ },
+ exportTheme() {
+ this.themeExporter.exportData()
+ },
+ onImport(parsed, forceSource = false) {
this.tempImportFile = parsed
this.loadTheme(parsed, 'file', forceSource)
},
- onImportFailure () {
- useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' })
+ onImportFailure() {
+ useInterfaceStore().pushGlobalNotice({
+ messageKey: 'settings.invalid_theme_imported',
+ level: 'error',
+ })
},
- importValidator (parsed) {
+ importValidator(parsed) {
const version = parsed._pleroma_theme_version
return version >= 1 || version <= 2
},
- clearAll () {
+ clearAll() {
this.loadThemeFromLocalStorage()
},
// Clears all the extra stuff when loading V1 theme
- clearV1 () {
+ clearV1() {
Object.keys(this.$data)
- .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
- .filter(_ => !v1OnlyNames.includes(_))
- .forEach(key => {
+ .filter((_) => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
+ .filter((_) => !v1OnlyNames.includes(_))
+ .forEach((key) => {
this.$data[key] = undefined
})
},
- clearRoundness () {
+ clearRoundness() {
Object.keys(this.$data)
- .filter(_ => _.endsWith('RadiusLocal'))
- .forEach(key => {
+ .filter((_) => _.endsWith('RadiusLocal'))
+ .forEach((key) => {
this.$data[key] = undefined
})
},
- clearOpacity () {
+ clearOpacity() {
Object.keys(this.$data)
- .filter(_ => _.endsWith('OpacityLocal'))
- .forEach(key => {
+ .filter((_) => _.endsWith('OpacityLocal'))
+ .forEach((key) => {
this.$data[key] = undefined
})
},
- clearShadows () {
+ clearShadows() {
this.shadowsLocal = {}
},
- clearFonts () {
+ clearFonts() {
this.fontsLocal = {}
},
@@ -594,7 +609,7 @@ export default {
* @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
* this allows importing source anyway
*/
- normalizeLocalState (theme, version = 0, source, forceSource = false) {
+ normalizeLocalState(theme, version = 0, source, forceSource = false) {
let input
if (typeof source !== 'undefined') {
if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) {
@@ -618,11 +633,17 @@ export default {
if (version === 0) {
if (input.version) version = input.version
// Old v1 naming: fg is text, btn is foreground
- if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
+ if (
+ typeof colors.text === 'undefined' &&
+ typeof colors.fg !== 'undefined'
+ ) {
version = 1
}
// New v2 naming: text is text, fg is foreground
- if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
+ if (
+ typeof colors.text !== 'undefined' &&
+ typeof colors.fg !== 'undefined'
+ ) {
version = 2
}
}
@@ -648,7 +669,7 @@ export default {
.add('cOrange')
}
- keys.forEach(key => {
+ keys.forEach((key) => {
const color = colors[key]
const hex = rgb2hex(colors[key])
this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
@@ -689,33 +710,32 @@ export default {
this.fontsLocal = fonts
}
},
- updateTheme3Preview () {
+ updateTheme3Preview() {
const theme2 = convertTheme2To3(this.previewTheme)
const theme3 = init({
inputRuleset: theme2,
ultimateBackgroundColor: '#000000',
- liteMode: true
+ liteMode: true,
})
const sheet = createStyleSheet('theme-tab-overall-preview', 90)
- const rule = getScopedVersion(
- getCssRules(theme3.eager),
- '&'
- ).join('\n')
+ const rule = getScopedVersion(getCssRules(theme3.eager), '&').join('\n')
sheet.clear()
sheet.addRule('#theme-preview {\n' + rule + '\n}')
sheet.ready = true
adoptStyleSheets()
- }
+ },
},
watch: {
- themeDataUsed () {
+ themeDataUsed() {
this.loadThemeFromLocalStorage()
},
- currentRadii () {
+ currentRadii() {
try {
- this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii
+ this.previewTheme.radii = generateRadii({
+ radii: this.currentRadii,
+ }).theme.radii
this.radiiInvalid = false
} catch (e) {
this.radiiInvalid = true
@@ -723,7 +743,7 @@ export default {
}
},
shadowsLocal: {
- handler () {
+ handler() {
try {
this.updatePreviewShadows()
this.shadowsInvalid = false
@@ -732,21 +752,23 @@ export default {
console.warn(e)
}
},
- deep: true
+ deep: true,
},
fontsLocal: {
- handler () {
+ handler() {
try {
- this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme.fonts
+ this.previewTheme.fonts = generateFonts({
+ fonts: this.fontsLocal,
+ }).theme.fonts
this.fontsInvalid = false
} catch (e) {
this.fontsInvalid = true
console.warn(e)
}
},
- deep: true
+ deep: true,
},
- currentColors () {
+ currentColors() {
try {
this.updatePreviewColors()
this.colorsInvalid = false
@@ -755,23 +777,25 @@ export default {
console.warn(e)
}
},
- currentOpacity () {
+ currentOpacity() {
try {
this.updatePreviewColors()
} catch (e) {
console.warn(e)
}
},
- selected () {
- this.selectedTheme = Object.entries(this.availableStyles).find(([, s]) => {
- if (Array.isArray(s)) {
- return s[0] === this.selected
- } else {
- return s.name === this.selected
- }
- })[1]
+ selected() {
+ this.selectedTheme = Object.entries(this.availableStyles).find(
+ ([, s]) => {
+ if (Array.isArray(s)) {
+ return s[0] === this.selected
+ } else {
+ return s.name === this.selected
+ }
+ },
+ )[1]
},
- selectedTheme () {
+ selectedTheme() {
this.dismissWarning()
if (this.selectedVersion === 1) {
if (!this.keepRoundness) {
@@ -799,8 +823,12 @@ export default {
this.cOrangeColorLocal = this.selectedTheme[8]
}
} else if (this.selectedVersion >= 2) {
- this.normalizeLocalState(this.selectedTheme.theme, 2, this.selectedTheme.source)
+ this.normalizeLocalState(
+ this.selectedTheme.theme,
+ 2,
+ this.selectedTheme.source,
+ )
}
- }
- }
+ },
+ },
}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.scss
similarity index 99%
rename from src/components/settings_modal/tabs/theme_tab/theme_tab.scss
rename to src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.scss
index ae2364dcc..9ec6b7b2f 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
+++ b/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.scss
@@ -1,5 +1,6 @@
-.theme-tab {
+.old-theme-tab {
min-width: var(--themeEditorMinWidth, fit-content);
+ margin: 1em;
.deprecation-warning {
padding: 0.5em;
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.vue
similarity index 99%
rename from src/components/settings_modal/tabs/theme_tab/theme_tab.vue
rename to src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.vue
index ec3f0cb5d..751e113a0 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/old_theme_tab/old_theme_tab.vue
@@ -1,5 +1,5 @@
-
+
{{ $t("settings.style.themes2_outdated") }}
@@ -1020,6 +1020,6 @@
-
+
-
+
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_preview.vue b/src/components/settings_modal/tabs/old_theme_tab/theme_preview.vue
similarity index 98%
rename from src/components/settings_modal/tabs/theme_tab/theme_preview.vue
rename to src/components/settings_modal/tabs/old_theme_tab/theme_preview.vue
index df2d9f9e9..778e92c3b 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_preview.vue
+++ b/src/components/settings_modal/tabs/old_theme_tab/theme_preview.vue
@@ -119,25 +119,21 @@
diff --git a/src/components/settings_modal/tabs/posts_tab.js b/src/components/settings_modal/tabs/posts_tab.js
new file mode 100644
index 000000000..dbf284d93
--- /dev/null
+++ b/src/components/settings_modal/tabs/posts_tab.js
@@ -0,0 +1,78 @@
+import FontControl from 'src/components/font_control/font_control.vue'
+import BooleanSetting from '../helpers/boolean_setting.vue'
+import ChoiceSetting from '../helpers/choice_setting.vue'
+import IntegerSetting from '../helpers/integer_setting.vue'
+import SharedComputedObject from '../helpers/shared_computed_object.js'
+import UnitSetting from '../helpers/unit_setting.vue'
+
+import { useLocalConfigStore } from 'src/stores/local_config.js'
+import { useSyncConfigStore } from 'src/stores/sync_config.js'
+
+const PostsTab = {
+ data() {
+ return {
+ conversationDisplayOptions: ['tree', 'linear'].map((mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.conversation_display_${mode}`),
+ })),
+ conversationOtherRepliesButtonOptions: ['below', 'inside'].map(
+ (mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.conversation_other_replies_button_${mode}`),
+ }),
+ ),
+ mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(
+ (mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.mention_link_display_${mode}`),
+ }),
+ ),
+ userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map((mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.user_popover_avatar_action_${mode}`),
+ })),
+ unsavedPostActionOptions: ['save', 'discard', 'confirm'].map((mode) => ({
+ key: mode,
+ value: mode,
+ label: this.$t(`settings.unsaved_post_action_${mode}`),
+ })),
+ loopSilentAvailable:
+ // Firefox
+ Object.getOwnPropertyDescriptor(
+ HTMLVideoElement.prototype,
+ 'mozHasAudio',
+ ) ||
+ // Chrome-likes
+ Object.getOwnPropertyDescriptor(
+ HTMLMediaElement.prototype,
+ 'webkitAudioDecodedByteCount',
+ ) ||
+ // Future spec, still not supported in Nightly 63 as of 08/2018
+ Object.getOwnPropertyDescriptor(
+ HTMLMediaElement.prototype,
+ 'audioTracks',
+ ),
+ }
+ },
+ components: {
+ BooleanSetting,
+ ChoiceSetting,
+ IntegerSetting,
+ FontControl,
+ UnitSetting,
+ },
+ computed: {
+ ...SharedComputedObject(),
+ },
+ methods: {
+ updateFont(path, value) {
+ useLocalConfigStore().set({ path, value })
+ },
+ },
+}
+
+export default PostsTab
diff --git a/src/components/settings_modal/tabs/posts_tab.scss b/src/components/settings_modal/tabs/posts_tab.scss
new file mode 100644
index 000000000..84c3d0dd2
--- /dev/null
+++ b/src/components/settings_modal/tabs/posts_tab.scss
@@ -0,0 +1,5 @@
+.posts-tab {
+ .greentext {
+ color: var(--funtextGreentext);
+ }
+}
diff --git a/src/components/settings_modal/tabs/posts_tab.vue b/src/components/settings_modal/tabs/posts_tab.vue
new file mode 100644
index 000000000..fc33d5476
--- /dev/null
+++ b/src/components/settings_modal/tabs/posts_tab.vue
@@ -0,0 +1,268 @@
+
+
+
+
{{ $t('settings.posts_appearance') }}
+
+
+
+ {{ $t('settings.collapse_subject') }}
+
+
+
+
+ {{ $t('settings.conversation_display') }}
+
+
+
+
+ {{ $t('settings.tree_advanced') }}
+
+
+
+
+ {{ $t('settings.tree_fade_ancestors') }}
+
+
+
+
+ {{ $t('settings.max_depth_in_thread') }}
+
+
+
+
+ {{ $t('settings.conversation_other_replies_button') }}
+
+
+
+
+
+ updateFont('fontPosts', v)"
+ />
+
+
+ updateFont('fontMonospace', v)"
+ />
+
+
+
+
+
+ {{ $t('settings.greentext_quotes') }}
+
+
+
+
+
+
+ {{ $t('settings.emoji_reactions_on_timeline') }}
+
+
+
+
{{ $t('settings.mention_links') }}
+
+
+
+ {{ $t('settings.mention_link_display') }}
+
+
+
+
+ {{ $t('settings.mention_link_fade_domain') }}
+
+
+
+
+
+
+ {{ $t('settings.mention_link_use_tooltip') }}
+
+
+
+
+ {{ $t('settings.mention_link_show_avatar') }}
+
+
+
+
+ {{ $t('settings.mention_link_bolden_you') }}
+
+
+
+
+ {{ $t('settings.no_rich_text_description') }}
+
+
+
+
+ {{ $t('settings.absolute_time_format_min_age') }}
+
+
+
+
+
+
{{ $t('settings.attachments') }}
+
+
+
+ {{ $t('settings.stop_gifs') }}
+
+
+
+
+ {{ $t('settings.non_square_emoji') }}
+
+
+
+
+ {{ $t('settings.nsfw_clickthrough') }}
+
+
+
+
+ {{ $t('settings.preload_images') }}
+
+
+
+
+ {{ $t('settings.use_one_click_nsfw') }}
+
+
+
+
+
+
+ {{ $t('settings.loop_video') }}
+
+
+
+
+
+ {{ $t('settings.play_videos_in_modal') }}
+
+
+
+
+ {{ $t('settings.use_contain_fit') }}
+
+
+
+
+ {{ $t('settings.fun') }}
+
+
+
+
+ {{ $t('settings.show_yous') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 9e13eed69..3e12f265f 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -1,24 +1,19 @@
-import UserCard from 'src/components/user_card/user_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
+import UserCard from 'src/components/user_card/user_card.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
-import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
- faTimes,
+ faCircleNotch,
faPlus,
- faCircleNotch
+ faTimes,
} from '@fortawesome/free-solid-svg-icons'
-library.add(
- faTimes,
- faPlus,
- faCircleNotch
-)
+library.add(faTimes, faPlus, faCircleNotch)
const ProfileTab = {
- data () {
+ data() {
return {
// Whether user is locked or not
locked: this.$store.state.users.currentUser.locked,
@@ -28,18 +23,17 @@ const ProfileTab = {
UserCard,
Checkbox,
BooleanSetting,
- ProfileSettingIndicator
},
computed: {
- user () {
+ user() {
return this.$store.state.users.currentUser
},
- ...SharedComputedObject()
+ ...SharedComputedObject(),
},
methods: {
- updateProfile () {
+ updateProfile() {
const params = {
- locked: this.locked
+ locked: this.locked,
}
this.$store.state.api.backendInteractor
@@ -51,13 +45,13 @@ const ProfileTab = {
.catch((error) => {
this.displayUploadError(error)
})
- }
+ },
},
watch: {
- locked () {
+ locked() {
this.updateProfile()
- }
- }
+ },
+ },
}
export default ProfileTab
diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss
index ce85188bd..6e8b2b131 100644
--- a/src/components/settings_modal/tabs/profile_tab.scss
+++ b/src/components/settings_modal/tabs/profile_tab.scss
@@ -1,6 +1,6 @@
.profile-tab {
// overriding global for better look
- .setting-item.profile-edit {
+ .setting-section.profile-edit {
margin: 0;
h2 {
@@ -11,5 +11,28 @@
padding: 1.2em;
overflow: hidden;
}
+
+ }
+
+ .setting-item {
+ .custom-boolean-setting {
+ display: grid;
+ grid-template-columns: subgrid;
+
+ .label {
+ grid-area: label;
+ text-align: right;
+ }
+
+ .-mobile & {
+ .label {
+ text-align: left;
+ }
+ }
+
+ .checkbox-indicator {
+ grid-area: control;
+ }
+ }
}
}
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 947be4a18..2bdf1e8fe 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -1,21 +1,24 @@
-
-
{{ $t('settings.account_profile_edit') }}
+
-
-
{{ $t('settings.account_privacy') }}
+
+
{{ $t('settings.account_privacy') }}