Compare commits
No commits in common. "shigusegubu-themes3" and "shigusegubu" have entirely different histories.
shigusegub
...
shigusegub
704 changed files with 32414 additions and 81210 deletions
|
|
@ -1,12 +0,0 @@
|
|||
node_modules/
|
||||
dist/
|
||||
logs/
|
||||
.DS_Store
|
||||
.git/
|
||||
config/local.json
|
||||
pleroma-backend/
|
||||
test/e2e/reports/
|
||||
test/e2e-playwright/test-results/
|
||||
test/e2e-playwright/playwright-report/
|
||||
__screenshots__/
|
||||
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
name: 'Bug report'
|
||||
about: 'Bug report for Pleroma FE'
|
||||
labels:
|
||||
- Bug
|
||||
body:
|
||||
- type: input
|
||||
id: env-browser
|
||||
attributes:
|
||||
label: Browser and OS
|
||||
description: What browser are you using, including version, and what OS are you running?
|
||||
placeholder: Firefox 140, Arch Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-instance
|
||||
attributes:
|
||||
label: Instance URL
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: env-backend
|
||||
attributes:
|
||||
label: Backend version information
|
||||
description: Backend version being used. (See Settings->Show advanced->Developer)
|
||||
placeholder: Pleroma BE 2.10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-frontend
|
||||
attributes:
|
||||
label: Frontend version information
|
||||
description: Frontend version being used. (See Settings->Show advanced->Developer)
|
||||
placeholder: Pleroma FE 2.10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-extensions
|
||||
attributes:
|
||||
label: Browser extensions
|
||||
description: List of browser extensions you are using, like uBlock, rikaichamp etc. If none leave empty.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: env-modifications
|
||||
attributes:
|
||||
label: Known instance/user customizations
|
||||
description: Whether you are using a Pleroma FE fork, any mods mods or instance level styles among others.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: bug-text
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: A short description of the bug. Images can be helpful.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-reproducer
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Ordered list of reproduction steps needed to make the bug happen. If you don't have reproduction steps, leave this empty.
|
||||
placeholder: |
|
||||
1. Log in with a fresh browser session
|
||||
2. Open timeline X
|
||||
3. Click on button Y
|
||||
4. Z broke
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: bug-seriousness
|
||||
attributes:
|
||||
label: Bug seriousness
|
||||
value: |
|
||||
* How annoying it is:
|
||||
* How often does it happen:
|
||||
* How many people does it affect:
|
||||
* Is there a workaround for it:
|
||||
- type: checkboxes
|
||||
id: duplicate-issues
|
||||
attributes:
|
||||
label: Duplicate issues
|
||||
hide_label: true
|
||||
description: Before submitting this issue, search for same or similar issues on the [Pleroma FE bug tracker](https://git.pleroma.social/pleroma/pleroma-fe/issues).
|
||||
options:
|
||||
- label: I've searched for same or similar issues before submitting this issue.
|
||||
required: true
|
||||
visible: [form]
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
name: 'Feature request / Suggestion / Improvement'
|
||||
about: 'Feature requests, suggestions and improvements for Pleroma FE'
|
||||
labels:
|
||||
- Feature Request / Enhancement
|
||||
body:
|
||||
- type: textarea
|
||||
id: issue-text
|
||||
attributes:
|
||||
label: Proposal
|
||||
placeholder: Make groups happen!
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: duplicate-issues
|
||||
attributes:
|
||||
label: Duplicate issues
|
||||
hide_label: true
|
||||
description: Before submitting this issue, search for same or similar requests on the [Pleroma FE bug tracker](https://git.pleroma.social/pleroma/pleroma-fe/issues).
|
||||
options:
|
||||
- label: I've searched for same or similar requests before submitting this issue.
|
||||
required: true
|
||||
visible: [form]
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
### Checklist
|
||||
- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `<code>.<type>`.
|
||||
|
||||
<!--
|
||||
`<code>` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name.
|
||||
|
||||
`<type>` can be `add`, `change`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the MR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change.
|
||||
|
||||
In the file, write the changelog entry. For example, if an MR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it.
|
||||
|
||||
If one changelog entry is not enough, you may add more. But that might mean you can split it into two MRs. Only use more than one changelog entry if you really need to (for example, when one change in the code fix two different bugs, or when refactoring).
|
||||
-->
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -4,11 +4,8 @@ dist/
|
|||
npm-debug.log
|
||||
test/unit/coverage
|
||||
test/e2e/reports
|
||||
test/e2e-playwright/test-results
|
||||
test/e2e-playwright/playwright-report
|
||||
selenium-debug.log
|
||||
.idea/
|
||||
.gitlab-ci-local/
|
||||
config/local.json
|
||||
src/assets/emoji.json
|
||||
logs/
|
||||
|
|
|
|||
148
.gitlab-ci.yml
148
.gitlab-ci.yml
|
|
@ -1,7 +1,7 @@
|
|||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official framework image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/node/tags/
|
||||
image: node:20
|
||||
image: node:18
|
||||
|
||||
stages:
|
||||
- check-changelog
|
||||
|
|
@ -34,23 +34,12 @@ check-changelog:
|
|||
- apk add git
|
||||
- sh ./tools/check-changelog
|
||||
|
||||
lint-eslint:
|
||||
lint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-eslint
|
||||
|
||||
lint-biome:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-biome
|
||||
|
||||
lint-stylelint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-stylelint
|
||||
- yarn lint
|
||||
- yarn stylelint
|
||||
|
||||
test:
|
||||
stage: test
|
||||
|
|
@ -71,135 +60,6 @@ test:
|
|||
- test/**/__screenshots__
|
||||
when: on_failure
|
||||
|
||||
e2e-pleroma:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||
services:
|
||||
- name: postgres:15-alpine
|
||||
alias: db
|
||||
- name: $PLEROMA_IMAGE
|
||||
alias: pleroma
|
||||
entrypoint: ["/bin/ash", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -eu
|
||||
|
||||
SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded
|
||||
CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs
|
||||
|
||||
echo '-- Waiting for database...'
|
||||
while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
echo '-- Writing E2E config overrides...'
|
||||
cat > $CONFIG_OVERRIDE_PATH <<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 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
|
||||
tags:
|
||||
- amd64
|
||||
- himem
|
||||
variables:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
FF_NETWORK_PER_BUILD: "true"
|
||||
PLEROMA_IMAGE: git.pleroma.social/pleroma/pleroma:stable
|
||||
POSTGRES_USER: pleroma
|
||||
POSTGRES_PASSWORD: pleroma
|
||||
POSTGRES_DB: pleroma
|
||||
DB_USER: pleroma
|
||||
DB_PASS: pleroma
|
||||
DB_NAME: pleroma
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
DOMAIN: localhost
|
||||
INSTANCE_NAME: Pleroma E2E
|
||||
E2E_ADMIN_USERNAME: admin
|
||||
E2E_ADMIN_PASSWORD: adminadmin
|
||||
E2E_ADMIN_EMAIL: admin@example.com
|
||||
ADMIN_EMAIL: $E2E_ADMIN_EMAIL
|
||||
NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
|
||||
VITE_PROXY_TARGET: http://pleroma:4000
|
||||
VITE_PROXY_ORIGIN: http://localhost:4000
|
||||
E2E_BASE_URL: http://localhost:8099
|
||||
script:
|
||||
- npm install -g yarn@1.22.22
|
||||
- yarn --frozen-lockfile
|
||||
- |
|
||||
echo "-- Waiting for Pleroma API..."
|
||||
api_ok="false"
|
||||
for _i in $(seq 1 120); do
|
||||
if wget -qO- http://pleroma:4000/api/v1/instance >/dev/null 2>&1; then
|
||||
api_ok="true"
|
||||
break
|
||||
fi
|
||||
sleep 1s
|
||||
done
|
||||
if [ "$api_ok" != "true" ]; then
|
||||
echo "Timed out waiting for Pleroma API to become available"
|
||||
exit 1
|
||||
fi
|
||||
- yarn e2e:pw
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- test/e2e-playwright/test-results
|
||||
- test/e2e-playwright/playwright-report
|
||||
|
||||
build:
|
||||
stage: build
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
### Release checklist
|
||||
* [ ] Bump version in `package.json`
|
||||
* [ ] Compile a changelog with the `tools/collect-changelog` script
|
||||
* [ ] Create an MR with an announcement to pleroma.social
|
||||
#### post-merge
|
||||
* [ ] Tag the release on the merge commit
|
||||
* [ ] Make the tag into a Gitlab Release™
|
||||
* [ ] Merge `master` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs)
|
||||
|
|
@ -1 +1 @@
|
|||
20.19.0
|
||||
18.20.8
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@
|
|||
"custom-property-pattern": null,
|
||||
"keyframes-name-pattern": null,
|
||||
"scss/operator-no-newline-after": null,
|
||||
"declaration-property-value-no-unknown": true,
|
||||
"scss/declaration-property-value-no-unknown": true,
|
||||
"declaration-block-no-redundant-longhand-properties": [
|
||||
true,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
when:
|
||||
- event: pull_request
|
||||
evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")'
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
- event: manual
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
depends_on:
|
||||
- test
|
||||
- test-e2e
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
memory: 'high'
|
||||
|
||||
steps:
|
||||
build:
|
||||
image: docker.io/node:20-alpine
|
||||
commands:
|
||||
- apk add --no-cache zip git
|
||||
- yarn --frozen-lockfile
|
||||
- yarn build
|
||||
- if [ "${CI_PIPELINE_EVENT}" = "push" ] || [ "${CI_PIPELINE_EVENT}" = "manual" ]; then zip -9qr ${CI_REPO_DEFAULT_BRANCH}.zip dist/; fi
|
||||
|
||||
upload-artifacts:
|
||||
image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
|
||||
when:
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
- event: manual
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
settings:
|
||||
user:
|
||||
from_secret: pleroma-ci-user
|
||||
password:
|
||||
from_secret: pleroma-ci-password
|
||||
update: true
|
||||
owner: 'pleroma'
|
||||
package_name: 'pleroma-fe-builds'
|
||||
package_version: ${CI_REPO_DEFAULT_BRANCH}
|
||||
file_source: ./${CI_REPO_DEFAULT_BRANCH}.zip
|
||||
file_name: latest.zip
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
when:
|
||||
- event: pull_request
|
||||
evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")'
|
||||
|
||||
steps:
|
||||
check-changelog:
|
||||
image: docker.io/alpine:3.23
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- sh ./tools/check-changelog
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
when:
|
||||
- event: pull_request
|
||||
evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")'
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
- event: manual
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
steps:
|
||||
install-depends:
|
||||
image: &node-image
|
||||
docker.io/node:20-alpine
|
||||
commands:
|
||||
- yarn --frozen-lockfile
|
||||
|
||||
eslint:
|
||||
image: *node-image
|
||||
depends_on: install-depends
|
||||
commands:
|
||||
- yarn ci-eslint
|
||||
|
||||
biome:
|
||||
image: *node-image
|
||||
depends_on: install-depends
|
||||
commands:
|
||||
- yarn ci-biome
|
||||
|
||||
stylelint:
|
||||
image: *node-image
|
||||
depends_on: install-depends
|
||||
commands:
|
||||
- yarn ci-stylelint
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
when:
|
||||
- event: pull_request
|
||||
evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")'
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
- event: manual
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
memory: 'high'
|
||||
|
||||
variables:
|
||||
artifacts_uploader_settings: &artifacts_uploader_settings
|
||||
user:
|
||||
from_secret: pleroma-ci-user
|
||||
password:
|
||||
from_secret: pleroma-ci-password
|
||||
owner: 'pleroma'
|
||||
package_name: 'pleroma-fe-test-artifacts'
|
||||
script_file_entrypoint: &script_file_entrypoint
|
||||
- /bin/sh
|
||||
- -c
|
||||
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
|
||||
|
||||
steps:
|
||||
test:
|
||||
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||
entrypoint: *script_file_entrypoint
|
||||
environment:
|
||||
APT_CACHE_DIR: apt-cache
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
E2E_BASE_URL: http://localhost:8099
|
||||
FF_NETWORK_PER_BUILD: "true"
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
VITE_PROXY_ORIGIN: "http://pleroma:4000"
|
||||
VITE_PROXY_TARGET: "http://pleroma:4000"
|
||||
commands:
|
||||
- |
|
||||
if [ "${CI_PIPELINE_EVENT}" != "pull_request" ]; then
|
||||
mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
||||
apt-get install -y zip
|
||||
fi
|
||||
- npm install -g yarn@1.22.22
|
||||
- yarn --frozen-lockfile
|
||||
- |
|
||||
echo "-- Waiting for Pleroma API..."
|
||||
api_ok="false"
|
||||
for _i in $(seq 1 120); do
|
||||
if wget -qO- http://pleroma:4000/api/v1/instance >/dev/null 2>&1; then
|
||||
api_ok="true"
|
||||
break
|
||||
fi
|
||||
sleep 1s
|
||||
done
|
||||
if [ "$api_ok" != "true" ]; then
|
||||
echo "Timed out waiting for Pleroma API to become available"
|
||||
exit 1
|
||||
fi
|
||||
- |
|
||||
if ! yarn e2e:pw; then
|
||||
[ "${CI_PIPELINE_EVENT}" = "pull_request" ] || zip -9qr ${CI_COMMIT_SHA:0:8}-e2e.zip ./test/e2e-playwright/test-results ./test/e2e-playwright/playwright-report
|
||||
exit 1
|
||||
fi
|
||||
|
||||
upload-artifacts:
|
||||
image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
|
||||
when:
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
status: [failure]
|
||||
- event: manual
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
status: [failure]
|
||||
settings:
|
||||
<<: *artifacts_uploader_settings
|
||||
package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}
|
||||
file_source: ./${CI_COMMIT_SHA:0:8}-e2e.zip
|
||||
file_name: ${CI_COMMIT_SHA:0:8}-e2e.zip
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: docker.io/postgres:13-alpine
|
||||
environment:
|
||||
POSTGRES_DB: pleroma_test
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
pleroma:
|
||||
image: git.pleroma.social/pleroma/pleroma:stable-e2e
|
||||
environment:
|
||||
ADMIN_EMAIL: "admin@example.com"
|
||||
NOTIFY_EMAIL: "admin@example.com"
|
||||
DB_USER: postgres
|
||||
DB_PASS: postgres
|
||||
DB_NAME: pleroma_test
|
||||
DB_HOST: postgres
|
||||
INSTANCE_NAME: Pleroma E2E
|
||||
E2E_ADMIN_USERNAME: admin
|
||||
E2E_ADMIN_PASSWORD: adminadmin
|
||||
E2E_ADMIN_EMAIL: "admin@example.com"
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
when:
|
||||
- event: pull_request
|
||||
evaluate: 'CI_COMMIT_SOURCE_BRANCH != "weblate" && not(CI_COMMIT_SOURCE_BRANCH startsWith "renovate/")'
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
- event: manual
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
memory: 'high'
|
||||
|
||||
variables:
|
||||
artifacts_uploader_settings: &artifacts_uploader_settings
|
||||
user:
|
||||
from_secret: pleroma-ci-user
|
||||
password:
|
||||
from_secret: pleroma-ci-password
|
||||
owner: 'pleroma'
|
||||
package_name: 'pleroma-fe-test-artifacts'
|
||||
script_file_entrypoint: &script_file_entrypoint
|
||||
- /bin/sh
|
||||
- -c
|
||||
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
|
||||
|
||||
steps:
|
||||
test:
|
||||
image: mcr.microsoft.com/playwright:v1.61.0-jammy
|
||||
environment:
|
||||
APT_CACHE_DIR: apt-cache
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
FF_NETWORK_PER_BUILD: "true"
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
entrypoint: *script_file_entrypoint
|
||||
commands:
|
||||
- |
|
||||
if [ "${CI_PIPELINE_EVENT}" != "pull_request" ]; then
|
||||
mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
||||
apt-get -y install zip
|
||||
fi
|
||||
- yarn --frozen-lockfile
|
||||
- |
|
||||
if ! yarn unit-ci; then
|
||||
[ "${CI_PIPELINE_EVENT}" = "pull_request" ] || zip -9qr ${CI_COMMIT_SHA:0:8}-screenshots.zip $(find . -type d -name __screenshots__)
|
||||
exit 1
|
||||
fi
|
||||
|
||||
upload-artifacts:
|
||||
image: docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
|
||||
when:
|
||||
- event: push
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
status: [failure]
|
||||
- event: manual
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
status: [failure]
|
||||
settings:
|
||||
<<: *artifacts_uploader_settings
|
||||
package_version: ${CI_REPO_DEFAULT_BRANCH}-${CI_COMMIT_SHA:0:8}
|
||||
file_source: ./${CI_COMMIT_SHA:0:8}-screenshots.zip
|
||||
file_name: ${CI_COMMIT_SHA:0:8}-screenshots.zip
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
|
|
@ -2,76 +2,6 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## 2.10.1
|
||||
### Fixed
|
||||
- fixed being unable to set actor type from profile page
|
||||
- fixed error when clicking mute menu itself (instead of submenu items)
|
||||
- fixed mute -> domain status submenu not working
|
||||
|
||||
### Internal
|
||||
- Add playwright E2E-tests with an optional docker-based backend
|
||||
|
||||
## 2.10.0
|
||||
### Changed
|
||||
- Temporary changes modal now shows actual countdown instead of fixed timeout
|
||||
- Disabled elements are more disabled now
|
||||
- Rearranged and split settings to make more sense and be less of a wall of text
|
||||
- On mobile settings now take up full width and presented in navigation style
|
||||
improved styles for settings
|
||||
|
||||
### Added
|
||||
- Most of the remaining AdminFE tabs were added into Admin Dashboard
|
||||
- It's now possible to customize PWA Manfiest from PleromaFE
|
||||
- Make every configuration option default-overridable by instance admins
|
||||
|
||||
### Fixed
|
||||
- Fixed settings not appearing if user never touched "show advanced" toggle
|
||||
- Fix display of the broken/deleted/banned users
|
||||
- Fixed incorrect emoji display in post interaction lists
|
||||
- Fixed list title not being saved when editing
|
||||
- Fixed poll notifications not being expandable
|
||||
|
||||
|
||||
## 2.9.3
|
||||
### Fixed
|
||||
- Being unable to update profile
|
||||
|
||||
## 2.9.2
|
||||
### Changed
|
||||
- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible
|
||||
- User card/profile got an overhaul
|
||||
- Profile editing overhaul
|
||||
- Visually combined subject and content fields in post form
|
||||
- Moved post form's emoji button into input field
|
||||
- Minor visual changes and fixes
|
||||
- Clicking on fav/rt/emoji notifications' contents expands/collapses it
|
||||
- Reduced time taken processing theme by half
|
||||
- Splash screen only appears if loading takes more than 2 seconds
|
||||
|
||||
### Added
|
||||
- Mutes received an update, adding support for regex, muting based on username and expiration time.
|
||||
- Mutes are now synchronized across sessions
|
||||
- Support for expiring mutes and blocks (if available)
|
||||
- Clicking on emoji shows bigger version of it alongside with its shortcode
|
||||
- Admins also are able to copy it into a local pack
|
||||
- Added support for Akkoma and IceShrimp.NET backends
|
||||
- Compatibility with stricter CSP (Akkoma backend)
|
||||
- Added a way to upload new packs from a URL or ZIP file via the Admin Dashboard
|
||||
- Unify show/hide content buttons
|
||||
- Add support for detachable scrollTop button
|
||||
- Option to left-align user bio
|
||||
- Cache assets and emojis with service worker
|
||||
- Indicate currently active V3 theme as a body element class
|
||||
- Add arithmetic blend ISS function
|
||||
|
||||
### Fixed
|
||||
- Display counter for status action buttons when they are in the menu
|
||||
- Fix bookmark button alignment in the extra actions menu
|
||||
- Instance favicons are no longer stretched
|
||||
- A lot more scalable UI fixes
|
||||
- Emoji picker now should work fine when emoji size is increased
|
||||
|
||||
## 2.8.0
|
||||
### Changed
|
||||
- BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image
|
||||
|
|
@ -104,8 +34,8 @@ This does not guarantee that browsers will or will not work.
|
|||
- Support displaying time in absolute format
|
||||
- Add draft management system
|
||||
- Compress most kinds of images on upload.
|
||||
- Added option to always convert images to JPEG format instead of using WebP when compressing images.
|
||||
- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
|
||||
- Added option to always convert images to JPEG format instead of using WebP when compressing images.
|
||||
- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
|
||||
- Inform users that Smithereen public polls are public
|
||||
- Splash screen + loading indicator to make process of identifying initialization issues and load performance
|
||||
- UI for making v3 themes and palettes, support for bundling v3 themes
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
# For Translators
|
||||
|
||||
To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/languages.js).
|
||||
To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/languages.js).
|
||||
|
||||
Pleroma-FE will set your language by your browser locale, but you can change language in settings.
|
||||
|
||||
|
|
@ -32,10 +32,10 @@ yarn unit
|
|||
|
||||
# For Contributors:
|
||||
|
||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/src/config/local.example.json)) to enable some convenience dev options:
|
||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
|
||||
|
||||
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
|
||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/pleroma/frontend_configurations`. Only works in dev mode.
|
||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
|
||||
|
||||
FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE.
|
||||
|
||||
|
|
|
|||
150
biome.json
150
biome.json
|
|
@ -1,150 +0,0 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"includes": ["**", "!!**/dist", "!!tools/emojis.json"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"domains": {
|
||||
"vue": "recommended"
|
||||
},
|
||||
"rules": {
|
||||
"recommended": false,
|
||||
"complexity": {
|
||||
"noAdjacentSpacesInRegex": "error",
|
||||
"noExtraBooleanCast": "error",
|
||||
"noUselessCatch": "error",
|
||||
"noUselessEscapeInRegex": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noConstAssign": "error",
|
||||
"noConstantCondition": "error",
|
||||
"noEmptyCharacterClassInRegex": "error",
|
||||
"noEmptyPattern": "error",
|
||||
"noGlobalObjectCalls": "error",
|
||||
"noInvalidBuiltinInstantiation": "error",
|
||||
"noInvalidConstructorSuper": "error",
|
||||
"noNonoctalDecimalEscape": "error",
|
||||
"noPrecisionLoss": "error",
|
||||
"noSelfAssign": "error",
|
||||
"noSetterReturn": "error",
|
||||
"noSwitchDeclarations": "error",
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnreachable": "error",
|
||||
"noUnreachableSuper": "error",
|
||||
"noUnsafeFinally": "error",
|
||||
"noUnsafeOptionalChaining": "error",
|
||||
"noUnusedLabels": "error",
|
||||
"noUnusedPrivateClassMembers": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"noUnusedImports": "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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import chalk from 'chalk'
|
||||
import semver from 'semver'
|
||||
import chalk from 'chalk'
|
||||
|
||||
import packageConfig from '../package.json' with { type: 'json' }
|
||||
|
||||
|
|
@ -7,8 +7,8 @@ var versionRequirements = [
|
|||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node,
|
||||
},
|
||||
versionRequirement: packageConfig.engines.node
|
||||
}
|
||||
]
|
||||
|
||||
export default function () {
|
||||
|
|
@ -16,26 +16,20 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { cp } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import serveStatic from 'serve-static'
|
||||
import { resolve } from 'node:path'
|
||||
import { cp } from 'node:fs/promises'
|
||||
|
||||
const getPrefix = (s) => {
|
||||
const getPrefix = s => {
|
||||
const padEnd = s.endsWith('/') ? s : s + '/'
|
||||
return padEnd.startsWith('/') ? padEnd : '/' + padEnd
|
||||
}
|
||||
|
|
@ -13,31 +13,28 @@ const copyPlugin = ({ inUrl, inFs }) => {
|
|||
let copyTarget
|
||||
const handler = serveStatic(inFs)
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'copy-plugin-serve',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
server.middlewares.use(prefix, handler)
|
||||
},
|
||||
return [{
|
||||
name: 'copy-plugin-serve',
|
||||
apply: 'serve',
|
||||
configureServer (server) {
|
||||
server.middlewares.use(prefix, handler)
|
||||
}
|
||||
}, {
|
||||
name: 'copy-plugin-build',
|
||||
apply: 'build',
|
||||
configResolved (config) {
|
||||
copyTarget = resolve(config.root, config.build.outDir, subdir)
|
||||
},
|
||||
{
|
||||
name: 'copy-plugin-build',
|
||||
apply: 'build',
|
||||
configResolved(config) {
|
||||
copyTarget = resolve(config.root, config.build.outDir, subdir)
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler() {
|
||||
console.info(`Copying '${inFs}' to ${copyTarget}...`)
|
||||
await cp(inFs, copyTarget, { recursive: true })
|
||||
console.info('Done.')
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler () {
|
||||
console.log(`Copying '${inFs}' to ${copyTarget}...`)
|
||||
await cp(inFs, copyTarget, { recursive: true })
|
||||
console.log('Done.')
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
export default copyPlugin
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
import { access } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { languages } from '../src/i18n/languages.js'
|
||||
import { access } from 'node:fs/promises'
|
||||
import { languages, langCodeToCldrName } from '../src/i18n/languages.js'
|
||||
|
||||
const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/'
|
||||
const specialAnnotationsLocale = {
|
||||
ja_easy: 'ja',
|
||||
ja_easy: 'ja'
|
||||
}
|
||||
|
||||
const internalToAnnotationsLocale = (internal) =>
|
||||
specialAnnotationsLocale[internal] || internal
|
||||
const internalToAnnotationsLocale = (internal) => specialAnnotationsLocale[internal] || internal
|
||||
|
||||
// This gets all the annotations that are accessible (whose language
|
||||
// can be chosen in the settings). Data for other languages are
|
||||
// discarded because there is no way for it to be fetched.
|
||||
const getAllAccessibleAnnotations = async (projectRoot) => {
|
||||
const imports = (
|
||||
await Promise.all(
|
||||
languages.map(async (lang) => {
|
||||
const imports = (await Promise.all(
|
||||
languages
|
||||
.map(async lang => {
|
||||
const destLang = internalToAnnotationsLocale(lang)
|
||||
const importModule = `${annotationsImportPrefix}${destLang}.json`
|
||||
const importFile = resolve(projectRoot, 'node_modules', importModule)
|
||||
|
|
@ -25,18 +23,11 @@ const getAllAccessibleAnnotations = async (projectRoot) => {
|
|||
await access(importFile)
|
||||
return `'${lang}': () => import('${importModule}')`
|
||||
} catch (e) {
|
||||
if (e.message.match(/ENOENT/)) {
|
||||
console.warn(`Missing emoji annotations locale: ${destLang}`)
|
||||
} else {
|
||||
console.error('test', e.message)
|
||||
}
|
||||
return
|
||||
}
|
||||
}),
|
||||
)
|
||||
)
|
||||
.filter((k) => k)
|
||||
.join(',\n')
|
||||
})))
|
||||
.filter(k => k)
|
||||
.join(',\n')
|
||||
|
||||
return `
|
||||
export const annotationsLoader = {
|
||||
|
|
@ -52,21 +43,21 @@ const emojisPlugin = () => {
|
|||
let projectRoot
|
||||
return {
|
||||
name: 'emojis-plugin',
|
||||
configResolved(conf) {
|
||||
configResolved (conf) {
|
||||
projectRoot = conf.root
|
||||
},
|
||||
resolveId(id) {
|
||||
resolveId (id) {
|
||||
if (id === emojiAnnotationsId) {
|
||||
return emojiAnnotationsIdResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
async load(id) {
|
||||
async load (id) {
|
||||
if (id === emojiAnnotationsIdResolved) {
|
||||
return await getAllAccessibleAnnotations(projectRoot)
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const target = 'node_modules/msw/lib/mockServiceWorker.js'
|
||||
|
||||
|
|
@ -8,10 +8,10 @@ const mswPlugin = () => {
|
|||
return {
|
||||
name: 'msw-plugin',
|
||||
apply: 'serve',
|
||||
configResolved(conf) {
|
||||
configResolved (conf) {
|
||||
projectRoot = conf.root
|
||||
},
|
||||
configureServer(server) {
|
||||
configureServer (server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
if (req.path === '/mockServiceWorker.js') {
|
||||
const file = await readFile(resolve(projectRoot, target))
|
||||
|
|
@ -21,7 +21,7 @@ const mswPlugin = () => {
|
|||
next()
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { languages, langCodeToJsonName } from '../src/i18n/languages.js'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { langCodeToJsonName, languages } from '../src/i18n/languages.js'
|
||||
|
||||
const i18nDir = resolve(
|
||||
dirname(dirname(fileURLToPath(import.meta.url))),
|
||||
'src/i18n',
|
||||
'src/i18n'
|
||||
)
|
||||
|
||||
export const i18nFiles = languages.reduce((acc, lang) => {
|
||||
|
|
@ -17,15 +16,13 @@ export const i18nFiles = languages.reduce((acc, lang) => {
|
|||
}, {})
|
||||
|
||||
export const generateServiceWorkerMessages = async () => {
|
||||
const msgArray = await Promise.all(
|
||||
Object.entries(i18nFiles).map(async ([lang, file]) => {
|
||||
const fileContent = await readFile(file, 'utf-8')
|
||||
const msg = {
|
||||
notifications: JSON.parse(fileContent).notifications || {},
|
||||
}
|
||||
return [lang, msg]
|
||||
}),
|
||||
)
|
||||
const msgArray = await Promise.all(Object.entries(i18nFiles).map(async ([lang, file]) => {
|
||||
const fileContent = await readFile(file, 'utf-8')
|
||||
const msg = {
|
||||
notifications: JSON.parse(fileContent).notifications || {}
|
||||
}
|
||||
return [lang, msg]
|
||||
}))
|
||||
return msgArray.reduce((acc, [lang, msg]) => {
|
||||
acc[lang] = msg
|
||||
return acc
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { exactRegex } from '@rolldown/pluginutils'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { build } from 'vite'
|
||||
|
||||
import {
|
||||
generateServiceWorkerMessages,
|
||||
i18nFiles,
|
||||
} from './service_worker_messages.js'
|
||||
import * as esbuild from 'esbuild'
|
||||
import { generateServiceWorkerMessages, i18nFiles } from './service_worker_messages.js'
|
||||
|
||||
const getSWMessagesAsText = async () => {
|
||||
const messages = await generateServiceWorkerMessages()
|
||||
|
|
@ -14,24 +11,123 @@ 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 }) => {
|
||||
const swFullSrc = resolve(projectRoot, swSrc)
|
||||
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
||||
const swEnvNameResolved = '\0' + swEnvName
|
||||
|
||||
export const buildSwPlugin = ({
|
||||
swSrc,
|
||||
swDest,
|
||||
}) => {
|
||||
let config
|
||||
|
||||
return {
|
||||
name: 'build-sw-plugin',
|
||||
enforce: 'post',
|
||||
configResolved(resolvedConfig) {
|
||||
resolvedConfig
|
||||
apply: 'build',
|
||||
configResolved (resolvedConfig) {
|
||||
config = {
|
||||
define: resolvedConfig.define,
|
||||
resolve: resolvedConfig.resolve,
|
||||
|
|
@ -39,89 +135,53 @@ export const buildSwPlugin = ({ swSrc, swDest }) => {
|
|||
publicDir: false,
|
||||
build: {
|
||||
...resolvedConfig.build,
|
||||
emptyOutDir: false,
|
||||
rolldownOptions: {
|
||||
input: {
|
||||
main: swSrc,
|
||||
},
|
||||
context: 'self',
|
||||
output: {
|
||||
entryFileNames: swDest,
|
||||
codeSplitting: false,
|
||||
format: 'iife',
|
||||
},
|
||||
lib: {
|
||||
entry: swSrc,
|
||||
formats: ['iife'],
|
||||
name: 'sw_pleroma'
|
||||
},
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: swDest
|
||||
}
|
||||
}
|
||||
},
|
||||
configFile: false,
|
||||
configFile: false
|
||||
}
|
||||
},
|
||||
generateBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler(_, bundle) {
|
||||
async handler (_, bundle) {
|
||||
const assets = Object.keys(bundle)
|
||||
.filter((name) => !/\.map$/.test(name))
|
||||
.map((name) => '/' + name)
|
||||
|
||||
.filter(name => !/\.map$/.test(name))
|
||||
.map(name => '/' + name)
|
||||
config.plugins.push({
|
||||
name: 'build-sw-env-plugin',
|
||||
mode: 'production',
|
||||
resolveId: {
|
||||
filter: { id: exactRegex(swEnvName) },
|
||||
handler: () => swEnvNameResolved,
|
||||
},
|
||||
load: {
|
||||
filter: { id: exactRegex(swEnvNameResolved) },
|
||||
handler() {
|
||||
return `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||
},
|
||||
resolveId (id) {
|
||||
if (id === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
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: [] }',
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const swBundle = await build(config)
|
||||
return swBundle.output[0]
|
||||
} catch (e) {
|
||||
console.error('Error building ServiceWorker:', e)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler() {
|
||||
if (process.env.VITEST) return
|
||||
console.info('Building service worker for production')
|
||||
try {
|
||||
await build(config)
|
||||
} catch (e) {
|
||||
console.error('Error building ServiceWorker:', e)
|
||||
}
|
||||
},
|
||||
},
|
||||
async handler () {
|
||||
console.log('Building service worker for production')
|
||||
await build(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,9 +191,9 @@ const swMessagesNameResolved = '\0' + swMessagesName
|
|||
export const swMessagesPlugin = () => {
|
||||
return {
|
||||
name: 'sw-messages-plugin',
|
||||
resolveId(id) {
|
||||
resolveId (id) {
|
||||
if (id === swMessagesName) {
|
||||
Object.values(i18nFiles).forEach((f) => {
|
||||
Object.values(i18nFiles).forEach(f => {
|
||||
this.addWatchFile(f)
|
||||
})
|
||||
return swMessagesNameResolved
|
||||
|
|
@ -141,11 +201,11 @@ export const swMessagesPlugin = () => {
|
|||
return null
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
async load (id) {
|
||||
if (id === swMessagesNameResolved) {
|
||||
return await getSWMessagesAsText()
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with {
|
||||
type: 'json',
|
||||
}
|
||||
|
||||
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' }
|
||||
import fs from 'fs'
|
||||
|
||||
Object.keys(emojis).map((k) => {
|
||||
emojis[k].map((e) => {
|
||||
delete e.unicode_version
|
||||
delete e.emoji_version
|
||||
delete e.skin_tone_support_unicode_version
|
||||
Object.keys(emojis)
|
||||
.map(k => {
|
||||
emojis[k].map(e => {
|
||||
delete e.unicode_version
|
||||
delete e.emoji_version
|
||||
delete e.skin_tone_support_unicode_version
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const res = {}
|
||||
Object.keys(emojis).map((k) => {
|
||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||
res[groupId] = emojis[k]
|
||||
})
|
||||
Object.keys(emojis)
|
||||
.map(k => {
|
||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||
res[groupId] = emojis[k]
|
||||
})
|
||||
|
||||
console.info('Updating emojis...')
|
||||
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res))
|
||||
|
|
|
|||
1
changelog.d/action-button-extra-counter.add
Normal file
1
changelog.d/action-button-extra-counter.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Display counter for status action buttons when they are on the menu
|
||||
1
changelog.d/akkoma-sharkey-net-support.add
Normal file
1
changelog.d/akkoma-sharkey-net-support.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Added support for Akkoma and IceShrimp.NET backend
|
||||
2
changelog.d/arithmetic-blend.add
Normal file
2
changelog.d/arithmetic-blend.add
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Add arithmetic blend ISS function
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix HTML attribute parsing for escaped quotes
|
||||
1
changelog.d/better-scroll-button.add
Normal file
1
changelog.d/better-scroll-button.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Add support for detachable scrollTop button
|
||||
1
changelog.d/bookmark-button-align.fix
Normal file
1
changelog.d/bookmark-button-align.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix bookmark button alignment in the extra actions menu
|
||||
1
changelog.d/csp.add
Normal file
1
changelog.d/csp.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Compatibility with stricter CSP (Akkoma backend)
|
||||
|
|
@ -1 +0,0 @@
|
|||
Migrated to Vite 8 and optimized our imports, more stuff is loaded on-demand, reducing the initial load time and transfer size
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix emojis breaking user bio/description editing
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fixed status action mute hiding itself on click
|
||||
1
changelog.d/mutes-sync.add
Normal file
1
changelog.d/mutes-sync.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Synchronized mutes, advanced mute control (regexp, expiry, naming)
|
||||
1
changelog.d/profile-error.fix
Normal file
1
changelog.d/profile-error.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix error styling for user profiles
|
||||
|
|
@ -1 +0,0 @@
|
|||
displaying other user's backgrounds (if supported by BE)
|
||||
|
|
@ -1 +0,0 @@
|
|||
Add quoting by URL and in replies
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix reply form crash when quote-reply settings are unavailable
|
||||
1
changelog.d/sw-cache-assets.add
Normal file
1
changelog.d/sw-cache-assets.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Cache assets and emojis with service worker
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
settings synchronization
|
||||
user highlight synchronization
|
||||
1
changelog.d/theme3-body-class.add
Normal file
1
changelog.d/theme3-body-class.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Indicate currently active V3 theme as a body element class
|
||||
1
changelog.d/unify-show-hide-buttons.add
Normal file
1
changelog.d/unify-show-hide-buttons.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Unify show/hide content buttons
|
||||
|
|
@ -1 +0,0 @@
|
|||
User administration + post scope/sensitivity admin change support
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_USER: pleroma
|
||||
POSTGRES_PASSWORD: pleroma
|
||||
POSTGRES_DB: pleroma
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U pleroma -d pleroma"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 30
|
||||
|
||||
pleroma:
|
||||
image: ${PLEROMA_IMAGE:-git.pleroma.social/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
|
||||
ports:
|
||||
- 4000:4000
|
||||
|
||||
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:8099
|
||||
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
|
||||
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
|
||||
command: ["yarn", "e2e:pw"]
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
FROM mcr.microsoft.com/playwright:v1.61.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"]
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
#!/bin/ash
|
||||
|
||||
set -eu
|
||||
|
||||
SEED_SENTINEL_PATH="/var/lib/pleroma/.e2e_seeded"
|
||||
CONFIG_OVERRIDE_PATH="/var/lib/pleroma/config.exs"
|
||||
|
||||
echo "-- Waiting for database..."
|
||||
while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
echo "-- Writing E2E config overrides..."
|
||||
cat > "$CONFIG_OVERRIDE_PATH" <<'EOF'
|
||||
import Config
|
||||
|
||||
config :pleroma, Pleroma.Captcha,
|
||||
enabled: false
|
||||
|
||||
config :pleroma, :instance,
|
||||
registrations_open: true,
|
||||
account_activation_required: false,
|
||||
approval_required: false
|
||||
EOF
|
||||
|
||||
echo "-- Running migrations..."
|
||||
/opt/pleroma/bin/pleroma_ctl migrate
|
||||
|
||||
echo "-- Starting!"
|
||||
/opt/pleroma/bin/pleroma start &
|
||||
PLEROMA_PID="$!"
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${PLEROMA_PID:-}" ] && kill -0 "$PLEROMA_PID" 2>/dev/null; then
|
||||
kill -TERM "$PLEROMA_PID"
|
||||
wait "$PLEROMA_PID" || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup INT TERM
|
||||
|
||||
echo "-- Waiting for API..."
|
||||
api_ok="false"
|
||||
for _i in $(seq 1 120); do
|
||||
if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then
|
||||
api_ok="true"
|
||||
break
|
||||
fi
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
if [ "$api_ok" != "true" ]; then
|
||||
echo "Timed out waiting for Pleroma API to become available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$SEED_SENTINEL_PATH" ]; then
|
||||
if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then
|
||||
echo "-- Seeding admin user (${E2E_ADMIN_USERNAME})..."
|
||||
if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then
|
||||
echo "-- User already exists (or creation failed), ensuring admin + confirmed..."
|
||||
/opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed
|
||||
fi
|
||||
else
|
||||
echo "-- Skipping admin seeding (missing E2E_ADMIN_* env)"
|
||||
fi
|
||||
|
||||
touch "$SEED_SENTINEL_PATH"
|
||||
fi
|
||||
|
||||
wait "$PLEROMA_PID"
|
||||
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
||||
|
||||
1. `/api/pleroma/frontend_configurations` - this is generated by backend and includes FE/Client-specific data. PleromaFE uses the `pleroma_fe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
|
||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/src/public/static/config.json).
|
||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/src/src/stores/instance.js) )
|
||||
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
|
||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json).
|
||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) )
|
||||
|
||||
## Instance-defaults
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ server {
|
|||
|
||||
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](../backend/API/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
||||
|
||||
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/src/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||
|
||||
For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +1,37 @@
|
|||
import js from '@eslint/js'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import vue from 'eslint-plugin-vue'
|
||||
import globals from 'globals'
|
||||
import vue from "eslint-plugin-vue";
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
|
||||
export default defineConfig([
|
||||
|
||||
export default [
|
||||
...vue.configs['flat/recommended'],
|
||||
globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']),
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['src/**/*.vue'],
|
||||
plugins: { js },
|
||||
extends: ['js/recommended'],
|
||||
files: ["**/*.js", "**/*.mjs", "**/*.vue"],
|
||||
ignores: ["build/*.js", "config/*.js"],
|
||||
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: 'module',
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
parser: "@babel/eslint-parser",
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.vitest,
|
||||
...globals.chai,
|
||||
...globals.commonjs,
|
||||
...globals.serviceworker,
|
||||
},
|
||||
...globals.serviceworker
|
||||
}
|
||||
},
|
||||
|
||||
rules: {
|
||||
'arrow-parens': 0,
|
||||
'generator-star-spacing': 0,
|
||||
'no-debugger': 0,
|
||||
'vue/require-prop-types': 0,
|
||||
'vue/multi-word-component-names': 0,
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@
|
|||
<link rel="preload" href="/static/pleromatan_apology_fox_small.webp" as="image" />
|
||||
<!-- putting styles here to avoid having to wait for styles to load up -->
|
||||
<link rel="stylesheet" id="splashscreen" href="/static/splash.css" />
|
||||
<link rel="stylesheet" id="custom-styles-holder" type="text/css" href="/static/empty.css" />
|
||||
<link rel="stylesheet" id="pleroma-eager-styles" type="text/css" href="/static/empty.css" />
|
||||
<link rel="stylesheet" id="pleroma-lazy-styles" type="text/css" href="/static/empty.css" />
|
||||
<link rel="stylesheet" id="theme-holder" type="text/css" href="/static/empty.css" />
|
||||
<!--server-generated-meta-->
|
||||
</head>
|
||||
<body>
|
||||
<noscript>To use Pleroma, please enable JavaScript.</noscript>
|
||||
<div id="splash" class="initial-hidden">
|
||||
<div id="splash">
|
||||
<!-- we are hiding entire graphic so no point showing credit -->
|
||||
<div aria-hidden="true" id="splash-credit">
|
||||
Art by pipivovott
|
||||
|
|
|
|||
104
package.json
104
package.json
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "2.10.1",
|
||||
"version": "2.7.1",
|
||||
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/src/CONTRIBUTORS.md>",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "node build/update-emoji.js && vite dev",
|
||||
|
|
@ -10,117 +10,109 @@
|
|||
"unit": "node build/update-emoji.js && vitest --run",
|
||||
"unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
|
||||
"unit:watch": "node build/update-emoji.js && vitest",
|
||||
"e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs",
|
||||
"e2e": "sh ./tools/e2e/run.sh",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "yarn run unit && yarn run e2e",
|
||||
"ci-biome": "yarn exec biome check",
|
||||
"ci-eslint": "yarn exec eslint",
|
||||
"ci-stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
|
||||
"lint": "yarn ci-biome; yarn ci-eslint; yarn ci-stylelint",
|
||||
"lint-fix": "yarn exec eslint -- --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write"
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.4",
|
||||
"@babel/runtime": "7.27.1",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.1.0",
|
||||
"@fortawesome/vue-fontawesome": "3.1.2",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.8",
|
||||
"@floatingghost/pinch-zoom-element": "1.3.1",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
|
||||
"@vuelidate/core": "2.0.3",
|
||||
"@vuelidate/validators": "2.0.4",
|
||||
"@web3-storage/parse-link-header": "^3.1.0",
|
||||
"body-scroll-lock": "3.1.5",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "2.0.1",
|
||||
"cropperjs": "2.0.0",
|
||||
"escape-html": "1.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.8.1",
|
||||
"pinia": "^3.0.4",
|
||||
"phoenix": "1.7.21",
|
||||
"pinia": "^3.0.0",
|
||||
"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.22",
|
||||
"vue": "3.5.17",
|
||||
"vue-i18n": "11",
|
||||
"vue-router": "4.6.4",
|
||||
"vue-router": "4.5.1",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.7",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.5",
|
||||
"@babel/eslint-parser": "7.28.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@babel/register": "7.28.3",
|
||||
"@biomejs/biome": "2.3.11",
|
||||
"@pinia/testing": "1.0.3",
|
||||
"@babel/core": "7.27.1",
|
||||
"@babel/eslint-parser": "7.27.1",
|
||||
"@babel/plugin-transform-runtime": "7.27.1",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/register": "7.27.1",
|
||||
"@ungap/event-target": "0.2.4",
|
||||
"@vitejs/devtools": "^0.3.1",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"@vitest/browser-playwright": "^4.1.7",
|
||||
"@vitest/browser": "^4.1.7",
|
||||
"@vitest/ui": "^4.1.7",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@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.22",
|
||||
"@vue/babel-plugin-jsx": "1.4.0",
|
||||
"@vue/compiler-sfc": "3.5.17",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.21",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "5.3.3",
|
||||
"chalk": "5.6.2",
|
||||
"chai": "5.2.0",
|
||||
"chalk": "5.4.1",
|
||||
"chromedriver": "135.0.4",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "9.39.2",
|
||||
"eslint": "9.26.0",
|
||||
"vue-eslint-parser": "10.1.3",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-n": "17.23.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-n": "17.18.0",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"eslint-plugin-vue": "10.1.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "5.1.0",
|
||||
"function-bind": "1.1.2",
|
||||
"http-proxy-middleware": "3.0.5",
|
||||
"iso-639-1": "3.1.5",
|
||||
"lodash": "4.17.21",
|
||||
"msw": "2.14.6",
|
||||
"nightwatch": "3.12.2",
|
||||
"oxc": "^1.0.1",
|
||||
"playwright": "1.61.0",
|
||||
"postcss": "8.5.6",
|
||||
"msw": "2.10.2",
|
||||
"nightwatch": "3.12.1",
|
||||
"playwright": "1.52.0",
|
||||
"postcss": "8.5.3",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"sass-embedded": "^1.100.0",
|
||||
"sass": "1.89.2",
|
||||
"selenium-server": "3.141.59",
|
||||
"semver": "7.7.3",
|
||||
"semver": "7.7.2",
|
||||
"serve-static": "2.2.0",
|
||||
"shelljs": "0.10.0",
|
||||
"sinon": "20.0.0",
|
||||
"sinon-chai": "4.0.1",
|
||||
"stylelint": "16.25.0",
|
||||
"sinon-chai": "4.0.0",
|
||||
"stylelint": "16.19.1",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.6.0",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-eslint2": "^5.1.0",
|
||||
"vite-plugin-stylelint": "^6.1.0",
|
||||
"vitest": "^4.1.7",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-eslint2": "^5.0.3",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
"vitest": "^3.0.7"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import autoprefixer from 'autoprefixer'
|
||||
|
||||
export default {
|
||||
plugins: [autoprefixer],
|
||||
plugins: [
|
||||
autoprefixer
|
||||
]
|
||||
}
|
||||
|
|
|
|||
1
public/static/empty.css
Normal file
1
public/static/empty.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
// nothing here //
|
||||
|
|
@ -1,26 +1,6 @@
|
|||
{
|
||||
"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",
|
||||
|
|
@ -32,28 +12,8 @@
|
|||
"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",
|
||||
|
|
@ -76,28 +36,8 @@
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -5,14 +5,15 @@ body {
|
|||
|
||||
#splash {
|
||||
--scale: 1;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto;
|
||||
align-content: center;
|
||||
place-items: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
flex-direction: column;
|
||||
background: #0f161e;
|
||||
font-family: sans-serif;
|
||||
|
|
@ -20,20 +21,13 @@ body {
|
|||
position: absolute;
|
||||
z-index: 9999;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
opacity: 1;
|
||||
transition: opacity 500ms ease-out 2s;
|
||||
}
|
||||
|
||||
#splash.hidden,
|
||||
#splash.initial-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#splash-credit {
|
||||
position: absolute;
|
||||
font-size: 1em;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
font-size: 14px;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
|
|
@ -65,17 +59,16 @@ 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 {
|
||||
|
|
@ -85,7 +78,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 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"]
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
287
src/App.js
287
src/App.js
|
|
@ -1,267 +1,187 @@
|
|||
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 GlobalError from 'src/components/global_error/global_error.vue'
|
||||
import GlobalNoticeList from 'src/components/global_notice_list/global_notice_list.vue'
|
||||
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import MobileNav from 'src/components/mobile_nav/mobile_nav.vue'
|
||||
import MobilePostStatusButton from 'src/components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
import UserPanel from 'src/components/user_panel/user_panel.vue'
|
||||
import 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 { windowHeight, windowWidth } from './services/window_utils/window_utils'
|
||||
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 { useEmojiStore } from 'src/stores/emoji.js'
|
||||
import { useI18nStore } from 'src/stores/i18n.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useShoutStore } from 'src/stores/shout.js'
|
||||
|
||||
// Helper to unwrap reactive proxies
|
||||
window.toValue = (x) => JSON.parse(JSON.stringify(x))
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(
|
||||
() => import('src/components/notifications/notifications.vue'),
|
||||
),
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel: defineAsyncComponent(
|
||||
() =>
|
||||
import('src/components/who_to_follow_panel/who_to_follow_panel.vue'),
|
||||
),
|
||||
ShoutPanel: defineAsyncComponent(
|
||||
() => import('src/components/shout_panel/shout_panel.vue'),
|
||||
),
|
||||
MediaModal: defineAsyncComponent(
|
||||
() => import('src/components/media_modal/media_modal.vue'),
|
||||
),
|
||||
WhoToFollowPanel,
|
||||
ShoutPanel,
|
||||
MediaModal,
|
||||
SideDrawer,
|
||||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal: defineAsyncComponent(
|
||||
() => import('src/components/settings_modal/settings_modal.vue'),
|
||||
),
|
||||
UpdateNotification: defineAsyncComponent(
|
||||
() =>
|
||||
import('src/components/update_notification/update_notification.vue'),
|
||||
),
|
||||
PostStatusModal: defineAsyncComponent(
|
||||
() => import('src/components/post_status_modal/post_status_modal.vue'),
|
||||
),
|
||||
UserReportingModal: defineAsyncComponent(
|
||||
() =>
|
||||
import('src/components/user_reporting_modal/user_reporting_modal.vue'),
|
||||
),
|
||||
EditStatusModal: defineAsyncComponent(
|
||||
() => import('src/components/edit_status_modal/edit_status_modal.vue'),
|
||||
),
|
||||
StatusHistoryModal: defineAsyncComponent(
|
||||
() =>
|
||||
import('src/components/status_history_modal/status_history_modal.vue'),
|
||||
),
|
||||
GlobalError,
|
||||
GlobalNoticeList,
|
||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
StatusHistoryModal,
|
||||
GlobalNoticeList
|
||||
},
|
||||
data: () => ({
|
||||
mobileActivePanel: 'timeline',
|
||||
mobileActivePanel: 'timeline'
|
||||
}),
|
||||
provide() {
|
||||
return {
|
||||
allowNonSquareEmoji: useMergedConfigStore().mergedConfig.nonSquareEmoji,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
themeApplied() {
|
||||
themeApplied () {
|
||||
this.removeSplash()
|
||||
},
|
||||
currentTheme() {
|
||||
currentTheme () {
|
||||
this.setThemeBodyClass()
|
||||
},
|
||||
layoutType() {
|
||||
layoutType () {
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
created () {
|
||||
// Load the locale from the storage
|
||||
const value = useMergedConfigStore().mergedConfig.interfaceLanguage
|
||||
useI18nStore().setLanguage(value)
|
||||
useEmojiStore().loadUnicodeEmojiData(value)
|
||||
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
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 (this.themeApplied) {
|
||||
if (useInterfaceStore().themeApplied) {
|
||||
this.setThemeBodyClass()
|
||||
this.removeSplash()
|
||||
}
|
||||
getOrCreateServiceWorker()
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
|
||||
},
|
||||
computed: {
|
||||
currentTheme() {
|
||||
if (this.styleDataUsed) {
|
||||
const styleMeta = this.styleDataUsed.find(
|
||||
(x) => x.component === '@meta',
|
||||
)
|
||||
themeApplied () {
|
||||
return useInterfaceStore().themeApplied
|
||||
},
|
||||
currentTheme () {
|
||||
if (useInterfaceStore().styleDataUsed) {
|
||||
const styleMeta = useInterfaceStore().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 } = useMergedConfigStore().mergedConfig
|
||||
navClasses () {
|
||||
const { navbarColumnStretch } = this.$store.getters.mergedConfig
|
||||
return [
|
||||
'-' + this.layoutType,
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : []),
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : [])
|
||||
]
|
||||
},
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
userBackground() {
|
||||
return this.currentUser.background_image
|
||||
},
|
||||
foreignProfileBackground() {
|
||||
return (
|
||||
useMergedConfigStore().mergedConfig.allowForeignUserBackground &&
|
||||
useInterfaceStore().foreignProfileBackground
|
||||
)
|
||||
},
|
||||
instanceBackground() {
|
||||
return useMergedConfigStore().mergedConfig.hideInstanceWallpaper
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
return this.mergedConfig.hideInstanceWallpaper
|
||||
? null
|
||||
: this.instanceBackgroundUrl
|
||||
: this.$store.state.instance.background
|
||||
},
|
||||
background() {
|
||||
return (
|
||||
this.foreignProfileBackground ||
|
||||
this.userBackground ||
|
||||
this.instanceBackground
|
||||
)
|
||||
},
|
||||
bgStyle() {
|
||||
background () { return this.userBackground || this.instanceBackground },
|
||||
bgStyle () {
|
||||
if (this.background) {
|
||||
return {
|
||||
'--body-background-image': `url(${this.background})`,
|
||||
'--body-background-image': `url(${this.background})`
|
||||
}
|
||||
}
|
||||
},
|
||||
shoutJoined() {
|
||||
return useShoutStore().joined
|
||||
shout () { return 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
|
||||
},
|
||||
isChats() {
|
||||
isChats () {
|
||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||
},
|
||||
isListEdit() {
|
||||
isListEdit () {
|
||||
return this.$route.name === 'lists-edit'
|
||||
},
|
||||
newPostButtonShown() {
|
||||
newPostButtonShown () {
|
||||
if (this.isChats) return false
|
||||
if (this.isListEdit) return false
|
||||
return (
|
||||
useMergedConfigStore().mergedConfig.alwaysShowNewPostButton ||
|
||||
this.layoutType === 'mobile'
|
||||
)
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
},
|
||||
shoutboxPosition() {
|
||||
return (
|
||||
useMergedConfigStore().mergedConfig.alwaysShowNewPostButton || false
|
||||
)
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
shoutboxPosition () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
||||
},
|
||||
hideShoutbox() {
|
||||
return useMergedConfigStore().mergedConfig.hideShoutbox
|
||||
hideShoutbox () {
|
||||
return this.$store.getters.mergedConfig.hideShoutbox
|
||||
},
|
||||
reverseLayout() {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } =
|
||||
useMergedConfigStore().mergedConfig
|
||||
layoutType () { return useInterfaceStore().layoutType },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
reverseLayout () {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
||||
if (this.layoutType !== 'wide') {
|
||||
return reverseSetting
|
||||
} else {
|
||||
return thirdColumnMode === 'notifications'
|
||||
? reverseSetting
|
||||
: !reverseSetting
|
||||
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
||||
}
|
||||
},
|
||||
noSticky() {
|
||||
return useMergedConfigStore().mergedConfig.disableStickyHeaders
|
||||
},
|
||||
showScrollbars() {
|
||||
return useMergedConfigStore().mergedConfig.showScrollbars
|
||||
},
|
||||
scrollParent() {
|
||||
return window /* this.$refs.appContentRef */
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.instanceSpecificPanelPresent &&
|
||||
!useMergedConfigStore().mergedConfig.hideISP
|
||||
)
|
||||
},
|
||||
...mapState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapState(useInterfaceStore, [
|
||||
'themeApplied',
|
||||
'styleDataUsed',
|
||||
'layoutType',
|
||||
]),
|
||||
...mapState(useInstanceStore, ['styleDataUsed']),
|
||||
...mapState(useInstanceCapabilitiesStore, [
|
||||
'suggestionsEnabled',
|
||||
'editingAvailable',
|
||||
]),
|
||||
...mapState(useInstanceStore, {
|
||||
instanceBackgroundUrl: (store) => store.instanceIdentity.background,
|
||||
showFeaturesPanel: (store) => store.instanceIdentity.showFeaturesPanel,
|
||||
instanceSpecificPanelPresent: (store) =>
|
||||
store.instanceIdentity.showInstanceSpecificPanel &&
|
||||
store.instanceIdentity.instanceSpecificPanelContent,
|
||||
}),
|
||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||
scrollParent () { return window; /* this.$refs.appContentRef */ },
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
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'])
|
||||
|
|
@ -269,10 +189,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()}`
|
||||
|
|
@ -288,19 +208,14 @@ 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()
|
||||
})
|
||||
setTimeout(() => {
|
||||
splashscreenRoot.remove() // forcibly remove it, should fix my plasma browser widget t. HJ
|
||||
}, 600)
|
||||
splashscreenRoot.classList.add('hidden')
|
||||
document.querySelector('#app').classList.remove('hidden')
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
183
src/App.scss
183
src/App.scss
|
|
@ -3,7 +3,7 @@
|
|||
@use "panel";
|
||||
|
||||
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
|
||||
:root {
|
||||
--status-margin: 0.75em;
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
|
||||
html {
|
||||
font-size: var(--textSize, 1rem);
|
||||
font-size: var(--textSize, 14px);
|
||||
|
||||
--navbar-height: var(--navbarSize, 3.5rem);
|
||||
--emoji-size: var(--emojiSize, 32px);
|
||||
|
|
@ -50,7 +50,7 @@ body {
|
|||
// have a cursor/pointer to operate them
|
||||
@media (any-pointer: fine) {
|
||||
* {
|
||||
scrollbar-color: var(--icon) transparent;
|
||||
scrollbar-color: var(--fg) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
|
|
@ -130,7 +130,7 @@ body {
|
|||
}
|
||||
// Body should have background to scrollbar otherwise it will use white (body color?)
|
||||
html {
|
||||
scrollbar-color: var(--icon) var(--wallpaper);
|
||||
scrollbar-color: var(--fg) var(--wallpaper);
|
||||
background: var(--wallpaper);
|
||||
}
|
||||
}
|
||||
|
|
@ -144,25 +144,6 @@ h4 {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--fg);
|
||||
border-radius: var(--roundness);
|
||||
padding: 0 0.2em;
|
||||
|
||||
& pre,
|
||||
pre & {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
&.pre {
|
||||
white-space: pre;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.iconLetter {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
|
@ -219,7 +200,6 @@ nav {
|
|||
background-color: var(--wallpaper);
|
||||
background-image: var(--body-background-image);
|
||||
background-position: 50%;
|
||||
transition: background-image 1s;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
|
|
@ -402,10 +382,6 @@ nav {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
|
||||
&.-transparent {
|
||||
backdrop-filter: blur(0.125em) contrast(60%);
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -430,14 +406,6 @@ nav {
|
|||
button:not(.button-default) {
|
||||
color: var(--text);
|
||||
font-size: 100%;
|
||||
text-align: initial;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: inline;
|
||||
font-family: inherit;
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
|
|
@ -445,6 +413,45 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
.list-item {
|
||||
border-color: var(--border);
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
border-top-width: 1px;
|
||||
|
||||
&.-active,
|
||||
&:hover {
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
&.-active + &,
|
||||
&:hover + & {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&:hover + .menu-item-collapsible:not(.-expanded) + &,
|
||||
&.-active + .menu-item-collapsible:not(.-expanded) + & {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: var(--roundness);
|
||||
border-top-left-radius: var(--roundness);
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-right-radius: var(--roundness);
|
||||
border-bottom-left-radius: var(--roundness);
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item,
|
||||
.list-item {
|
||||
display: block;
|
||||
|
|
@ -463,6 +470,22 @@ nav {
|
|||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__vertical-gap: 0.5em;
|
||||
|
||||
&.-non-interactive {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
a,
|
||||
button:not(.button-default) {
|
||||
text-align: initial;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: inline;
|
||||
font-family: inherit;
|
||||
line-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.button-unstyled {
|
||||
|
|
@ -486,12 +509,6 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
label {
|
||||
&.-disabled {
|
||||
color: var(--textFaint);
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border: none;
|
||||
|
|
@ -508,10 +525,6 @@ textarea {
|
|||
height: unset;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--textFaint)
|
||||
}
|
||||
|
||||
--_padding: 0.5em;
|
||||
|
||||
border: none;
|
||||
|
|
@ -532,10 +545,6 @@ textarea {
|
|||
&[disabled="disabled"],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--textFaint);
|
||||
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&[type="range"] {
|
||||
|
|
@ -561,8 +570,6 @@ textarea {
|
|||
& + label::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
+ label::before {
|
||||
|
|
@ -662,8 +669,7 @@ option {
|
|||
list-style: none;
|
||||
display: grid;
|
||||
grid-auto-flow: row dense;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20em, 1fr));
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
li {
|
||||
border: 1px solid var(--border);
|
||||
|
|
@ -673,6 +679,11 @@ option {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
|
@ -684,6 +695,7 @@ option {
|
|||
--_roundness-right: 0;
|
||||
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
> *:first-child,
|
||||
|
|
@ -730,15 +742,17 @@ option {
|
|||
}
|
||||
|
||||
&.-dot {
|
||||
min-height: 0.6em;
|
||||
max-height: 0.6em;
|
||||
min-width: 0.6em;
|
||||
max-width: 0.6em;
|
||||
left: calc(50% + 0.5em);
|
||||
top: calc(50% - 1em);
|
||||
line-height: 0;
|
||||
min-height: 8px;
|
||||
max-height: 8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 0;
|
||||
font-size: 0;
|
||||
left: calc(50% - 4px);
|
||||
top: calc(50% - 4px);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
&.-counter {
|
||||
|
|
@ -760,19 +774,6 @@ option {
|
|||
padding: 0 0.25em;
|
||||
border-radius: var(--roundness);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
&.-dismissible {
|
||||
display: flex;
|
||||
padding-left: 0.5em;
|
||||
margin: 0;
|
||||
align-items: baseline;
|
||||
line-height: 2;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.faint {
|
||||
|
|
@ -782,20 +783,21 @@ option {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.notice-dismissible {
|
||||
display: flex;
|
||||
padding: 0.75em 1em;
|
||||
align-items: baseline;
|
||||
line-height: 1.5;
|
||||
.visibility-notice {
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--textFaint);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
p,
|
||||
span {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
margin: 0;
|
||||
}
|
||||
.notice-dismissible {
|
||||
padding-right: 4rem;
|
||||
position: relative;
|
||||
|
||||
.dismiss {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.5em;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
|
@ -934,7 +936,12 @@ option {
|
|||
|
||||
#splash {
|
||||
pointer-events: none;
|
||||
// transition: opacity 0.5s;
|
||||
transition: opacity 0.5s;
|
||||
opacity: 1;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#status {
|
||||
&.css-ok {
|
||||
|
|
@ -1073,7 +1080,7 @@ option {
|
|||
scale: 1.0063 0.9938;
|
||||
translate: 0 -10%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-timing-function: ease-in-ou;
|
||||
}
|
||||
|
||||
90% {
|
||||
|
|
|
|||
13
src/App.vue
13
src/App.vue
|
|
@ -28,10 +28,10 @@
|
|||
>
|
||||
<user-panel />
|
||||
<template v-if="layoutType !== 'mobile'">
|
||||
<NavPanel />
|
||||
<InstanceSpecificPanel v-if="showInstanceSpecificPanel" />
|
||||
<FeaturesPanel v-if="!currentUser && showFeaturesPanel" />
|
||||
<WhoToFollowPanel v-if="currentUser && suggestionsEnabled" />
|
||||
<nav-panel />
|
||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||
<features-panel v-if="!currentUser && showFeaturesPanel" />
|
||||
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
|
||||
<div id="notifs-sidebar" />
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -60,8 +60,8 @@
|
|||
/>
|
||||
</div>
|
||||
<MediaModal />
|
||||
<ShoutPanel
|
||||
v-if="currentUser && !hideShoutbox && shoutJoined"
|
||||
<shout-panel
|
||||
v-if="currentUser && shout && !hideShoutbox"
|
||||
:floating="true"
|
||||
class="floating-shout mobile-hidden"
|
||||
:class="{ '-left': shoutboxPosition }"
|
||||
|
|
@ -73,7 +73,6 @@
|
|||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal :class="layoutModalClass" />
|
||||
<UpdateNotification />
|
||||
<GlobalError />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
491
src/api/admin.js
491
src/api/admin.js
|
|
@ -1,491 +0,0 @@
|
|||
import { promisedRequest } from './helpers.js'
|
||||
|
||||
const REPORTS = '/api/v1/pleroma/admin/reports'
|
||||
const CONFIG_URL = '/api/v1/pleroma/admin/config'
|
||||
const DESCRIPTIONS_URL = '/api/v1/pleroma/admin/config/descriptions'
|
||||
|
||||
const ANNOUNCEMENTS_URL = (id = '') =>
|
||||
`/api/v1/pleroma/admin/announcements/${id}`
|
||||
|
||||
const FRONTENDS_URL = '/api/v1/pleroma/admin/frontends'
|
||||
const FRONTENDS_INSTALL_URL = '/api/v1/pleroma/admin/frontends/install'
|
||||
|
||||
const USERS_URL = (nickname = '') => `/api/v1/pleroma/admin/users/${nickname}`
|
||||
const USERS_URL_LIST = ({
|
||||
page,
|
||||
pageSize,
|
||||
filters = {},
|
||||
query = '',
|
||||
name = '',
|
||||
email = '',
|
||||
}) => {
|
||||
const {
|
||||
local = false,
|
||||
external = false,
|
||||
active = false,
|
||||
needApproval = false,
|
||||
unconfirmed = false,
|
||||
deactivated = false,
|
||||
isAdmin = true,
|
||||
isModerator = true,
|
||||
} = filters
|
||||
const filters_str = [
|
||||
local && 'local',
|
||||
external && 'external',
|
||||
active && 'active',
|
||||
needApproval && 'need_approval',
|
||||
unconfirmed && 'unconfirmed',
|
||||
deactivated && 'deactivated',
|
||||
isAdmin && 'is_admin',
|
||||
isModerator && 'is_moderator',
|
||||
]
|
||||
.filter((x) => x)
|
||||
.join(',')
|
||||
return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}`
|
||||
}
|
||||
|
||||
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
|
||||
|
||||
const PERMISSION_GROUP_URL = (right) =>
|
||||
`/api/pleroma/admin/users/permission_group/${right}`
|
||||
const ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate'
|
||||
const DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate'
|
||||
const SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest'
|
||||
const UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest'
|
||||
const APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve'
|
||||
const CONFIRM_USERS_URL = '/api/v1/pleroma/admin/users/confirm_email'
|
||||
const RESEND_CONFIRMATION_EMAIL_URL =
|
||||
'/api/v1/pleroma/admin/users/resend_confirmation_email'
|
||||
const LIST_STATUSES_URL = ({ id, page, pageSize, godmode, withReblogs }) =>
|
||||
`/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&page=${page}&godmode=${godmode}&with_reblogs=${withReblogs}`
|
||||
const CHANGE_STATUS_SCOPE_URL = (id) => `/api/v1/pleroma/admin/statuses/${id}`
|
||||
const REQUIRE_PASSWORD_CHANGE_URL =
|
||||
'/api/v1/pleroma/admin/users/force_password_reset'
|
||||
|
||||
const DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa'
|
||||
const EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
|
||||
const EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
|
||||
const EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}`
|
||||
const EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download'
|
||||
const EMOJI_PACKS_DL_REMOTE_ZIP_URL = '/api/v1/pleroma/emoji/packs/download_zip'
|
||||
const EMOJI_PACKS_LS_REMOTE_URL = (url, page, pageSize) =>
|
||||
`/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}`
|
||||
const EMOJI_UPDATE_FILE_URL = (name) =>
|
||||
`/api/v1/pleroma/emoji/packs/files?name=${name}`
|
||||
|
||||
//
|
||||
|
||||
export const setUsersTags = ({
|
||||
tags,
|
||||
credentials,
|
||||
value,
|
||||
screen_names: nicknames,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: TAG_USER_URL,
|
||||
method: value ? 'PUT' : 'DELETE',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames,
|
||||
tags,
|
||||
},
|
||||
})
|
||||
|
||||
export const setUsersRight = ({
|
||||
right,
|
||||
credentials,
|
||||
value,
|
||||
screen_names: nicknames,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: PERMISSION_GROUP_URL(right),
|
||||
method: value ? 'POST' : 'DELETE',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames,
|
||||
},
|
||||
})
|
||||
|
||||
export const setUsersActivationStatus = ({
|
||||
credentials,
|
||||
screen_names: nicknames,
|
||||
value,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: value ? ACTIVATE_USERS_URL : DEACTIVATE_USERS_URL,
|
||||
method: 'PATCH',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames,
|
||||
},
|
||||
}).then((response) => response.users)
|
||||
|
||||
export const setUsersApprovalStatus = ({
|
||||
credentials,
|
||||
screen_names: nicknames,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: APPROVE_USERS_URL,
|
||||
method: 'PATCH',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames,
|
||||
},
|
||||
}).then((response) => response.users)
|
||||
|
||||
export const setUsersConfirmationStatus = ({
|
||||
credentials,
|
||||
screen_names: nicknames,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: CONFIRM_USERS_URL,
|
||||
method: 'PATCH',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames,
|
||||
},
|
||||
}).then((response) => response.users)
|
||||
|
||||
export const setUsersSuggestionStatus = ({
|
||||
credentials,
|
||||
screen_names: nicknames,
|
||||
value,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: value ? SUGGEST_USERS_URL : UNSUGGEST_USERS_URL,
|
||||
method: 'PATCH',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames,
|
||||
},
|
||||
}).then((response) => response.users)
|
||||
|
||||
export const getUserData = ({ credentials, screen_name: nickname }) =>
|
||||
promisedRequest({
|
||||
url: USERS_URL(nickname),
|
||||
method: 'GET',
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const deleteAccounts = ({ credentials, screen_names: nicknames }) =>
|
||||
promisedRequest({
|
||||
url: USERS_URL(),
|
||||
method: 'DELETE',
|
||||
credentials,
|
||||
payload: {
|
||||
nicknames,
|
||||
},
|
||||
})
|
||||
|
||||
export const getAnnouncements = ({ id, credentials }) =>
|
||||
promisedRequest({ url: ANNOUNCEMENTS_URL(id), credentials })
|
||||
|
||||
// the reported list is hardly useful because standards are for dating i guess,
|
||||
// so make sure to fetchIfMissing right afterward using this call
|
||||
export const listUsers = ({ opts, credentials }) =>
|
||||
promisedRequest({
|
||||
url: USERS_URL_LIST(opts),
|
||||
credentials,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
export const resendConfirmationEmail = ({
|
||||
screen_names: nicknames,
|
||||
credentials,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: RESEND_CONFIRMATION_EMAIL_URL,
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
payload: {
|
||||
nicknames,
|
||||
},
|
||||
})
|
||||
|
||||
export const requirePasswordChange = ({
|
||||
screen_names: nicknames,
|
||||
credentials,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: REQUIRE_PASSWORD_CHANGE_URL,
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
payload: {
|
||||
nicknames,
|
||||
},
|
||||
})
|
||||
|
||||
export const disableMFA = ({ screen_name: nickname, credentials }) =>
|
||||
promisedRequest({
|
||||
url: DISABLE_MFA_URL,
|
||||
credentials,
|
||||
method: 'PUT',
|
||||
payload: {
|
||||
nickname,
|
||||
},
|
||||
})
|
||||
|
||||
export const listStatuses = ({ opts, credentials }) =>
|
||||
promisedRequest({
|
||||
url: LIST_STATUSES_URL(opts),
|
||||
credentials,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
export const changeStatusScope = ({
|
||||
opts: { id, sensitive, visibility },
|
||||
credentials,
|
||||
}) => {
|
||||
var payload = {}
|
||||
if (typeof sensitive !== 'undefined') {
|
||||
payload['sensitive'] = sensitive
|
||||
}
|
||||
if (typeof visibility !== 'undefined') {
|
||||
payload['visibility'] = visibility
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: CHANGE_STATUS_SCOPE_URL(id),
|
||||
credentials,
|
||||
method: 'PUT',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
export const announcementToPayload = ({
|
||||
content,
|
||||
startsAt,
|
||||
endsAt,
|
||||
allDay,
|
||||
}) => {
|
||||
const payload = { content }
|
||||
|
||||
if (typeof startsAt !== 'undefined') {
|
||||
payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null
|
||||
}
|
||||
|
||||
if (typeof endsAt !== 'undefined') {
|
||||
payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null
|
||||
}
|
||||
|
||||
if (typeof allDay !== 'undefined') {
|
||||
payload.all_day = allDay
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export const postAnnouncement = ({
|
||||
credentials,
|
||||
content,
|
||||
startsAt,
|
||||
endsAt,
|
||||
allDay,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: ANNOUNCEMENTS_URL(),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload: announcementToPayload({ content, startsAt, endsAt, allDay }),
|
||||
})
|
||||
|
||||
export const editAnnouncement = ({
|
||||
id,
|
||||
credentials,
|
||||
content,
|
||||
startsAt,
|
||||
endsAt,
|
||||
allDay,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: ANNOUNCEMENTS_URL(id),
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
payload: announcementToPayload({ content, startsAt, endsAt, allDay }),
|
||||
})
|
||||
|
||||
export const deleteAnnouncement = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: ANNOUNCEMENTS_URL(id),
|
||||
credentials,
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
export const setReportState = ({ id, state, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: REPORTS,
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
payload: {
|
||||
reports: [
|
||||
{
|
||||
id,
|
||||
state,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const getInstanceDBConfig = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: CONFIG_URL,
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const getInstanceConfigDescriptions = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: DESCRIPTIONS_URL,
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const getAvailableFrontends = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: FRONTENDS_URL,
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const pushInstanceDBConfig = ({ credentials, payload }) =>
|
||||
promisedRequest({
|
||||
url: CONFIG_URL,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
payload,
|
||||
})
|
||||
|
||||
export const installFrontend = ({ credentials, payload }) =>
|
||||
promisedRequest({
|
||||
url: FRONTENDS_INSTALL_URL,
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload,
|
||||
})
|
||||
|
||||
// Emoji packs
|
||||
export const deleteEmojiPack = ({ name }) =>
|
||||
promisedRequest({
|
||||
url: EMOJI_PACK_URL(name),
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
export const reloadEmoji = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: EMOJI_RELOAD_URL,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const importEmojiFromFS = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: EMOJI_IMPORT_FS_URL,
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const createEmojiPack = ({ name, credentials }) =>
|
||||
promisedRequest({
|
||||
url: EMOJI_PACK_URL(name),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const listRemoteEmojiPacks = ({
|
||||
instance,
|
||||
page,
|
||||
pageSize,
|
||||
credentials,
|
||||
}) => {
|
||||
if (!instance.startsWith('http')) {
|
||||
instance = 'https://' + instance
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize),
|
||||
credentials,
|
||||
})
|
||||
}
|
||||
|
||||
export const downloadRemoteEmojiPack = ({
|
||||
instance,
|
||||
packName,
|
||||
as,
|
||||
credentials,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: EMOJI_PACKS_DL_REMOTE_URL,
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload: {
|
||||
url: instance,
|
||||
name: packName,
|
||||
as,
|
||||
},
|
||||
})
|
||||
|
||||
export const downloadRemoteEmojiPackZIP = ({
|
||||
url,
|
||||
packName,
|
||||
file,
|
||||
credentials,
|
||||
}) => {
|
||||
const data = new FormData()
|
||||
if (file) data.set('file', file)
|
||||
if (url) data.set('url', url)
|
||||
data.set('name', packName)
|
||||
|
||||
return promisedRequest({
|
||||
url: EMOJI_PACKS_DL_REMOTE_ZIP_URL,
|
||||
method: 'POST',
|
||||
formData: data,
|
||||
})
|
||||
}
|
||||
|
||||
export const saveEmojiPackMetadata = ({ name, newData, credentials }) =>
|
||||
promisedRequest({
|
||||
url: EMOJI_PACK_URL(name),
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
payload: { metadata: newData },
|
||||
})
|
||||
|
||||
export const addNewEmojiFile = ({ packName, file, shortcode, filename }) => {
|
||||
const data = new FormData()
|
||||
if (filename.trim() !== '') {
|
||||
data.set('filename', filename)
|
||||
}
|
||||
if (shortcode.trim() !== '') {
|
||||
data.set('shortcode', shortcode)
|
||||
}
|
||||
data.set('file', file)
|
||||
|
||||
return promisedRequest({
|
||||
url: EMOJI_UPDATE_FILE_URL(packName),
|
||||
method: 'POST',
|
||||
formData: data,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateEmojiFile = ({
|
||||
packName,
|
||||
shortcode,
|
||||
newShortcode,
|
||||
newFilename,
|
||||
credentials,
|
||||
force,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: EMOJI_UPDATE_FILE_URL(packName),
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
payload: {
|
||||
shortcode,
|
||||
new_shortcode: newShortcode,
|
||||
new_filename: newFilename,
|
||||
force,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteEmojiFile = ({ packName, shortcode }) =>
|
||||
promisedRequest({
|
||||
url: `${EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import { paramsString, promisedRequest } from './helpers.js'
|
||||
|
||||
import { parseChat } from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
|
||||
const PLEROMA_CHATS_URL = '/api/v1/pleroma/chats'
|
||||
const PLEROMA_CHAT_URL = (id) => `/api/v1/pleroma/chats/by-account-id/${id}`
|
||||
const PLEROMA_CHAT_MESSAGES_URL = (id, { maxId, sinceId, limit }) =>
|
||||
`/api/v1/pleroma/chats/${id}/messages${paramsString({ maxId, sinceId, limit })}`
|
||||
const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read`
|
||||
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) =>
|
||||
`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
||||
|
||||
export const chats = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_CHATS_URL,
|
||||
credentials,
|
||||
}).then(({ data }) => ({
|
||||
chatList: data.map(parseChat).filter((c) => c),
|
||||
}))
|
||||
|
||||
export const getOrCreateChat = ({ accountId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_CHAT_URL(accountId),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const chatMessages = ({
|
||||
id,
|
||||
credentials,
|
||||
maxId,
|
||||
sinceId,
|
||||
limit = 20,
|
||||
}) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_CHAT_MESSAGES_URL(id, { maxId, sinceId, limit }),
|
||||
method: 'GET',
|
||||
credentials,
|
||||
})
|
||||
}
|
||||
|
||||
export const sendChatMessage = ({
|
||||
id,
|
||||
content,
|
||||
mediaId = null,
|
||||
idempotencyKey,
|
||||
credentials,
|
||||
}) => {
|
||||
const payload = {
|
||||
content,
|
||||
}
|
||||
|
||||
if (mediaId) {
|
||||
payload.media_id = mediaId
|
||||
}
|
||||
|
||||
const headers = {}
|
||||
|
||||
if (idempotencyKey) {
|
||||
headers['idempotency-key'] = idempotencyKey
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
||||
method: 'POST',
|
||||
payload,
|
||||
credentials,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
export const readChat = ({ id, lastReadId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_CHAT_READ_URL(id),
|
||||
method: 'POST',
|
||||
payload: {
|
||||
last_read_id: lastReadId,
|
||||
},
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const deleteChatMessage = ({ chatId, messageId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId),
|
||||
method: 'DELETE',
|
||||
credentials,
|
||||
})
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
import { snakeCase } from 'lodash'
|
||||
|
||||
import { StatusCodeError } from 'src/services/errors/errors'
|
||||
|
||||
export const paramsString = (params = {}) => {
|
||||
if (params == null || params === undefined) return ''
|
||||
|
||||
if (typeof params !== 'object' || Array.isArray(params)) {
|
||||
throw new Error('Params are not an object!')
|
||||
}
|
||||
|
||||
const entries = (() => {
|
||||
if (params instanceof Map) {
|
||||
return params.entries()
|
||||
} else {
|
||||
return Object.entries(params)
|
||||
}
|
||||
})()
|
||||
|
||||
if (entries.length === 0) return ''
|
||||
|
||||
const arrays = []
|
||||
const nonArrays = []
|
||||
|
||||
entries.forEach(([k, v]) => {
|
||||
if (v == null) return // Drop nulls
|
||||
if (
|
||||
(typeof v === 'object' && !Array.isArray(v)) ||
|
||||
typeof v === 'function'
|
||||
) {
|
||||
throw new Error('Param cannot be non-primitive!')
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
arrays.push([k, v])
|
||||
} else {
|
||||
nonArrays.push([k, v])
|
||||
}
|
||||
})
|
||||
|
||||
arrays.forEach(([k, array]) => {
|
||||
array.forEach((v) => {
|
||||
if (
|
||||
typeof v === 'object' ||
|
||||
typeof v === 'function' ||
|
||||
typeof v === 'undefined'
|
||||
)
|
||||
throw new Error('Array param cannot contain non-primitives!')
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
'?' +
|
||||
[
|
||||
...nonArrays.map(([k, v]) => [snakeCase(k), v]),
|
||||
// turning [a,[1,2,3]] into [[a[],1],[a[],2],[a[],3]]
|
||||
...arrays.reduce(
|
||||
(acc, [k, arrayValue]) => [
|
||||
...acc,
|
||||
...arrayValue.map((v) => [snakeCase(k) + '[]', v]),
|
||||
],
|
||||
[],
|
||||
),
|
||||
]
|
||||
.map(([k, v]) => `${k}=${window.encodeURIComponent(v)}`)
|
||||
.join('&')
|
||||
)
|
||||
}
|
||||
|
||||
export const promisedRequest = async ({
|
||||
method,
|
||||
url,
|
||||
payload,
|
||||
formData,
|
||||
cache,
|
||||
credentials,
|
||||
headers = {},
|
||||
}) => {
|
||||
const options = {
|
||||
method,
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...headers,
|
||||
},
|
||||
}
|
||||
|
||||
if (!formData) {
|
||||
options.headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
options.cache = cache
|
||||
}
|
||||
|
||||
if (formData || payload) {
|
||||
options.body = formData || JSON.stringify(payload)
|
||||
}
|
||||
|
||||
if (credentials) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
...authHeaders(credentials),
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, options)
|
||||
const data = await (async () => {
|
||||
const [contentType] = response.headers
|
||||
.get('content-type')
|
||||
.split(';')
|
||||
.map((x) => x.toLowerCase().trim())
|
||||
const contentLength = parseInt(response.headers.get('content-length'))
|
||||
if (contentLength === 0) return null
|
||||
|
||||
switch (contentType) {
|
||||
case 'text/plain':
|
||||
return await response.text()
|
||||
case 'application/json':
|
||||
return await response.json()
|
||||
default:
|
||||
return await response.bytes()
|
||||
}
|
||||
})()
|
||||
|
||||
const { ok, status } = response
|
||||
|
||||
if (ok) {
|
||||
return { response, status, data }
|
||||
} else {
|
||||
throw new StatusCodeError(response.status, data, { url, options }, response)
|
||||
}
|
||||
}
|
||||
|
||||
const authHeaders = (accessToken) => {
|
||||
if (accessToken) {
|
||||
return { Authorization: `Bearer ${accessToken}` }
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { promisedRequest } from './helpers.js'
|
||||
|
||||
export const verifyOTPCode = ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
instance,
|
||||
mfaToken,
|
||||
code,
|
||||
}) => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_id', clientId)
|
||||
formData.append('client_secret', clientSecret)
|
||||
formData.append('mfa_token', mfaToken)
|
||||
formData.append('code', code)
|
||||
formData.append('challenge_type', 'totp')
|
||||
|
||||
return promisedRequest({
|
||||
url: '/oauth/mfa/challenge',
|
||||
method: 'POST',
|
||||
formData,
|
||||
})
|
||||
}
|
||||
|
||||
export const verifyRecoveryCode = ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
instance,
|
||||
mfaToken,
|
||||
code,
|
||||
}) => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_id', clientId)
|
||||
formData.append('client_secret', clientSecret)
|
||||
formData.append('mfa_token', mfaToken)
|
||||
formData.append('code', code)
|
||||
formData.append('challenge_type', 'recovery')
|
||||
|
||||
return promisedRequest({
|
||||
url: `${instance}/oauth/mfa/challenge`,
|
||||
method: 'POST',
|
||||
formData,
|
||||
})
|
||||
}
|
||||
146
src/api/oauth.js
146
src/api/oauth.js
|
|
@ -1,146 +0,0 @@
|
|||
import { paramsString, promisedRequest } from './helpers.js'
|
||||
|
||||
const REDIRECT_URI = `${window.location.origin}/oauth-callback`
|
||||
|
||||
export const MASTODON_APP_VERIFY_URL = '/api/v1/apps/verify_credentials'
|
||||
export const MASTODON_APP_URL = '/api/v1/apps'
|
||||
export const OAUTH_TOKEN_URL = '/oauth/token'
|
||||
export const OAUTH_MFA_CHALLENGE_URL = '/oauth/mfa/challenge'
|
||||
export const OAUTH_REVOKE_URL = '/oauth/revoke'
|
||||
|
||||
export const createApp = () => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_name', 'PleromaFE')
|
||||
formData.append('website', 'https://pleroma.social')
|
||||
formData.append('redirect_uris', REDIRECT_URI)
|
||||
formData.append('scopes', 'read write follow push admin')
|
||||
|
||||
return promisedRequest({
|
||||
method: 'POST',
|
||||
url: MASTODON_APP_URL,
|
||||
formData,
|
||||
}).then(({ data, ...rest }) => ({
|
||||
...rest,
|
||||
data: {
|
||||
...data,
|
||||
clientId: data.client_id,
|
||||
clientSecret: data.client_secret,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export const verifyAppToken = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_APP_VERIFY_URL,
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const getLoginUrl = ({ instance, clientId }) => {
|
||||
const data = {
|
||||
responseType: 'code',
|
||||
clientId,
|
||||
redirectUri: REDIRECT_URI,
|
||||
scope: 'read write follow push admin',
|
||||
}
|
||||
|
||||
return `${instance}/oauth/authorize${paramsString(data)}`
|
||||
}
|
||||
|
||||
export const getTokenWithCredentials = ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
username,
|
||||
password,
|
||||
}) => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_id', clientId)
|
||||
formData.append('client_secret', clientSecret)
|
||||
formData.append('grant_type', 'password')
|
||||
formData.append('username', username)
|
||||
formData.append('password', password)
|
||||
|
||||
return promisedRequest({
|
||||
url: OAUTH_TOKEN_URL,
|
||||
method: 'POST',
|
||||
formData,
|
||||
})
|
||||
}
|
||||
|
||||
export const getToken = ({ clientId, clientSecret, code }) => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_id', clientId)
|
||||
formData.append('client_secret', clientSecret)
|
||||
formData.append('grant_type', 'authorization_code')
|
||||
formData.append('code', code)
|
||||
formData.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||
|
||||
return promisedRequest({
|
||||
url: OAUTH_TOKEN_URL,
|
||||
method: 'POST',
|
||||
formData,
|
||||
})
|
||||
}
|
||||
|
||||
export const getClientToken = ({ clientId, clientSecret }) => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_id', clientId)
|
||||
formData.append('client_secret', clientSecret)
|
||||
formData.append('grant_type', 'client_credentials')
|
||||
formData.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||
|
||||
return promisedRequest({
|
||||
url: OAUTH_TOKEN_URL,
|
||||
method: 'POST',
|
||||
formData,
|
||||
})
|
||||
}
|
||||
|
||||
export const verifyOTPCode = ({ app, mfaToken, code }) => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_id', app.client_id)
|
||||
formData.append('client_secret', app.client_secret)
|
||||
formData.append('mfa_token', mfaToken)
|
||||
formData.append('code', code)
|
||||
formData.append('challenge_type', 'totp')
|
||||
|
||||
return promisedRequest({
|
||||
url: OAUTH_MFA_CHALLENGE_URL,
|
||||
method: 'POST',
|
||||
formData,
|
||||
})
|
||||
}
|
||||
|
||||
export const verifyRecoveryCode = ({ app, mfaToken, code }) => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_id', app.client_id)
|
||||
formData.append('client_secret', app.client_secret)
|
||||
formData.append('mfa_token', mfaToken)
|
||||
formData.append('code', code)
|
||||
formData.append('challenge_type', 'recovery')
|
||||
|
||||
return promisedRequest({
|
||||
url: OAUTH_MFA_CHALLENGE_URL,
|
||||
method: 'POST',
|
||||
formData,
|
||||
})
|
||||
}
|
||||
|
||||
export const revokeToken = ({ app, token }) => {
|
||||
const formData = new window.FormData()
|
||||
|
||||
formData.append('client_id', app.clientId)
|
||||
formData.append('client_secret', app.clientSecret)
|
||||
formData.append('token', token)
|
||||
|
||||
return promisedRequest({
|
||||
url: OAUTH_REVOKE_URL,
|
||||
method: 'POST',
|
||||
formData,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
import { paramsString, promisedRequest } from './helpers.js'
|
||||
import { MASTODON_USER_TIMELINE_URL } from './timelines.js'
|
||||
|
||||
import {
|
||||
parseSource,
|
||||
parseStatus,
|
||||
parseUser,
|
||||
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
|
||||
const MASTODON_SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
|
||||
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
|
||||
const MASTODON_PASSWORD_RESET_URL = ({ email }) =>
|
||||
`/auth/password${paramsString({ email })}`
|
||||
|
||||
const MASTODON_FOLLOWING_URL = (
|
||||
id,
|
||||
{ minId, maxId, sinceId, limit, withRelationships },
|
||||
) =>
|
||||
`/api/v1/accounts/${id}/following${paramsString({ minId, maxId, sinceId, limit, withRelationships })}`
|
||||
const MASTODON_FOLLOWERS_URL = (
|
||||
id,
|
||||
{ minId, maxId, sinceId, limit, withRelationships },
|
||||
) =>
|
||||
`/api/v1/accounts/${id}/followers${paramsString({ minId, maxId, sinceId, limit, withRelationships })}`
|
||||
|
||||
export const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}`
|
||||
const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context`
|
||||
const MASTODON_STATUS_SOURCE_URL = (id) => `/api/v1/statuses/${id}/source`
|
||||
const MASTODON_STATUS_HISTORY_URL = (id) => `/api/v1/statuses/${id}/history`
|
||||
const MASTODON_USER_URL = '/api/v1/accounts'
|
||||
const MASTODON_USER_LOOKUP_URL = ({ acct }) =>
|
||||
`/api/v1/accounts/lookup${paramsString({ acct })}`
|
||||
const MASTODON_POLL_URL = (id = '') => `/api/v1/polls/${id}`
|
||||
const MASTODON_STATUS_FAVORITEDBY_URL = (id) =>
|
||||
`/api/v1/statuses/${id}/favourited_by`
|
||||
const MASTODON_STATUS_REBLOGGEDBY_URL = (id) =>
|
||||
`/api/v1/statuses/${id}/reblogged_by`
|
||||
const MASTODON_SEARCH_2 = ({
|
||||
q,
|
||||
resolve,
|
||||
limit,
|
||||
offset,
|
||||
following,
|
||||
type,
|
||||
withRelationships,
|
||||
accountId,
|
||||
excludeUnreviewed,
|
||||
}) =>
|
||||
`/api/v2/search${paramsString({ q, resolve, limit, offset, following, type, withRelationships, accountId, excludeUnreviewed })}`
|
||||
const MASTODON_USER_SEARCH_URL = ({ q, resolve }) =>
|
||||
`/api/v1/accounts/search${paramsString({ q, resolve })}`
|
||||
const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
|
||||
const PLEROMA_EMOJI_REACTIONS_URL = (id) =>
|
||||
`/api/v1/pleroma/statuses/${id}/reactions`
|
||||
const PLEROMA_SCROBBLES_URL = (id, { maxId, sinceId, minId, limit, offset }) =>
|
||||
`/api/v1/pleroma/accounts/${id}/scrobbles${paramsString({ maxId, sinceId, minId, limit, offset })}`
|
||||
|
||||
const EMOJI_PACKS_URL = (page, pageSize) =>
|
||||
`/api/v1/pleroma/emoji/packs${paramsString({ page, pageSize })}`
|
||||
|
||||
// Params needed:
|
||||
// nickname
|
||||
// email
|
||||
// fullname
|
||||
// password
|
||||
// password_confirm
|
||||
//
|
||||
// Optional
|
||||
// bio
|
||||
// homepage
|
||||
// location
|
||||
// token
|
||||
// language
|
||||
export const register = ({ params, credentials }) => {
|
||||
const { nickname, ...rest } = params
|
||||
return promisedRequest({
|
||||
url: MASTODON_REGISTRATION_URL,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
payload: {
|
||||
nickname,
|
||||
locale: 'en_US',
|
||||
agreement: true,
|
||||
...rest,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const getCaptcha = () =>
|
||||
promisedRequest({
|
||||
url: '/api/pleroma/captcha',
|
||||
})
|
||||
|
||||
export const fetchUser = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: `${MASTODON_USER_URL}/${id}`,
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||
|
||||
export const fetchUserByName = ({ name, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_USER_LOOKUP_URL({ acct: name }),
|
||||
credentials,
|
||||
})
|
||||
.then(({ data }) => data.id)
|
||||
.catch((error) => {
|
||||
if (error && error.statusCode === 404) {
|
||||
// Either the backend does not support lookup endpoint,
|
||||
// or there is no user with such name. Fallback and treat name as id.
|
||||
return name
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
.then((id) => fetchUser({ id, credentials }))
|
||||
|
||||
export const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_FOLLOWING_URL(id, { maxId, sinceId, limit }),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||
|
||||
export const fetchFollowers = ({
|
||||
id,
|
||||
maxId,
|
||||
sinceId,
|
||||
limit = 20,
|
||||
credentials,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_FOLLOWERS_URL(id, {
|
||||
maxId,
|
||||
sinceId,
|
||||
limit,
|
||||
withRelationships: true,
|
||||
}),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||
|
||||
export const fetchConversation = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_STATUS_CONTEXT_URL(id),
|
||||
credentials,
|
||||
}).then((result) => ({
|
||||
...result,
|
||||
data: {
|
||||
...result.data,
|
||||
ancestors: result.data.ancestors.map(parseStatus),
|
||||
descendants: result.data.descendants.map(parseStatus),
|
||||
},
|
||||
}))
|
||||
|
||||
export const fetchStatus = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_STATUS_URL(id),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const fetchStatusSource = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_STATUS_SOURCE_URL(id),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseSource(data) }))
|
||||
|
||||
export const fetchStatusHistory = ({ status, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_STATUS_HISTORY_URL(status.id),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => {
|
||||
return [...data].reverse().map((item) => {
|
||||
item.originalStatus = status
|
||||
return { ...rest, data: parseStatus(item) }
|
||||
})
|
||||
})
|
||||
|
||||
export const listEmojiPacks = ({ page, pageSize, credentials }) =>
|
||||
promisedRequest({
|
||||
url: EMOJI_PACKS_URL(page, pageSize),
|
||||
})
|
||||
|
||||
export const fetchPinnedStatuses = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_USER_TIMELINE_URL(id, { pinned: true }),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseStatus) }))
|
||||
|
||||
export const verifyCredentials = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LOGIN_URL,
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||
|
||||
export const resetPassword = ({ email }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_PASSWORD_RESET_URL({ email }),
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export const suggestions = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_SUGGESTIONS_URL,
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const fetchPoll = ({ pollId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_POLL_URL(encodeURIComponent(pollId)),
|
||||
method: 'GET',
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const fetchFavoritedByUsers = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_STATUS_FAVORITEDBY_URL(id),
|
||||
method: 'GET',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||
|
||||
export const fetchRebloggedByUsers = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_STATUS_REBLOGGEDBY_URL(id),
|
||||
method: 'GET',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||
|
||||
export const fetchEmojiReactions = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_EMOJI_REACTIONS_URL(id),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({
|
||||
...rest,
|
||||
data: data.map((r) => {
|
||||
r.accounts = r.accounts.map(parseUser)
|
||||
return r
|
||||
}),
|
||||
}))
|
||||
|
||||
export const searchUsers = ({ credentials, query }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_USER_SEARCH_URL({ q: query, resolve: true }),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||
|
||||
export const search2 = ({
|
||||
credentials,
|
||||
q,
|
||||
resolve,
|
||||
limit,
|
||||
offset,
|
||||
following,
|
||||
type,
|
||||
}) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_SEARCH_2({
|
||||
q,
|
||||
resolve,
|
||||
limit,
|
||||
offset,
|
||||
following,
|
||||
type,
|
||||
withRelationships: true,
|
||||
}),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => {
|
||||
data.accounts = data.accounts.slice(0, limit).map((u) => parseUser(u))
|
||||
data.statuses = data.statuses.slice(0, limit).map((s) => parseStatus(s))
|
||||
return { ...rest, data }
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchKnownDomains = ({ credentials }) =>
|
||||
promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials })
|
||||
|
||||
export const fetchScrobbles = ({ accountId, limit = 1 }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_SCROBBLES_URL(accountId, { limit }),
|
||||
})
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
import { paramsString, promisedRequest } from './helpers.js'
|
||||
|
||||
import {
|
||||
parseLinkHeaderPagination,
|
||||
parseNotification,
|
||||
parseStatus,
|
||||
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
|
||||
const MASTODON_USER_HOME_TIMELINE_URL = ({
|
||||
minId,
|
||||
sinceId,
|
||||
maxId,
|
||||
limit,
|
||||
replyVisibility,
|
||||
}) =>
|
||||
`/api/v1/timelines/home${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||
const MASTODON_LIST_TIMELINE_URL = (
|
||||
id,
|
||||
{ minId, sinceId, maxId, limit, replyVisibility },
|
||||
) =>
|
||||
`/api/v1/timelines/list/${id}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||
const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = ({
|
||||
minId,
|
||||
sinceId,
|
||||
maxId,
|
||||
limit,
|
||||
replyVisibility,
|
||||
}) =>
|
||||
`/api/v1/timelines/direct${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||
const MASTODON_PUBLIC_TIMELINE = ({
|
||||
minId,
|
||||
sinceId,
|
||||
maxId,
|
||||
limit,
|
||||
replyVisibility,
|
||||
local,
|
||||
remote,
|
||||
onlyMedia,
|
||||
}) =>
|
||||
`/api/v1/timelines/public${paramsString({ minId, sinceId, maxId, limit, replyVisibility, local, remote, onlyMedia })}`
|
||||
const MASTODON_TAG_TIMELINE_URL = (
|
||||
tag,
|
||||
{ minId, sinceId, maxId, limit, replyVisibility },
|
||||
) =>
|
||||
`/api/v1/timelines/tag/${tag}${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||
export const MASTODON_USER_TIMELINE_URL = (
|
||||
id,
|
||||
{ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia },
|
||||
) =>
|
||||
`/api/v1/accounts/${id}/statuses${paramsString({ minId, sinceId, maxId, limit, replyVisibility, pinned, onlyMedia })}`
|
||||
const MASTODON_USER_FAVORITES_TIMELINE_URL = ({
|
||||
minId,
|
||||
sinceId,
|
||||
maxId,
|
||||
limit,
|
||||
replyVisibility,
|
||||
}) =>
|
||||
`/api/v1/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||
const MASTODON_BOOKMARK_TIMELINE_URL = ({
|
||||
minId,
|
||||
sinceId,
|
||||
maxId,
|
||||
limit,
|
||||
replyVisibility,
|
||||
folderId,
|
||||
}) =>
|
||||
`/api/v1/bookmarks${paramsString({ minId, sinceId, maxId, limit, replyVisibility, folderId })}`
|
||||
const PLEROMA_STATUS_QUOTES_URL = (
|
||||
id,
|
||||
{ minId, sinceId, maxId, limit, replyVisibility },
|
||||
) =>
|
||||
`/api/v1/pleroma/statuses/${id}/quotes${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||
const PLEROMA_USER_FAVORITES_TIMELINE_URL = (
|
||||
id,
|
||||
{ minId, sinceId, maxId, limit, replyVisibility },
|
||||
) =>
|
||||
`/api/v1/pleroma/accounts/${id}/favourites${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||
const AKKOMA_BUBBLE_TIMELINE_URL = ({
|
||||
minId,
|
||||
sinceId,
|
||||
maxId,
|
||||
limit,
|
||||
replyVisibility,
|
||||
}) =>
|
||||
`/api/v1/timelines/bubble${paramsString({ minId, sinceId, maxId, limit, replyVisibility })}`
|
||||
|
||||
const MASTODON_USER_NOTIFICATIONS_URL = ({
|
||||
minId,
|
||||
sinceId,
|
||||
maxId,
|
||||
limit,
|
||||
includeTypes,
|
||||
replyVisibility,
|
||||
}) =>
|
||||
`/api/v1/notifications${paramsString({ minId, sinceId, maxId, limit, includeTypes, replyVisibility })}`
|
||||
|
||||
export const fetchTimeline = ({
|
||||
timeline,
|
||||
credentials,
|
||||
sinceId,
|
||||
minId,
|
||||
maxId,
|
||||
userId,
|
||||
listId,
|
||||
statusId,
|
||||
tag,
|
||||
withMuted,
|
||||
replyVisibility = 'all',
|
||||
includeTypes = [],
|
||||
bookmarkFolderId,
|
||||
}) => {
|
||||
const timelineUrls = {
|
||||
friends: MASTODON_USER_HOME_TIMELINE_URL,
|
||||
public: MASTODON_PUBLIC_TIMELINE,
|
||||
publicAndExternal: MASTODON_PUBLIC_TIMELINE,
|
||||
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
|
||||
user: MASTODON_USER_TIMELINE_URL,
|
||||
media: MASTODON_USER_TIMELINE_URL,
|
||||
list: MASTODON_LIST_TIMELINE_URL,
|
||||
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
||||
publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL,
|
||||
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL,
|
||||
bubble: AKKOMA_BUBBLE_TIMELINE_URL,
|
||||
tag: MASTODON_TAG_TIMELINE_URL,
|
||||
quotes: PLEROMA_STATUS_QUOTES_URL,
|
||||
|
||||
notifications: MASTODON_USER_NOTIFICATIONS_URL,
|
||||
}
|
||||
const urlFunc = timelineUrls[timeline]
|
||||
|
||||
const twoArgs = new Set([
|
||||
'user',
|
||||
'media',
|
||||
'list',
|
||||
'publicFavorites',
|
||||
'tag',
|
||||
'quotes',
|
||||
])
|
||||
|
||||
const params = {
|
||||
minId,
|
||||
sinceId,
|
||||
maxId,
|
||||
limit: 20,
|
||||
}
|
||||
|
||||
const id = (() => {
|
||||
switch (timeline) {
|
||||
case 'user':
|
||||
case 'media':
|
||||
return userId
|
||||
case 'list':
|
||||
return listId
|
||||
case 'quotes':
|
||||
return statusId
|
||||
case 'tag':
|
||||
return tag
|
||||
}
|
||||
})()
|
||||
|
||||
const isNotifications = timeline === 'notifications'
|
||||
|
||||
if (timeline === 'media') {
|
||||
params.onlyMedia = true
|
||||
}
|
||||
if (timeline === 'public') {
|
||||
params.local = true
|
||||
}
|
||||
if (timeline !== 'favorites' && timeline !== 'bookmarks') {
|
||||
params.withMuted = withMuted
|
||||
}
|
||||
if (replyVisibility !== 'all') {
|
||||
params.replyVisibility = replyVisibility
|
||||
}
|
||||
if (timeline === 'bookmarks' && bookmarkFolderId) {
|
||||
params.folderId = bookmarkFolderId
|
||||
}
|
||||
|
||||
if (isNotifications && includeTypes.length > 0) {
|
||||
params.includeTypes = includeTypes
|
||||
}
|
||||
|
||||
const url = twoArgs.has(timeline) ? urlFunc(id, params) : urlFunc(params)
|
||||
return promisedRequest({ url, credentials }).then((result) => {
|
||||
const pagination = parseLinkHeaderPagination(
|
||||
result.response.headers.get('Link'),
|
||||
{
|
||||
flakeId: timeline !== 'bookmarks' && timeline !== 'notifications',
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data.map(isNotifications ? parseNotification : parseStatus),
|
||||
pagination,
|
||||
}
|
||||
})
|
||||
}
|
||||
921
src/api/user.js
921
src/api/user.js
|
|
@ -1,921 +0,0 @@
|
|||
import { concat, last } from 'lodash'
|
||||
|
||||
import { paramsString, promisedRequest } from './helpers.js'
|
||||
import { fetchFriends, MASTODON_STATUS_URL } from './public.js'
|
||||
|
||||
import {
|
||||
parseAttachment,
|
||||
parseStatus,
|
||||
parseUser,
|
||||
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
|
||||
const MUTES_IMPORT_URL = '/api/pleroma/mutes_import'
|
||||
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
|
||||
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
|
||||
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
|
||||
const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
|
||||
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
||||
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
|
||||
const ALIASES_URL = '/api/pleroma/aliases'
|
||||
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
||||
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
|
||||
|
||||
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
|
||||
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
|
||||
|
||||
const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
|
||||
const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
|
||||
const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp'
|
||||
|
||||
const MASTODON_DISMISS_NOTIFICATION_URL = (id) =>
|
||||
`/api/v1/notifications/${id}/dismiss`
|
||||
const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite`
|
||||
const MASTODON_UNFAVORITE_URL = (id) => `/api/v1/statuses/${id}/unfavourite`
|
||||
const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog`
|
||||
const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog`
|
||||
const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}`
|
||||
const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow`
|
||||
const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow`
|
||||
|
||||
const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests'
|
||||
const MASTODON_APPROVE_USER_URL = (id) =>
|
||||
`/api/v1/follow_requests/${id}/authorize`
|
||||
const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject`
|
||||
const MASTODON_USER_RELATIONSHIPS_URL = ({ id, withSuspended }) =>
|
||||
`/api/v1/accounts/relationships/${paramsString({ id, withSuspended })}`
|
||||
const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists`
|
||||
export const MASTODON_LIST_URL = (id = '') => `/api/v1/lists/${id}`
|
||||
export const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts`
|
||||
const MASTODON_USER_BLOCKS_URL = ({
|
||||
maxId,
|
||||
sinceId,
|
||||
limit,
|
||||
withRelationships,
|
||||
}) =>
|
||||
`/api/v1/blocks/${paramsString({ maxId, sinceId, limit, withRelationships })}`
|
||||
const MASTODON_USER_MUTES_URL = ({
|
||||
maxId,
|
||||
sinceId,
|
||||
limit,
|
||||
withRelationships,
|
||||
}) =>
|
||||
`/api/v1/mutes/${paramsString({ maxId, sinceId, limit, withRelationships })}`
|
||||
const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block`
|
||||
const MASTODON_UNBLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/unblock`
|
||||
const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute`
|
||||
const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute`
|
||||
const MASTODON_REMOVE_USER_FROM_FOLLOWERS = (id) =>
|
||||
`/api/v1/accounts/${id}/remove_from_followers`
|
||||
const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note`
|
||||
const MASTODON_BOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark`
|
||||
const MASTODON_UNBOOKMARK_STATUS_URL = (id) =>
|
||||
`/api/v1/statuses/${id}/unbookmark`
|
||||
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
|
||||
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
||||
const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes`
|
||||
const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
|
||||
const MASTODON_REPORT_USER_URL = '/api/v1/reports'
|
||||
const MASTODON_PIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/pin`
|
||||
const MASTODON_UNPIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/unpin`
|
||||
const MASTODON_MUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/mute`
|
||||
const MASTODON_UNMUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/unmute`
|
||||
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
|
||||
const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements'
|
||||
const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) =>
|
||||
`/api/v1/announcements/${id}/dismiss`
|
||||
const PLEROMA_EMOJI_REACT_URL = (id, emoji) =>
|
||||
`/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) =>
|
||||
`/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
||||
const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders'
|
||||
const PLEROMA_BOOKMARK_FOLDER_URL = (id) =>
|
||||
`/api/v1/pleroma/bookmark_folders/${id}`
|
||||
|
||||
// #Posts
|
||||
export const favorite = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_FAVORITE_URL(id),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const unfavorite = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_UNFAVORITE_URL(id),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const retweet = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_RETWEET_URL(id),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const unretweet = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_UNRETWEET_URL(id),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const reactWithEmoji = ({ id, emoji, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_EMOJI_REACT_URL(id, emoji),
|
||||
method: 'PUT',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const unreactWithEmoji = ({ id, emoji, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
|
||||
method: 'DELETE',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const bookmarkStatus = ({ id, credentials, ...options }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_BOOKMARK_STATUS_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload: {
|
||||
folder_id: options.folder_id,
|
||||
},
|
||||
})
|
||||
|
||||
export const unbookmarkStatus = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_UNBOOKMARK_STATUS_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
export const pinOwnStatus = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_PIN_OWN_STATUS(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const unpinOwnStatus = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_UNPIN_OWN_STATUS(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const muteConversation = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_MUTE_CONVERSATION(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const unmuteConversation = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_UNMUTE_CONVERSATION(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
|
||||
export const vote = ({ pollId, choices, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_VOTE_URL(encodeURIComponent(pollId)),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
payload: {
|
||||
choices,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// #Posting
|
||||
export const postStatus = ({
|
||||
credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
visibility,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaIds = [],
|
||||
inReplyToStatusId,
|
||||
quoteId,
|
||||
contentType,
|
||||
preview,
|
||||
idempotencyKey,
|
||||
}) => {
|
||||
const form = new FormData()
|
||||
const pollOptions = poll.options || []
|
||||
|
||||
form.append('status', status)
|
||||
form.append('source', 'Pleroma FE')
|
||||
if (spoilerText) form.append('spoiler_text', spoilerText)
|
||||
if (visibility) form.append('visibility', visibility)
|
||||
if (sensitive) form.append('sensitive', sensitive)
|
||||
if (contentType) form.append('content_type', contentType)
|
||||
mediaIds.forEach((val) => {
|
||||
form.append('media_ids[]', val)
|
||||
})
|
||||
if (pollOptions.some((option) => option !== '')) {
|
||||
const normalizedPoll = {
|
||||
expires_in: parseInt(poll.expiresIn, 10),
|
||||
multiple: poll.multiple,
|
||||
}
|
||||
Object.keys(normalizedPoll).forEach((key) => {
|
||||
form.append(`poll[${key}]`, normalizedPoll[key])
|
||||
})
|
||||
|
||||
pollOptions.forEach((option) => {
|
||||
form.append('poll[options][]', option)
|
||||
})
|
||||
}
|
||||
if (inReplyToStatusId) {
|
||||
form.append('in_reply_to_id', inReplyToStatusId)
|
||||
}
|
||||
if (quoteId) {
|
||||
form.append('quote_id', quoteId)
|
||||
}
|
||||
if (preview) {
|
||||
form.append('preview', 'true')
|
||||
}
|
||||
|
||||
const headers = {}
|
||||
if (idempotencyKey) {
|
||||
headers['idempotency-key'] = idempotencyKey
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: MASTODON_POST_STATUS_URL,
|
||||
formData: form,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
headers,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
}
|
||||
|
||||
export const editStatus = ({
|
||||
id,
|
||||
credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaIds = [],
|
||||
contentType,
|
||||
}) => {
|
||||
const form = new FormData()
|
||||
const pollOptions = poll.options || []
|
||||
|
||||
form.append('status', status)
|
||||
if (spoilerText) form.append('spoiler_text', spoilerText)
|
||||
if (sensitive) form.append('sensitive', sensitive)
|
||||
if (contentType) form.append('content_type', contentType)
|
||||
mediaIds.forEach((val) => {
|
||||
form.append('media_ids[]', val)
|
||||
})
|
||||
|
||||
if (pollOptions.some((option) => option !== '')) {
|
||||
const normalizedPoll = {
|
||||
expires_in: parseInt(poll.expiresIn, 10),
|
||||
multiple: poll.multiple,
|
||||
}
|
||||
Object.keys(normalizedPoll).forEach((key) => {
|
||||
form.append(`poll[${key}]`, normalizedPoll[key])
|
||||
})
|
||||
|
||||
pollOptions.forEach((option) => {
|
||||
form.append('poll[options][]', option)
|
||||
})
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: MASTODON_STATUS_URL(id),
|
||||
formData: form,
|
||||
method: 'PUT',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
|
||||
}
|
||||
|
||||
export const deleteStatus = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_DELETE_URL(id),
|
||||
credentials,
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
export const uploadMedia = ({ formData, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_MEDIA_UPLOAD_URL,
|
||||
formData,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) }))
|
||||
|
||||
export const setMediaDescription = ({ id, description, credentials }) =>
|
||||
promisedRequest({
|
||||
url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`,
|
||||
method: 'PUT',
|
||||
credentials,
|
||||
payload: {
|
||||
description,
|
||||
},
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseAttachment(data) }))
|
||||
|
||||
// #Notifications
|
||||
export const dismissNotification = ({ credentials, id }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_DISMISS_NOTIFICATION_URL(id),
|
||||
method: 'POST',
|
||||
payload: { id },
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const markNotificationsAsSeen = ({
|
||||
id,
|
||||
credentials,
|
||||
single = false,
|
||||
}) => {
|
||||
const formData = new FormData()
|
||||
|
||||
if (single) {
|
||||
formData.append('id', id)
|
||||
} else {
|
||||
formData.append('max_id', id)
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: NOTIFICATION_READ_URL,
|
||||
formData,
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// #Announcements
|
||||
export const getAnnouncements = ({ credentials }) =>
|
||||
promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials })
|
||||
|
||||
export const dismissAnnouncement = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
// #Imports
|
||||
export const importMutes = ({ file, credentials }) => {
|
||||
const formData = new FormData()
|
||||
formData.append('list', file)
|
||||
return promisedRequest({
|
||||
url: MUTES_IMPORT_URL,
|
||||
formData,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
}).then((response) => response.ok)
|
||||
}
|
||||
|
||||
export const importBlocks = ({ file, credentials }) => {
|
||||
const formData = new FormData()
|
||||
formData.append('list', file)
|
||||
return promisedRequest({
|
||||
url: BLOCKS_IMPORT_URL,
|
||||
formData,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
}).then((response) => response.ok)
|
||||
}
|
||||
|
||||
export const importFollows = ({ file, credentials }) => {
|
||||
const formData = new FormData()
|
||||
formData.append('list', file)
|
||||
return promisedRequest({
|
||||
url: FOLLOW_IMPORT_URL,
|
||||
formData,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
}).then((response) => response.ok)
|
||||
}
|
||||
|
||||
export const exportFriends = ({ id, credentials }) => {
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO refactor this
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let friends = []
|
||||
let more = true
|
||||
while (more) {
|
||||
const maxId = friends.length > 0 ? last(friends).id : undefined
|
||||
const users = await fetchFriends({
|
||||
id,
|
||||
maxId,
|
||||
credentials,
|
||||
withRelationships: true,
|
||||
})
|
||||
friends = concat(friends, users)
|
||||
if (users.length === 0) {
|
||||
more = false
|
||||
}
|
||||
}
|
||||
resolve(friends)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// #Profile settings
|
||||
export const updateNotificationSettings = ({ credentials, settings }) => {
|
||||
return promisedRequest({
|
||||
url: NOTIFICATION_SETTINGS_URL,
|
||||
credentials,
|
||||
method: 'PUT',
|
||||
payload: settings,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateProfileImages = ({
|
||||
credentials,
|
||||
avatar = null,
|
||||
avatarName = null,
|
||||
banner = null,
|
||||
background = null,
|
||||
}) => {
|
||||
const form = new FormData()
|
||||
if (avatar !== null) {
|
||||
if (avatarName !== null) {
|
||||
form.append('avatar', avatar, avatarName)
|
||||
} else {
|
||||
form.append('avatar', avatar)
|
||||
}
|
||||
}
|
||||
if (banner !== null) form.append('header', banner)
|
||||
if (background !== null) form.append('pleroma_background_image', background)
|
||||
return promisedRequest({
|
||||
url: MASTODON_PROFILE_UPDATE_URL,
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
formData: form,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||
}
|
||||
|
||||
export const updateProfile = ({ credentials, params }) => {
|
||||
const formData = new FormData()
|
||||
|
||||
for (const name in params) {
|
||||
if (name === 'fields_attributes') {
|
||||
params[name].forEach((param, i) => {
|
||||
formData.append(name + `[${i}][name]`, param.name)
|
||||
formData.append(name + `[${i}][value]`, param.value)
|
||||
})
|
||||
} else {
|
||||
if (typeof params[name] === 'object') {
|
||||
console.warn(
|
||||
'Object detected in updateProfile API call. This will not work, use updateProfileJSON instead.',
|
||||
)
|
||||
console.warn('Object:\n' + JSON.stringify(params[name], null, 2))
|
||||
}
|
||||
formData.append(name, params[name])
|
||||
}
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: MASTODON_PROFILE_UPDATE_URL,
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
formData,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||
}
|
||||
|
||||
export const updateProfileJSON = ({ credentials, params }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_PROFILE_UPDATE_URL,
|
||||
credentials,
|
||||
payload: params,
|
||||
method: 'PATCH',
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: parseUser(data) }))
|
||||
|
||||
export const changeEmail = ({ credentials, email, password }) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('email', email)
|
||||
form.append('password', password)
|
||||
|
||||
return promisedRequest({
|
||||
url: CHANGE_EMAIL_URL,
|
||||
formData: form,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
})
|
||||
}
|
||||
|
||||
export const moveAccount = ({ credentials, password, targetAccount }) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('password', password)
|
||||
form.append('target_account', targetAccount)
|
||||
|
||||
return promisedRequest({
|
||||
url: MOVE_ACCOUNT_URL,
|
||||
formData: form,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
})
|
||||
}
|
||||
|
||||
export const changePassword = ({
|
||||
credentials,
|
||||
password,
|
||||
newPassword,
|
||||
newPasswordConfirmation,
|
||||
}) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('password', password)
|
||||
form.append('new_password', newPassword)
|
||||
form.append('new_password_confirmation', newPasswordConfirmation)
|
||||
|
||||
return promisedRequest({
|
||||
url: CHANGE_PASSWORD_URL,
|
||||
formData: form,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
})
|
||||
}
|
||||
|
||||
// #MFA
|
||||
export const settingsMFA = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: MFA_SETTINGS_URL,
|
||||
credentials,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
export const mfaDisableOTP = ({ credentials, password }) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('password', password)
|
||||
|
||||
return promisedRequest({
|
||||
url: MFA_DISABLE_OTP_URL,
|
||||
formData: form,
|
||||
method: 'DELETE',
|
||||
credentials,
|
||||
})
|
||||
}
|
||||
|
||||
export const mfaConfirmOTP = ({ credentials, password, token }) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('password', password)
|
||||
form.append('code', token)
|
||||
|
||||
return promisedRequest({
|
||||
url: MFA_CONFIRM_OTP_URL,
|
||||
formData: form,
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
export const mfaSetupOTP = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: MFA_SETUP_OTP_URL,
|
||||
credentials,
|
||||
method: 'GET',
|
||||
})
|
||||
export const generateMfaBackupCodes = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: MFA_BACKUP_CODES_URL,
|
||||
credentials,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
// #Aliases
|
||||
export const addAlias = ({ credentials, alias }) =>
|
||||
promisedRequest({
|
||||
url: ALIASES_URL,
|
||||
method: 'PUT',
|
||||
credentials,
|
||||
payload: { alias },
|
||||
})
|
||||
|
||||
export const deleteAlias = ({ credentials, alias }) =>
|
||||
promisedRequest({
|
||||
url: ALIASES_URL,
|
||||
method: 'DELETE',
|
||||
credentials,
|
||||
payload: { alias },
|
||||
})
|
||||
|
||||
export const listAliases = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: ALIASES_URL,
|
||||
method: 'GET',
|
||||
credentials,
|
||||
params: {
|
||||
_cacheBooster: new Date().getTime(),
|
||||
},
|
||||
})
|
||||
|
||||
// User manipulation
|
||||
export const fetchUserRelationship = ({ id, withSuspended, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_USER_RELATIONSHIPS_URL({ id, withSuspended }),
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const followUser = ({ id, credentials, ...options }) => {
|
||||
const payload = {}
|
||||
|
||||
if (options.reblogs !== undefined) {
|
||||
payload.reblogs = options.reblogs
|
||||
}
|
||||
|
||||
if (options.notify !== undefined) {
|
||||
payload.notify = options.notify
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: MASTODON_FOLLOW_URL(id),
|
||||
payload,
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export const unfollowUser = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_UNFOLLOW_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
export const fetchUserInLists = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_USER_IN_LISTS(id),
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const removeUserFromFollowers = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_REMOVE_USER_FROM_FOLLOWERS(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
export const fetchFollowRequests = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_FOLLOW_REQUESTS_URL,
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||
|
||||
export const approveUser = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_APPROVE_USER_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
export const denyUser = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_DENY_USER_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
export const editUserNote = ({ id, credentials, comment }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_USER_NOTE_URL(id),
|
||||
credentials,
|
||||
payload: {
|
||||
comment,
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
export const fetchMutes = ({ maxId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_USER_MUTES_URL({ maxId, withRelationships: true }),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||
|
||||
export const muteUser = ({ id, expiresIn, credentials }) => {
|
||||
const payload = {}
|
||||
if (expiresIn) {
|
||||
payload.expires_in = expiresIn
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: MASTODON_MUTE_USER_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
export const unmuteUser = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_UNMUTE_USER_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
export const fetchBlocks = ({ maxId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_USER_BLOCKS_URL({ maxId, withRelationships: true }),
|
||||
credentials,
|
||||
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
|
||||
|
||||
export const blockUser = ({ id, expiresIn, credentials }) => {
|
||||
const payload = {}
|
||||
if (expiresIn) {
|
||||
payload.duration = expiresIn
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: MASTODON_BLOCK_USER_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
export const unblockUser = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_UNBLOCK_USER_URL(id),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
export const reportUser = ({
|
||||
credentials,
|
||||
userId,
|
||||
statusIds,
|
||||
comment,
|
||||
forward,
|
||||
}) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_REPORT_USER_URL,
|
||||
method: 'POST',
|
||||
payload: {
|
||||
account_id: userId,
|
||||
status_ids: statusIds,
|
||||
comment,
|
||||
forward,
|
||||
},
|
||||
credentials,
|
||||
})
|
||||
|
||||
// #Domain mutes
|
||||
export const fetchDomainMutes = ({ credentials }) =>
|
||||
promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
|
||||
|
||||
export const muteDomain = ({ domain, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_DOMAIN_BLOCKS_URL,
|
||||
method: 'POST',
|
||||
payload: { domain },
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const unmuteDomain = ({ domain, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_DOMAIN_BLOCKS_URL,
|
||||
method: 'DELETE',
|
||||
payload: { domain },
|
||||
credentials,
|
||||
})
|
||||
|
||||
// #Backups
|
||||
export const addBackup = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_BACKUP_URL,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const listBackups = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_BACKUP_URL,
|
||||
method: 'GET',
|
||||
credentials,
|
||||
params: {
|
||||
_cacheBooster: new Date().getTime(),
|
||||
},
|
||||
})
|
||||
|
||||
// #OAuth
|
||||
export const fetchOAuthTokens = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: '/api/oauth_tokens.json',
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const revokeOAuthToken = ({ id, credentials }) =>
|
||||
promisedRequest({
|
||||
url: `/api/oauth_tokens/${id}`,
|
||||
credentials,
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
// #Lists
|
||||
export const fetchLists = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LIST_URL(),
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const createList = ({ title, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LIST_URL(),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload: { title },
|
||||
})
|
||||
|
||||
export const getList = ({ listId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LIST_URL(listId),
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const updateList = ({ listId, title, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LIST_URL(listId),
|
||||
|
||||
credentials,
|
||||
method: 'PUT',
|
||||
payload: { title },
|
||||
})
|
||||
|
||||
export const getListAccounts = ({ listId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LIST_ACCOUNTS_URL(listId),
|
||||
credentials,
|
||||
}).then((data) => data.map(({ id }) => id))
|
||||
|
||||
export const addAccountsToList = ({ listId, accountIds, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LIST_ACCOUNTS_URL(listId),
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload: { account_ids: accountIds },
|
||||
})
|
||||
|
||||
export const removeAccountsFromList = ({ listId, accountIds, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LIST_ACCOUNTS_URL(listId),
|
||||
credentials,
|
||||
method: 'DELETE',
|
||||
payload: { account_ids: accountIds },
|
||||
})
|
||||
|
||||
export const deleteList = ({ listId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: MASTODON_LIST_URL(listId),
|
||||
method: 'DELETE',
|
||||
credentials,
|
||||
})
|
||||
|
||||
// #Bookmarks
|
||||
export const fetchBookmarkFolders = ({ credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_BOOKMARK_FOLDERS_URL,
|
||||
credentials,
|
||||
})
|
||||
|
||||
export const createBookmarkFolder = ({ name, emoji, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_BOOKMARK_FOLDERS_URL,
|
||||
credentials,
|
||||
method: 'POST',
|
||||
payload: { name, emoji },
|
||||
})
|
||||
|
||||
export const updateBookmarkFolder = ({ folderId, name, emoji, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_BOOKMARK_FOLDER_URL(folderId),
|
||||
credentials,
|
||||
method: 'PATCH',
|
||||
payload: { name, emoji },
|
||||
})
|
||||
|
||||
export const deleteBookmarkFolder = ({ folderId, credentials }) =>
|
||||
promisedRequest({
|
||||
url: PLEROMA_BOOKMARK_FOLDER_URL(folderId),
|
||||
method: 'DELETE',
|
||||
credentials,
|
||||
})
|
||||
|
||||
// #So long and thanks for all the fish
|
||||
export const deleteAccount = ({ credentials, password }) => {
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('password', password)
|
||||
|
||||
return promisedRequest({
|
||||
url: DELETE_ACCOUNT_URL,
|
||||
formData,
|
||||
method: 'POST',
|
||||
credentials,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
import { paramsString } from './helpers.js'
|
||||
|
||||
import {
|
||||
parseChat,
|
||||
parseNotification,
|
||||
parseStatus,
|
||||
} from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
|
||||
const MASTODON_STREAMING = ({ accessToken, stream }) =>
|
||||
`/api/v1/streaming${paramsString({ accessToken, stream })}`
|
||||
|
||||
export const getMastodonSocketURI = ({ credentials, stream }) => {
|
||||
return MASTODON_STREAMING({ accessToken: credentials, stream })
|
||||
}
|
||||
|
||||
const MASTODON_STREAMING_EVENTS = new Set([
|
||||
'update',
|
||||
'notification',
|
||||
'delete',
|
||||
'filters_changed',
|
||||
'status.update',
|
||||
])
|
||||
|
||||
const PLEROMA_STREAMING_EVENTS = new Set([
|
||||
'pleroma:chat_update',
|
||||
'pleroma:respond',
|
||||
])
|
||||
|
||||
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
|
||||
// Uses EventTarget and a CustomEvent to proxy events
|
||||
export const ProcessedWS = ({
|
||||
url,
|
||||
preprocessor = handleMastoWS,
|
||||
id = 'Unknown',
|
||||
credentials,
|
||||
}) => {
|
||||
const eventTarget = new EventTarget()
|
||||
const socket = new WebSocket(url)
|
||||
if (!socket) throw new Error(`Failed to create socket ${id}`)
|
||||
const proxy = (original, eventName, processor = (a) => a) => {
|
||||
original.addEventListener(eventName, (eventData) => {
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent(eventName, { detail: processor(eventData) }),
|
||||
)
|
||||
})
|
||||
}
|
||||
socket.addEventListener('open', (wsEvent) => {
|
||||
console.debug(`[WS][${id}] Socket connected`, wsEvent)
|
||||
if (credentials) {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: 'pleroma:authenticate',
|
||||
token: credentials,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
socket.addEventListener('error', (wsEvent) => {
|
||||
console.debug(`[WS][${id}] Socket errored`, wsEvent)
|
||||
})
|
||||
socket.addEventListener('close', (wsEvent) => {
|
||||
console.debug(
|
||||
`[WS][${id}] Socket disconnected with code ${wsEvent.code}`,
|
||||
wsEvent,
|
||||
)
|
||||
})
|
||||
// Commented code reason: very spammy, uncomment to enable message debug logging
|
||||
/*
|
||||
socket.addEventListener('message', (wsEvent) => {
|
||||
console.debug(
|
||||
`[WS][${id}] Message received`,
|
||||
wsEvent
|
||||
)
|
||||
})
|
||||
/**/
|
||||
|
||||
const onAuthenticated = () => {
|
||||
eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated'))
|
||||
}
|
||||
|
||||
proxy(socket, 'open')
|
||||
proxy(socket, 'close')
|
||||
proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated }))
|
||||
proxy(socket, 'error')
|
||||
|
||||
// 1000 = Normal Closure
|
||||
eventTarget.close = () => {
|
||||
socket.close(1000, 'Shutting down socket')
|
||||
}
|
||||
eventTarget.getState = () => socket.readyState
|
||||
eventTarget.subscribe = (stream, args = {}) => {
|
||||
console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args)
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: 'subscribe',
|
||||
stream,
|
||||
...args,
|
||||
}),
|
||||
)
|
||||
}
|
||||
eventTarget.unsubscribe = (stream, args = {}) => {
|
||||
console.debug(
|
||||
`[WS][${id}] Unsubscribing from stream ${stream} with args`,
|
||||
args,
|
||||
)
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: 'unsubscribe',
|
||||
stream,
|
||||
...args,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return eventTarget
|
||||
}
|
||||
|
||||
export const handleMastoWS = (
|
||||
wsEvent,
|
||||
{
|
||||
onAuthenticated = () => {
|
||||
/* no-op */
|
||||
},
|
||||
} = {},
|
||||
) => {
|
||||
const { data } = wsEvent
|
||||
if (!data) return
|
||||
const parsedEvent = JSON.parse(data)
|
||||
const { event, payload } = parsedEvent
|
||||
if (
|
||||
MASTODON_STREAMING_EVENTS.has(event) ||
|
||||
PLEROMA_STREAMING_EVENTS.has(event)
|
||||
) {
|
||||
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
|
||||
if (event === 'delete') {
|
||||
return { event, id: payload }
|
||||
}
|
||||
const data = payload ? JSON.parse(payload) : null
|
||||
if (event === 'pleroma:respond') {
|
||||
if (data.type === 'pleroma:authenticate') {
|
||||
if (data.result === 'success') {
|
||||
console.debug('[WS] Successfully authenticated')
|
||||
onAuthenticated()
|
||||
} else {
|
||||
if (data.error === 'already_authenticated') {
|
||||
onAuthenticated()
|
||||
} else {
|
||||
console.error('[WS] Unable to authenticate:', data.error)
|
||||
wsEvent.target.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
} else if (event === 'update') {
|
||||
return { event, status: parseStatus(data) }
|
||||
} else if (event === 'status.update') {
|
||||
return { event, status: parseStatus(data) }
|
||||
} else if (event === 'notification') {
|
||||
return { event, notification: parseNotification(data) }
|
||||
} else if (event === 'pleroma:chat_update') {
|
||||
return { event, chatUpdate: parseChat(data) }
|
||||
}
|
||||
} else {
|
||||
console.warn('Unknown event', wsEvent)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const WSConnectionStatus = Object.freeze({
|
||||
JOINED: 1,
|
||||
CLOSED: 2,
|
||||
ERROR: 3,
|
||||
DISABLED: 4,
|
||||
STARTING: 5,
|
||||
STARTING_INITIAL: 6,
|
||||
})
|
||||
|
|
@ -1,51 +1,29 @@
|
|||
/* global process */
|
||||
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import Status from 'src/components/status/status.vue'
|
||||
import StillImage from 'src/components/still-image/still-image.vue'
|
||||
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeLayers,
|
||||
} from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
config.autoAddCss = false
|
||||
|
||||
import App from '../App.vue'
|
||||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||
import { applyStyleConfig } from '../services/style_setter/style_setter.js'
|
||||
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
||||
import {
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
} from '../services/window_utils/window_utils'
|
||||
import routes from './routes'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
import { useI18nStore } from 'src/stores/i18n'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
|
||||
|
||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||
import {
|
||||
INSTANCE_DEFAULT_CONFIG_DEFINITIONS,
|
||||
INSTANCE_IDENTITY_DEFAULT_DEFINITIONS,
|
||||
INSTANCE_IDENTIY_EXTERNAL,
|
||||
} from 'src/modules/default_config_state.js'
|
||||
|
||||
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { applyConfig } from '../services/style_setter/style_setter.js'
|
||||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
||||
|
||||
import { useOAuthStore } from 'src/stores/oauth'
|
||||
import { useI18nStore } from 'src/stores/i18n'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
|
||||
let staticInitialResults = null
|
||||
|
||||
|
|
@ -54,9 +32,7 @@ const parsedInitialResults = () => {
|
|||
return null
|
||||
}
|
||||
if (!staticInitialResults) {
|
||||
staticInitialResults = JSON.parse(
|
||||
document.getElementById('initial-results').textContent,
|
||||
)
|
||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||
}
|
||||
return staticInitialResults
|
||||
}
|
||||
|
|
@ -78,7 +54,7 @@ const preloadFetch = async (request) => {
|
|||
return {
|
||||
ok: true,
|
||||
json: () => requestData,
|
||||
text: () => requestData,
|
||||
text: () => requestData
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,38 +63,20 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const res = await preloadFetch('/api/v1/instance')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const textLimit = data.max_toot_chars
|
||||
const textlimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaExtensionsAvailable',
|
||||
data.pleroma,
|
||||
)
|
||||
useInstanceStore().set({
|
||||
path: 'limits.textLimit',
|
||||
value: textLimit,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'accountApprovalRequired',
|
||||
value: data.approval_required,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'birthdayRequired',
|
||||
value: !!data.pleroma?.metadata.birthday_required,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'birthdayMinAge',
|
||||
value: data.pleroma?.metadata.birthday_min_age || 0,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaExtensionsAvailable', value: data.pleroma })
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma?.metadata.birthday_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma?.metadata.birthday_min_age || 0 })
|
||||
|
||||
if (vapidPublicKey) {
|
||||
useInstanceStore().set({
|
||||
path: 'vapidPublicKey',
|
||||
value: vapidPublicKey,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
}
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load instance config, potentially fatal')
|
||||
|
|
@ -135,12 +93,10 @@ const getBackendProvidedConfig = async () => {
|
|||
const data = await res.json()
|
||||
return data.pleroma_fe
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Could not load backend-provided frontend config, potentially fatal',
|
||||
)
|
||||
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -151,13 +107,11 @@ const getStaticConfig = async () => {
|
|||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to load static/config.json, continuing without it.',
|
||||
error,
|
||||
)
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
console.warn(error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,25 +129,51 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
config = Object.assign({}, staticConfig, apiConfig)
|
||||
}
|
||||
|
||||
Object.keys(INSTANCE_IDENTITY_DEFAULT_DEFINITIONS).forEach((source) => {
|
||||
if (source === 'name') return
|
||||
if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return
|
||||
useInstanceStore().set({
|
||||
value:
|
||||
config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default,
|
||||
path: `instanceIdentity.${source}`,
|
||||
})
|
||||
const copyInstanceOption = (name) => {
|
||||
store.dispatch('setInstanceOption', { name, value: config[name] })
|
||||
}
|
||||
|
||||
copyInstanceOption('theme')
|
||||
copyInstanceOption('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_DEFAULT_CONFIG_DEFINITIONS).forEach((source) =>
|
||||
useInstanceStore().set({
|
||||
value:
|
||||
config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
|
||||
path: `prefsStorage.${source}`,
|
||||
}),
|
||||
)
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMargin',
|
||||
value: typeof config.logoMargin === 'undefined'
|
||||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
useAuthFlowStore().setInitialStrategy(config.loginMethod)
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
copyInstanceOption('showInstanceSpecificPanel')
|
||||
copyInstanceOption('minimalScopesMode')
|
||||
copyInstanceOption('hideMutedPosts')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('sidebarRight')
|
||||
}
|
||||
|
||||
const getTOS = async ({ store }) => {
|
||||
|
|
@ -201,9 +181,9 @@ const getTOS = async ({ store }) => {
|
|||
const res = await window.fetch('/static/terms-of-service.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
useInstanceStore().set({ path: 'instanceIdentity.tos', value: html })
|
||||
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load TOS\n", e)
|
||||
|
|
@ -215,12 +195,9 @@ const getInstancePanel = async ({ store }) => {
|
|||
const res = await preloadFetch('/instance/panel.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
useInstanceStore().set({
|
||||
path: 'instanceIdentity.instanceSpecificPanelContent',
|
||||
value: html,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load instance panel\n", e)
|
||||
|
|
@ -232,39 +209,41 @@ const getStickers = async ({ store }) => {
|
|||
const res = await window.fetch('/static/stickers.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const stickers = (
|
||||
await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta,
|
||||
}
|
||||
}),
|
||||
)
|
||||
).sort((a, b) => {
|
||||
const stickers = (await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
return a.meta.title.localeCompare(b.meta.title)
|
||||
})
|
||||
useEmojiStore().setStickers(stickers)
|
||||
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load stickers\n", e)
|
||||
}
|
||||
}
|
||||
|
||||
const getAppSecret = async ({ store }) => {
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.userToken) {
|
||||
store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||
useInstanceStore().set({
|
||||
path: 'staffAccounts',
|
||||
value: nicknames,
|
||||
})
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
}
|
||||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
|
|
@ -275,187 +254,97 @@ const getNodeInfo = async ({ store }) => {
|
|||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
const features = metadata.features
|
||||
useInstanceStore().set({
|
||||
path: 'instanceIdentity.name',
|
||||
value: metadata.nodeName,
|
||||
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pleromaCustomEmojiReactionsAvailable',
|
||||
value:
|
||||
features.includes('pleroma_custom_emoji_reactions') ||
|
||||
features.includes('custom_emoji_reactions')
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'registrationOpen',
|
||||
value: data.openRegistrations,
|
||||
})
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'mediaProxyAvailable',
|
||||
features.includes('media_proxy'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'safeDM',
|
||||
features.includes('safe_dm_mentions'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'shoutAvailable',
|
||||
features.includes('chat'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaChatMessagesAvailable',
|
||||
features.includes('pleroma_chat_messages'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaCustomEmojiReactionsAvailable',
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
|
||||
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
|
||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] })
|
||||
|
||||
features.includes('pleroma_custom_emoji_reactions') ||
|
||||
features.includes('custom_emoji_reactions'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaBookmarkFoldersAvailable',
|
||||
features.includes('pleroma:bookmark_folders'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'gopherAvailable',
|
||||
features.includes('gopher'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pollsAvailable',
|
||||
features.includes('polls'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'editingAvailable',
|
||||
features.includes('editing'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'mailerEnabled',
|
||||
metadata.mailerEnabled,
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'quotingAvailable',
|
||||
features.includes('quote_posting'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'groupActorAvailable',
|
||||
features.includes('pleroma:group_actors'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'blockExpiration',
|
||||
features.includes('pleroma:block_expiration'),
|
||||
)
|
||||
useInstanceStore().set({
|
||||
path: 'localBubbleInstances',
|
||||
value: metadata.localBubbleInstances ?? [],
|
||||
})
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'localBubble',
|
||||
(metadata.localBubbleInstances ?? []).length > 0,
|
||||
)
|
||||
|
||||
useInstanceStore().set({
|
||||
path: 'limits.pollLimits',
|
||||
value: metadata.pollLimits,
|
||||
})
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
useInstanceStore().set({
|
||||
path: 'limits.uploadlimit',
|
||||
value: parseInt(uploadLimits.general),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.avatarlimit',
|
||||
value: parseInt(uploadLimits.avatar),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.backgroundlimit',
|
||||
value: parseInt(uploadLimits.background),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.bannerlimit',
|
||||
value: parseInt(uploadLimits.banner),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.fieldsLimits',
|
||||
value: metadata.fieldsLimits,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
||||
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
||||
|
||||
useInstanceStore().set({
|
||||
path: 'restrictedNicknames',
|
||||
value: metadata.restrictedNicknames,
|
||||
})
|
||||
useInstanceCapabilitiesStore().set('postFormats', metadata.postFormats)
|
||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||
|
||||
const suggestions = metadata.suggestions
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'suggestionsEnabled',
|
||||
suggestions.enabled,
|
||||
)
|
||||
// this is unused, why?
|
||||
useInstanceCapabilitiesStore().set('suggestionsWeb', suggestions.web)
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
||||
|
||||
const software = data.software
|
||||
useInstanceStore().set({
|
||||
path: 'backendVersion',
|
||||
value: software.version,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'backendRepository',
|
||||
value: software.repository,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository })
|
||||
|
||||
const priv = metadata.private
|
||||
useInstanceStore().set({ path: 'privateMode', value: priv })
|
||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
useInstanceStore().set({
|
||||
path: 'frontendVersion',
|
||||
value: frontendVersion,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
|
||||
const federation = metadata.federation
|
||||
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'tagPolicyAvailable',
|
||||
typeof federation.mrf_policies === 'undefined'
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'tagPolicyAvailable',
|
||||
value: typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy'),
|
||||
)
|
||||
|
||||
useInstanceStore().set({
|
||||
path: 'federationPolicy',
|
||||
value: federation,
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'federating',
|
||||
value:
|
||||
typeof federation.enabled === 'undefined' ? true : federation.enabled,
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
value: typeof federation.enabled === 'undefined'
|
||||
? true
|
||||
: federation.enabled
|
||||
})
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
useInstanceStore().set({
|
||||
path: 'accountActivationRequired',
|
||||
value: accountActivationRequired,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load nodeinfo', e)
|
||||
console.warn('Could not load nodeinfo')
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
const setConfig = async ({ store }) => {
|
||||
// apiConfig, staticConfig
|
||||
const configInfos = await Promise.all([
|
||||
getBackendProvidedConfig({ store }),
|
||||
getStaticConfig(),
|
||||
])
|
||||
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
getAppSecret({ store })
|
||||
await setSettings({ store, apiConfig, staticConfig })
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.userToken) {
|
||||
return store.dispatch('loginUser', oauth.userToken)
|
||||
if (oauth.getUserToken) {
|
||||
return store.dispatch('loginUser', oauth.getUserToken)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
|
@ -467,16 +356,6 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
|
||||
app.use(pinia)
|
||||
|
||||
app.config.errorHandler = (error, instance, info) => {
|
||||
console.error(
|
||||
'Global Vue Error Handler caught an error:',
|
||||
error,
|
||||
instance,
|
||||
info,
|
||||
)
|
||||
useInterfaceStore().setGlobalError({ error, instance, info })
|
||||
}
|
||||
|
||||
const waitForAllStoresToLoad = async () => {
|
||||
// the stores that do not persist technically do not need to be awaited here,
|
||||
// but that involves either hard-coding the stores in some place (prone to errors)
|
||||
|
|
@ -485,37 +364,29 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
if (process.env.NODE_ENV === 'development') {
|
||||
// do some checks to avoid common errors
|
||||
if (!Object.keys(allStores).length) {
|
||||
throw new Error(
|
||||
'No stores are available. Check the code in src/boot/after_store.js',
|
||||
)
|
||||
throw new Error('No stores are available. Check the code in src/boot/after_store.js')
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Object.entries(allStores).map(async ([name, mod]) => {
|
||||
const isStoreName = (name) => name.startsWith('use')
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (Object.keys(mod).filter(isStoreName).length !== 1) {
|
||||
throw new Error(
|
||||
'Each store file must export exactly one store as a named export. Check your code in src/stores/',
|
||||
)
|
||||
Object.entries(allStores)
|
||||
.map(async ([name, mod]) => {
|
||||
const isStoreName = name => name.startsWith('use')
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (Object.keys(mod).filter(isStoreName).length !== 1) {
|
||||
throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/')
|
||||
}
|
||||
}
|
||||
}
|
||||
const storeFuncName = Object.keys(mod).find(isStoreName)
|
||||
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
||||
const p = mod[storeFuncName]().$persistLoaded
|
||||
if (!(p instanceof Promise)) {
|
||||
throw new Error(
|
||||
`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`,
|
||||
)
|
||||
const storeFuncName = Object.keys(mod).find(isStoreName)
|
||||
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
||||
const p = mod[storeFuncName]().$persistLoaded
|
||||
if (!(p instanceof Promise)) {
|
||||
throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`)
|
||||
}
|
||||
await p
|
||||
} else {
|
||||
throw new Error(`Store module ${name} does not export a 'use...' function`)
|
||||
}
|
||||
await p
|
||||
} else {
|
||||
throw new Error(
|
||||
`Store module ${name} does not export a 'use...' function`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -526,45 +397,30 @@ 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
|
||||
useInstanceStore().set({ path: 'server', value: server })
|
||||
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
|
||||
await setConfig({ store })
|
||||
try {
|
||||
await useInterfaceStore()
|
||||
.applyTheme()
|
||||
.catch((e) => {
|
||||
console.error('Error setting theme', e)
|
||||
})
|
||||
await useInterfaceStore().applyTheme().catch((e) => { console.error('Error setting theme', e) })
|
||||
} catch (e) {
|
||||
window.splashError(e)
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
applyStyleConfig(useMergedConfigStore().mergedConfig, i18n.global)
|
||||
applyConfig(store.state.config, i18n.global)
|
||||
|
||||
// Now we can try getting the server settings and logging in
|
||||
// Most of these are preloaded into the index.html so blocking is minimized
|
||||
|
|
@ -572,9 +428,13 @@ 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')
|
||||
store.dispatch('loadDrafts')
|
||||
useAnnouncementsStore().startFetchingAnnouncements()
|
||||
getTOS({ store })
|
||||
getStickers({ store })
|
||||
|
||||
|
|
@ -582,11 +442,11 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
history: createWebHistory(),
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some((m) => m.meta.dontScroll)) {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
return false
|
||||
}
|
||||
return savedPosition || { left: 0, top: 0 }
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
useI18nStore().setI18n(i18n)
|
||||
|
|
@ -608,9 +468,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,28 +1,42 @@
|
|||
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 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 NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
import PublicAndExternalTimeline from 'src/components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import PublicTimeline from 'src/components/public_timeline/public_timeline.vue'
|
||||
import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue'
|
||||
import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
|
||||
import Drafts from 'components/drafts/drafts.vue'
|
||||
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
|
||||
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
if (store.state.users.currentUser) {
|
||||
next()
|
||||
} else {
|
||||
next(
|
||||
useInstanceStore().instanceIdentity.redirectRootNoLogin || '/main/all',
|
||||
)
|
||||
next(store.state.instance.redirectRootNoLogin || '/main/all')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -31,283 +45,64 @@ export default (store) => {
|
|||
name: 'root',
|
||||
path: '/',
|
||||
redirect: () => {
|
||||
return (
|
||||
(store.state.users.currentUser
|
||||
? useInstanceStore().instanceIdentity.redirectRootLogin
|
||||
: useInstanceStore().instanceIdentity.redirectRootNoLogin) ||
|
||||
'/main/all'
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'public-external-timeline',
|
||||
path: '/main/all',
|
||||
component: PublicAndExternalTimeline,
|
||||
},
|
||||
{
|
||||
name: 'public-timeline',
|
||||
path: '/main/public',
|
||||
component: PublicTimeline,
|
||||
},
|
||||
{
|
||||
name: 'friends',
|
||||
path: '/main/friends',
|
||||
component: FriendsTimeline,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
return (store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
}
|
||||
},
|
||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'bubble', path: '/bubble', component: BubbleTimeline },
|
||||
{
|
||||
name: 'conversation',
|
||||
path: '/notice/:id',
|
||||
component: ConversationPage,
|
||||
meta: { dontScroll: true },
|
||||
},
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
|
||||
{
|
||||
name: 'remote-user-profile-acct',
|
||||
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'remote-user-profile',
|
||||
path: '/remote-users/:hostname/:username',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'external-user-profile',
|
||||
path: '/users/$:id',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/user_profile/user_profile.vue'),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'user-profile-admin-view',
|
||||
path: '/users/$:id/admin_view',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/user_profile/user_profile_admin_view.vue'),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'interactions',
|
||||
path: '/users/:username/interactions',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/interactions/interactions.vue'),
|
||||
),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'dms',
|
||||
path: '/users/:username/dms',
|
||||
component: DMs,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'registration',
|
||||
path: '/registration',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/registration/registration.vue'),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'password-reset',
|
||||
path: '/password-reset',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/password_reset/password_reset.vue'),
|
||||
),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'registration-token',
|
||||
path: '/registration/:token',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/registration/registration.vue'),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'friend-requests',
|
||||
path: '/friend-requests',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/follow_requests/follow_requests.vue'),
|
||||
),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
path: '/:username/notifications',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/notifications/notifications.vue'),
|
||||
),
|
||||
props: () => ({ disableTeleport: true }),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
component: 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'
|
||||
),
|
||||
),
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{ name: '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 (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
|
||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||
routes = routes.concat([
|
||||
{
|
||||
name: 'chat',
|
||||
path: '/users/:username/chats/:recipient_id',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/chat/chat.vue'),
|
||||
),
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'chats',
|
||||
path: '/users/:username/chats',
|
||||
component: defineAsyncComponent(
|
||||
() => import('src/components/chat_list/chat_list.vue'),
|
||||
),
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,8 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import FeaturesPanel from 'src/components/features_panel/features_panel.vue'
|
||||
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import MRFTransparencyPanel from 'src/components/mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import StaffPanel from 'src/components/staff_panel/staff_panel.vue'
|
||||
import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
const pleromaFeCommitUrl =
|
||||
'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
|
||||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
|
||||
const About = {
|
||||
components: {
|
||||
|
|
@ -18,28 +10,16 @@ const About = {
|
|||
FeaturesPanel,
|
||||
TermsOfServicePanel,
|
||||
StaffPanel,
|
||||
MRFTransparencyPanel,
|
||||
MRFTransparencyPanel
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel() {
|
||||
return useInstanceStore().instanceIdentity.showFeaturesPanel
|
||||
},
|
||||
frontendVersionLink() {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
...mapState(useInstanceStore, [
|
||||
'backendVersion',
|
||||
'backendRepository',
|
||||
'frontendVersion',
|
||||
]),
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&
|
||||
!useMergedConfigStore().mergedConfig.hideISP &&
|
||||
useInstanceStore().instanceIdentity.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default About
|
||||
|
|
|
|||
|
|
@ -1,47 +1,11 @@
|
|||
<template>
|
||||
<div class="About column-inner">
|
||||
<div class="column-inner">
|
||||
<instance-specific-panel v-if="showInstanceSpecificPanel" />
|
||||
<staff-panel />
|
||||
<terms-of-service-panel />
|
||||
<MRFTransparencyPanel />
|
||||
<features-panel v-if="showFeaturesPanel" />
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('settings.version.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl>
|
||||
<dt>{{ $t('settings.version.backend_version') }}</dt>
|
||||
<dd>
|
||||
<a
|
||||
:href="backendRepository"
|
||||
target="_blank"
|
||||
>
|
||||
{{ backendVersion }}
|
||||
</a>
|
||||
</dd>
|
||||
<dt>{{ $t('settings.version.frontend_version') }}</dt>
|
||||
<dd>
|
||||
<a
|
||||
:href="frontendVersionLink"
|
||||
target="_blank"
|
||||
>
|
||||
{{ frontendVersion }}
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./about.js"></script>
|
||||
<style>
|
||||
.About {
|
||||
dl {
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,111 +1,99 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisV
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useReportsStore } from 'src/stores/reports'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisV)
|
||||
library.add(
|
||||
faEllipsisV
|
||||
)
|
||||
|
||||
const AccountActions = {
|
||||
props: ['user', 'relationship'],
|
||||
data() {
|
||||
props: [
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingConfirmBlock: false,
|
||||
showingConfirmRemoveFollower: false,
|
||||
showingConfirmRemoveFollower: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover,
|
||||
UserListMenu,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
UserTimedFilterModal: defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
),
|
||||
),
|
||||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
showConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
hideConfirmBlock() {
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats() {
|
||||
showConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
showRepeats () {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
hideRepeats() {
|
||||
hideRepeats () {
|
||||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser() {
|
||||
if (this.$refs.timedBlockDialog) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
blockUser () {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showingConfirmBlock = true
|
||||
}
|
||||
this.showConfirmBlock()
|
||||
}
|
||||
},
|
||||
doBlockUser() {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
doBlockUser () {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser() {
|
||||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers() {
|
||||
removeUserFromFollowers () {
|
||||
if (!this.shouldConfirmRemoveUserFromFollowers) {
|
||||
this.doRemoveUserFromFollowers()
|
||||
} else {
|
||||
this.showConfirmRemoveUserFromFollowers()
|
||||
}
|
||||
},
|
||||
doRemoveUserFromFollowers() {
|
||||
doRemoveUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
this.hideConfirmRemoveUserFromFollowers()
|
||||
},
|
||||
reportUser() {
|
||||
reportUser () {
|
||||
useReportsStore().openUserReportingModal({ userId: this.user.id })
|
||||
},
|
||||
openChat() {
|
||||
openChat () {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: {
|
||||
username: this.$store.state.users.currentUser.screen_name,
|
||||
recipient_id: this.user.id,
|
||||
},
|
||||
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock() {
|
||||
return useMergedConfigStore().mergedConfig.modalOnBlock
|
||||
shouldConfirmBlock () {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
shouldConfirmRemoveUserFromFollowers() {
|
||||
return useMergedConfigStore().mergedConfig.modalOnRemoveUserFromFollowers
|
||||
shouldConfirmRemoveUserFromFollowers () {
|
||||
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
|
||||
},
|
||||
...mapState(useInstanceCapabilitiesStore, [
|
||||
'blockExpiration',
|
||||
'pleromaChatMessagesAvailable',
|
||||
]),
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountActions
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template #content>
|
||||
|
|
@ -94,9 +95,8 @@
|
|||
</template>
|
||||
</Popover>
|
||||
<teleport to="#modal">
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmBlock && !blockExpiration"
|
||||
ref="blockDialog"
|
||||
<confirm-modal
|
||||
v-if="showingConfirmBlock"
|
||||
:title="$t('user_card.block_confirm_title')"
|
||||
:confirm-text="$t('user_card.block_confirm_accept_button')"
|
||||
:cancel-text="$t('user_card.block_confirm_cancel_button')"
|
||||
|
|
@ -114,10 +114,10 @@
|
|||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</ConfirmModal>
|
||||
</confirm-modal>
|
||||
</teleport>
|
||||
<teleport to="#modal">
|
||||
<ConfirmModal
|
||||
<confirm-modal
|
||||
v-if="showingConfirmRemoveFollower"
|
||||
:title="$t('user_card.remove_follower_confirm_title')"
|
||||
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
|
||||
|
|
@ -136,13 +136,7 @@
|
|||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</ConfirmModal>
|
||||
<UserTimedFilterModal
|
||||
v-if="blockExpiration"
|
||||
ref="timedBlockDialog"
|
||||
:is-mute="false"
|
||||
:user="user"
|
||||
/>
|
||||
</confirm-modal>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,58 +1,57 @@
|
|||
export default {
|
||||
name: 'Alert',
|
||||
selector: '.alert',
|
||||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Link',
|
||||
'Border',
|
||||
'ButtonUnstyled'
|
||||
],
|
||||
variants: {
|
||||
normal: '.neutral',
|
||||
info: '.info',
|
||||
error: '.error',
|
||||
warning: '.warning',
|
||||
success: '.success',
|
||||
success: '.success'
|
||||
},
|
||||
editor: {
|
||||
border: 1,
|
||||
aspect: '3 / 1',
|
||||
aspect: '3 / 1'
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--text',
|
||||
opacity: 0.5,
|
||||
blur: '9px',
|
||||
},
|
||||
blur: '9px'
|
||||
}
|
||||
},
|
||||
{
|
||||
parent: {
|
||||
component: 'Alert',
|
||||
component: 'Alert'
|
||||
},
|
||||
component: 'Border',
|
||||
directives: {
|
||||
textColor: '--parent',
|
||||
},
|
||||
textColor: '--parent'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'error',
|
||||
directives: {
|
||||
background: '--cRed',
|
||||
},
|
||||
background: '--cRed'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'warning',
|
||||
directives: {
|
||||
background: '--cOrange',
|
||||
},
|
||||
background: '--cOrange'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'success',
|
||||
directives: {
|
||||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'info',
|
||||
directives: {
|
||||
background: '--cBlue',
|
||||
},
|
||||
},
|
||||
],
|
||||
background: '--cGreen'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,126 +1,109 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import RichContent from '../rich_content/rich_content.jsx'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: undefined,
|
||||
allDay: undefined
|
||||
},
|
||||
editError: '',
|
||||
editError: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
announcement: Object
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
canEditAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.has('announcements_manage_announcements')
|
||||
)
|
||||
canEditAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
},
|
||||
content() {
|
||||
content () {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead() {
|
||||
isRead () {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt() {
|
||||
publishedAt () {
|
||||
const time = this.announcement.published_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
startsAt() {
|
||||
startsAt () {
|
||||
const time = this.announcement.starts_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
endsAt() {
|
||||
endsAt () {
|
||||
const time = this.announcement.ends_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
inactive() {
|
||||
inactive () {
|
||||
return this.announcement.inactive
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsRead() {
|
||||
markAsRead () {
|
||||
if (!this.isRead) {
|
||||
return useAnnouncementsStore().markAnnouncementAsRead(
|
||||
this.announcement.id,
|
||||
)
|
||||
return 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
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
Checkbox,
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
disabled: Boolean,
|
||||
},
|
||||
disabled: Boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementEditor
|
||||
|
|
|
|||
|
|
@ -1,65 +1,59 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import Announcement from 'src/components/announcement/announcement.vue'
|
||||
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
const AnnouncementsPage = {
|
||||
components: {
|
||||
Announcement,
|
||||
AnnouncementEditor,
|
||||
AnnouncementEditor
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: false,
|
||||
allDay: false
|
||||
},
|
||||
posting: false,
|
||||
error: undefined,
|
||||
error: undefined
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
mounted () {
|
||||
useAnnouncementsStore().fetchAnnouncements()
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
announcements() {
|
||||
announcements () {
|
||||
return useAnnouncementsStore().announcements
|
||||
},
|
||||
canPostAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.has('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
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
export default {
|
||||
emits: ['resetAsyncComponent'],
|
||||
methods: {
|
||||
retry() {
|
||||
retry () {
|
||||
this.$emit('resetAsyncComponent')
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
|
||||
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 fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAlignRight,
|
||||
faFile,
|
||||
faImage,
|
||||
faMusic,
|
||||
faPencilAlt,
|
||||
faPlayCircle,
|
||||
faSearchPlus,
|
||||
faStop,
|
||||
faTimes,
|
||||
faTrashAlt,
|
||||
faImage,
|
||||
faVideo,
|
||||
faPlayCircle,
|
||||
faTimes,
|
||||
faStop,
|
||||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
|
|
@ -35,7 +31,7 @@ library.add(
|
|||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight,
|
||||
faAlignRight
|
||||
)
|
||||
|
||||
const Attachment = {
|
||||
|
|
@ -50,75 +46,72 @@ const Attachment = {
|
|||
'remove',
|
||||
'shiftUp',
|
||||
'shiftDn',
|
||||
'edit',
|
||||
'edit'
|
||||
],
|
||||
emits: ['play', 'pause', 'naturalSizeLoad'],
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
nsfwImage:
|
||||
useInstanceStore().instanceIdentity.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: useMergedConfigStore().mergedConfig.hideNsfw,
|
||||
preloadImage: useMergedConfigStore().mergedConfig.preloadImage,
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img: this.attachment.type === 'image' && document.createElement('img'),
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
showDescription: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Flash: defineAsyncComponent(() => import('src/components/flash/flash.vue')),
|
||||
|
||||
VideoAttachment: defineAsyncComponent(
|
||||
() => import('src/components/video_attachment/video_attachment.vue'),
|
||||
),
|
||||
Popover,
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
classNames () {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
'-nsfw-placeholder': this.hidden,
|
||||
'-editable': this.edit !== undefined,
|
||||
'-compact': this.compact,
|
||||
'-compact': this.compact
|
||||
},
|
||||
'-type-' + this.attachment.type,
|
||||
'-type-' + this.type,
|
||||
this.size && '-size-' + this.size,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
]
|
||||
},
|
||||
usePlaceholder() {
|
||||
usePlaceholder () {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit() {
|
||||
return this.mergedConfig.useContainFit
|
||||
useContainFit () {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
placeholderName() {
|
||||
placeholderName () {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
return this.attachment.type.toUpperCase()
|
||||
return this.type.toUpperCase()
|
||||
}
|
||||
return this.attachment.description
|
||||
},
|
||||
placeholderIconClass() {
|
||||
if (this.attachment.type === 'image') return 'image'
|
||||
if (this.attachment.type === 'video') return 'video'
|
||||
if (this.attachment.type === 'audio') return 'music'
|
||||
placeholderIconClass () {
|
||||
if (this.type === 'image') return 'image'
|
||||
if (this.type === 'video') return 'video'
|
||||
if (this.type === 'audio') return 'music'
|
||||
return 'file'
|
||||
},
|
||||
referrerpolicy() {
|
||||
return useInstanceCapabilitiesStore().mediaProxyAvailable
|
||||
? ''
|
||||
: 'no-referrer'
|
||||
referrerpolicy () {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
hidden() {
|
||||
type () {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
},
|
||||
hidden () {
|
||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty() {
|
||||
return this.attachment.type === 'html' && !this.attachment.oembed
|
||||
isEmpty () {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
},
|
||||
useModal() {
|
||||
useModal () {
|
||||
let modalTypes = []
|
||||
switch (this.size) {
|
||||
case 'hide':
|
||||
|
|
@ -131,63 +124,64 @@ const Attachment = {
|
|||
: ['image']
|
||||
break
|
||||
}
|
||||
return modalTypes.includes(this.attachment.type)
|
||||
return modalTypes.includes(this.type)
|
||||
},
|
||||
videoTag() {
|
||||
videoTag () {
|
||||
return this.useModal ? 'button' : 'span'
|
||||
},
|
||||
...mapState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
watch: {
|
||||
'attachment.description'(newVal) {
|
||||
'attachment.description' (newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription(newVal) {
|
||||
localDescription (newVal) {
|
||||
this.onEdit(newVal)
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkClicked({ target }) {
|
||||
linkClicked ({ target }) {
|
||||
if (target.tagName === 'A') {
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
openModal() {
|
||||
openModal () {
|
||||
if (this.useModal) {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
} else if (this.attachment.type === 'unknown') {
|
||||
} else if (this.type === 'unknown') {
|
||||
window.open(this.attachment.url)
|
||||
}
|
||||
},
|
||||
openModalForce() {
|
||||
openModalForce () {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
},
|
||||
onEdit(event) {
|
||||
onEdit (event) {
|
||||
this.edit && this.edit(this.attachment, event)
|
||||
},
|
||||
onRemove() {
|
||||
onRemove () {
|
||||
this.remove && this.remove(this.attachment)
|
||||
},
|
||||
onShiftUp() {
|
||||
onShiftUp () {
|
||||
this.shiftUp && this.shiftUp(this.attachment)
|
||||
},
|
||||
onShiftDn() {
|
||||
onShiftDn () {
|
||||
this.shiftDn && this.shiftDn(this.attachment)
|
||||
},
|
||||
stopFlash() {
|
||||
stopFlash () {
|
||||
this.$refs.flash.closePlayer()
|
||||
},
|
||||
setFlashLoaded(event) {
|
||||
setFlashLoaded (event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleHidden(event) {
|
||||
toggleDescription () {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden (event) {
|
||||
if (
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
!this.showHidden &&
|
||||
(this.attachment.type !== 'video' ||
|
||||
this.mergedConfig.playVideosInModal)
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
return
|
||||
|
|
@ -207,12 +201,12 @@ const Attachment = {
|
|||
this.showHidden = !this.showHidden
|
||||
}
|
||||
},
|
||||
onImageLoad(image) {
|
||||
onImageLoad (image) {
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Attachment
|
||||
|
|
|
|||
|
|
@ -107,9 +107,9 @@
|
|||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 4.5em;
|
||||
top: calc(50% - 2.25rem);
|
||||
left: calc(50% - 2.25rem);
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
|
||||
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
width: 2em;
|
||||
height: 2em;
|
||||
margin-left: 0.5em;
|
||||
font-size: 1em;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,27 +265,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/components/attachment/attachment.style.js
Normal file
27
src/components/attachment/attachment.style.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export default {
|
||||
name: 'Attachment',
|
||||
selector: '.Attachment',
|
||||
notEditable: true,
|
||||
validInnerComponents: [
|
||||
'Border',
|
||||
'Button',
|
||||
'Input'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Button',
|
||||
parent: {
|
||||
component: 'Attachment'
|
||||
},
|
||||
directives: {
|
||||
background: '#FFFFFF',
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
@click="openModal"
|
||||
>
|
||||
<a
|
||||
v-if="attachment.type !== 'html'"
|
||||
v-if="type !== 'html'"
|
||||
class="placeholder"
|
||||
target="_blank"
|
||||
:href="attachment.url"
|
||||
|
|
@ -23,23 +23,28 @@
|
|||
>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-default attachment-button"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
<FAIcon icon="trash-alt" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && edit"
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
<textarea
|
||||
<input
|
||||
v-if="edit"
|
||||
v-model="localDescription"
|
||||
type="text"
|
||||
class="input description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
/>
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
|
|
@ -65,7 +70,7 @@
|
|||
:src="nsfwImage"
|
||||
>
|
||||
<FAIcon
|
||||
v-if="attachment.type === 'video'"
|
||||
v-if="type === 'video'"
|
||||
class="play-icon"
|
||||
icon="play-circle"
|
||||
/>
|
||||
|
|
@ -75,32 +80,24 @@
|
|||
class="attachment-buttons"
|
||||
>
|
||||
<button
|
||||
v-if="attachment.type === 'flash' && flashLoaded"
|
||||
class="button-default attachment-button -transparent"
|
||||
v-if="type === 'flash' && flashLoaded"
|
||||
class="button-default attachment-button"
|
||||
:title="$t('status.attachment_stop_flash')"
|
||||
@click.prevent="stopFlash"
|
||||
>
|
||||
<FAIcon icon="stop" />
|
||||
</button>
|
||||
<Popover
|
||||
v-if="attachment.description && size !== 'small' && !edit && attachment.type !== 'unknown'"
|
||||
trigger="click"
|
||||
popover-class="popover popover-default description-popover"
|
||||
:trigger-attrs="{ 'class': 'button-default attachment-button -transparent', 'title': $t('status.attachment_description') }"
|
||||
>
|
||||
<template #trigger>
|
||||
<FAIcon icon="align-right" />
|
||||
</template>
|
||||
<template #content>
|
||||
<details open>
|
||||
<summary>{{ $t('status.attachment_description') }}</summary>
|
||||
<span>{{ localDescription }}</span>
|
||||
</details>
|
||||
</template>
|
||||
</Popover>
|
||||
<button
|
||||
v-if="!useModal && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||
class="button-default attachment-button"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
>
|
||||
<FAIcon icon="align-right" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!useModal && type !== 'unknown'"
|
||||
class="button-default attachment-button"
|
||||
:title="$t('status.show_attachment_in_modal')"
|
||||
@click.prevent="openModalForce"
|
||||
>
|
||||
|
|
@ -108,7 +105,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="nsfw && hideNsfwLocal"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-default attachment-button"
|
||||
:title="$t('status.hide_attachment')"
|
||||
@click.prevent="toggleHidden"
|
||||
>
|
||||
|
|
@ -116,7 +113,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftUp"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-default attachment-button"
|
||||
:title="$t('status.move_up')"
|
||||
@click.prevent="onShiftUp"
|
||||
>
|
||||
|
|
@ -124,7 +121,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftDn"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-default attachment-button"
|
||||
:title="$t('status.move_down')"
|
||||
@click.prevent="onShiftDn"
|
||||
>
|
||||
|
|
@ -132,7 +129,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-default attachment-button"
|
||||
:title="$t('status.remove_attachment')"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
|
|
@ -141,7 +138,7 @@
|
|||
</div>
|
||||
|
||||
<a
|
||||
v-if="attachment.type === 'image' && (!hidden || preloadImage)"
|
||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||
class="image-container"
|
||||
:class="{'-hidden': hidden && preloadImage }"
|
||||
:href="attachment.url"
|
||||
|
|
@ -159,7 +156,7 @@
|
|||
</a>
|
||||
|
||||
<a
|
||||
v-if="attachment.type === 'unknown' && !hidden"
|
||||
v-if="type === 'unknown' && !hidden"
|
||||
class="placeholder-container"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
|
|
@ -176,7 +173,7 @@
|
|||
|
||||
<component
|
||||
:is="videoTag"
|
||||
v-if="attachment.type === 'video' && !hidden"
|
||||
v-if="type === 'video' && !hidden"
|
||||
class="video-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
|
|
@ -196,13 +193,13 @@
|
|||
</component>
|
||||
|
||||
<span
|
||||
v-if="attachment.type === 'audio' && !hidden"
|
||||
v-if="type === 'audio' && !hidden"
|
||||
class="audio-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
<audio
|
||||
v-if="attachment.type === 'audio'"
|
||||
v-if="type === 'audio'"
|
||||
:src="attachment.url"
|
||||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
|
|
@ -213,7 +210,7 @@
|
|||
</span>
|
||||
|
||||
<div
|
||||
v-if="attachment.type === 'html' && attachment.oembed"
|
||||
v-if="type === 'html' && attachment.oembed"
|
||||
class="oembed-container"
|
||||
@click.prevent="linkClicked"
|
||||
>
|
||||
|
|
@ -232,7 +229,7 @@
|
|||
</div>
|
||||
|
||||
<span
|
||||
v-if="attachment.type === 'flash' && !hidden"
|
||||
v-if="type === 'flash' && !hidden"
|
||||
class="flash-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
|
|
@ -247,16 +244,21 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && edit"
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
<textarea
|
||||
<input
|
||||
v-if="edit"
|
||||
v-model="localDescription"
|
||||
type="text"
|
||||
class="input description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
/>
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,28 @@
|
|||
import { mapState } from 'pinia'
|
||||
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'
|
||||
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'
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -2,55 +2,51 @@ 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
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
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,
|
||||
useInstanceStore().restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarList
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,24 @@
|
|||
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 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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
},
|
||||
showLineLabels: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
|
||||
UserLink,
|
||||
RichContent,
|
||||
UserLink
|
||||
},
|
||||
methods: {
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
useInstanceStore().restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
allowNonSquareEmoji() {
|
||||
return useMergedConfigStore().mergedConfig.nonSquareEmoji
|
||||
},
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BasicUserCard
|
||||
|
|
|
|||
|
|
@ -23,22 +23,13 @@
|
|||
:title="user.name"
|
||||
class="basic-user-card-user-name"
|
||||
>
|
||||
<strong v-if="showLineLabels">
|
||||
{{ $t('admin_dash.users.labels.name_colon') }}
|
||||
{{ ' ' }}
|
||||
</strong>
|
||||
<RichContent
|
||||
class="basic-user-card-user-name-value"
|
||||
:html="user.name"
|
||||
:emoji="user.emoji"
|
||||
:allow-non-square-emoji="allowNonSquareEmoji"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong v-if="showLineLabels">
|
||||
{{ $t('admin_dash.users.labels.handle_colon') }}
|
||||
{{ ' ' }}
|
||||
</strong>
|
||||
<user-link
|
||||
class="basic-user-card-screen-name"
|
||||
:user="user"
|
||||
|
|
@ -54,10 +45,8 @@
|
|||
<style lang="scss">
|
||||
.basic-user-card {
|
||||
display: flex;
|
||||
flex: 1 1 10em;
|
||||
min-width: 1em;
|
||||
flex: 1 0;
|
||||
margin: 0;
|
||||
line-height: 1.25;
|
||||
|
||||
--emoji-size: 1em;
|
||||
|
||||
|
|
@ -79,7 +68,7 @@
|
|||
|
||||
&-user-name-value,
|
||||
&-screen-name {
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue