Merge branch 'weight-removal' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-09-02 21:04:46 +03:00
commit f8ba217f0c
55 changed files with 2354 additions and 4109 deletions

View file

@ -1,223 +0,0 @@
# 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
stages:
- check-changelog
- lint
- build
- test
- deploy
# https://git.pleroma.social/help/ci/yaml/workflow.md#switch-between-branch-pipelines-and-merge-request-pipelines
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
check-changelog:
stage: check-changelog
image: alpine
rules:
- if: $CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == 'pleroma/pleroma-fe' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^renovate/
when: never
- if: $CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == 'pleroma/pleroma-fe' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == 'weblate'
when: never
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
before_script: ''
after_script: ''
cache: {}
script:
- apk add git
- sh ./tools/check-changelog
lint-eslint:
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
test:
stage: test
tags:
- amd64
- himem
variables:
APT_CACHE_DIR: apt-cache
script:
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
- yarn
- yarn playwright install firefox
- yarn playwright install-deps
- yarn unit-ci
artifacts:
# When the test fails, upload screenshots for better context on why it fails
paths:
- test/**/__screenshots__
when: on_failure
e2e-pleroma:
stage: test
image: mcr.microsoft.com/playwright:v1.61.0-jammy
services:
- name: postgres:15-alpine
alias: db
- name: $PLEROMA_IMAGE
alias: pleroma
entrypoint: ["/bin/ash", "-c"]
command:
- |
set -eu
SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded
CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs
echo '-- Waiting for database...'
while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do
sleep 1s
done
echo '-- Writing E2E config overrides...'
cat > $CONFIG_OVERRIDE_PATH <<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:
- amd64
- himem
script:
- yarn
- yarn build
artifacts:
paths:
- dist/
docs-deploy:
stage: deploy
image: alpine:latest
only:
- develop@pleroma/pleroma-fe
before_script:
- apk add curl
script:
- curl -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' https://git.pleroma.social/api/v4/projects/673/trigger/pipeline

View file

@ -14,7 +14,7 @@ labels:
steps: steps:
build: build:
image: docker.io/node:20-alpine image: docker.io/node:26-alpine
commands: commands:
- apk add --no-cache zip git - apk add --no-cache zip git
- yarn --frozen-lockfile - yarn --frozen-lockfile

View file

@ -11,7 +11,7 @@ labels:
steps: steps:
build: build:
image: docker.io/node:20-alpine image: docker.io/node:26-alpine
commands: commands:
- yarn --frozen-lockfile - yarn --frozen-lockfile
- yarn build - yarn build

View file

@ -9,7 +9,7 @@ when:
steps: steps:
install-depends: install-depends:
image: &node-image image: &node-image
docker.io/node:20-alpine docker.io/node:26-alpine
commands: commands:
- yarn --frozen-lockfile - yarn --frozen-lockfile

View file

@ -81,7 +81,7 @@ In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with
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/src/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters. For most part, PleromaFE tries to store all the info it can get in global pinia 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.
PleromaFE also tries to persist this store, however only stable data is stored, such as user authentication and preferences, user highlights. Persistence is performed by saving and loading chunk of vuex store in browser's LocalStorage/IndexedDB. PleromaFE also tries to persist this store, however only stable data is stored, such as user authentication and preferences, user highlights. Persistence is performed by saving and loading chunk of vuex store in browser's LocalStorage/IndexedDB.

View file

@ -1,29 +1,15 @@
import js from '@eslint/js'
import { defineConfig, globalIgnores } from 'eslint/config' import { defineConfig, globalIgnores } from 'eslint/config'
import vue from 'eslint-plugin-vue' import vue from 'eslint-plugin-vue'
import globals from 'globals'
export default defineConfig([ export default defineConfig([
...vue.configs['flat/recommended'], ...vue.configs['flat/recommended'],
globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']), globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']),
{ {
files: ['src/**/*.vue'], files: ['src/**/*.vue'],
plugins: { js },
extends: ['js/recommended'],
languageOptions: { languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
parserOptions: { parserOptions: {
parser: '@babel/eslint-parser', parser: '@babel/eslint-parser',
}, },
globals: {
...globals.browser,
...globals.vitest,
...globals.chai,
...globals.commonjs,
...globals.serviceworker,
},
}, },
rules: { rules: {

View file

@ -21,7 +21,7 @@
"lint-fix": "yarn exec eslint -- --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write" "lint-fix": "yarn exec eslint -- --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.4", "@babel/runtime": "^8.0.0",
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "7.1.0", "@fortawesome/fontawesome-svg-core": "7.1.0",
"@fortawesome/free-regular-svg-icons": "7.1.0", "@fortawesome/free-regular-svg-icons": "7.1.0",
@ -38,79 +38,58 @@
"click-outside-vue3": "4.0.1", "click-outside-vue3": "4.0.1",
"cropperjs": "2.0.1", "cropperjs": "2.0.1",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"globals": "^16.0.0",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"localforage": "1.10.0", "localforage": "1.10.0",
"lodash-es": "4.17.21",
"parse-link-header": "2.0.0", "parse-link-header": "2.0.0",
"phoenix": "1.8.1", "phoenix": "1.8.1",
"pinia": "^3.0.4", "pinia": "4.0.3",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"qrcode": "1.5.4", "qrcode": "1.5.4",
"querystring-es3": "0.2.1", "querystring-es3": "0.2.1",
"url": "0.11.4",
"utf8": "3.0.0", "utf8": "3.0.0",
"uuid": "11.1.0", "uuid": "11.1.0",
"vue": "3.5.22", "vue": "3.5.42",
"vue-i18n": "11", "vue-i18n": "11.4.0",
"vue-router": "4.6.4", "vue-router": "5.3.1",
"vue-virtual-scroller": "^2.0.0-beta.7", "vue-virtual-scroller": "^3.0.5"
"vuex": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.5", "@babel/core": "^8.0.0",
"@babel/eslint-parser": "7.28.5", "@babel/eslint-parser": "^8.0.0",
"@babel/plugin-transform-runtime": "7.28.5", "@babel/plugin-transform-runtime": "^8.0.0",
"@babel/preset-env": "7.28.5", "@babel/preset-env": "^8.0.0",
"@babel/register": "7.28.3", "@babel/register": "^8.0.0",
"@biomejs/biome": "2.3.11", "@biomejs/biome": "2.5.11",
"@pinia/testing": "1.0.3", "@pinia/testing": "1.0.3",
"@ungap/event-target": "0.2.4", "@ungap/event-target": "0.2.4",
"@vitejs/devtools": "^0.3.1", "@vitejs/devtools": "^0.3.1",
"@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue": "^6.0.8",
"@vitejs/plugin-vue-jsx": "^5.1.5", "@vitejs/plugin-vue-jsx": "^5.1.5",
"@vitest/browser": "^4.1.7", "@vitest/browser": "^4.1.11",
"@vitest/browser-playwright": "^4.1.7", "@vitest/browser-playwright": "^4.1.11",
"@vitest/coverage-v8": "^4.1.10", "@vitest/coverage-v8": "^4.1.11",
"@vitest/ui": "^4.1.7", "@vitest/ui": "^4.1.11",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-plugin-jsx": "3.0.0",
"@vue/babel-plugin-jsx": "1.5.0",
"@vue/compiler-sfc": "3.5.22", "@vue/compiler-sfc": "3.5.22",
"@vue/test-utils": "2.4.6", "@vue/devtools-api": "^8.1.5",
"@vue/test-utils": "2.5.0",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"chai": "5.3.3",
"chalk": "5.6.2", "chalk": "5.6.2",
"chromedriver": "135.0.4",
"connect-history-api-fallback": "2.0.0",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"custom-event-polyfill": "1.0.7",
"eslint": "9.39.2", "eslint": "9.39.2",
"eslint-config-standard": "17.1.0",
"eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.23.1",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-vue": "10.6.2", "eslint-plugin-vue": "10.6.2",
"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", "iso-639-1": "3.1.5",
"lodash-es": "4.17.21",
"msw": "2.14.6", "msw": "2.14.6",
"nightwatch": "3.12.2", "nightwatch": "3.12.2",
"oxc": "^1.0.1",
"playwright": "1.61.0", "playwright": "1.61.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6", "postcss-scss": "^4.0.6",
"sass-embedded": "^1.100.0", "sass-embedded": "^1.100.0",
"selenium-server": "3.141.59", "selenium-server": "3.141.59",
"semver": "7.7.3",
"serve-static": "2.2.0", "serve-static": "2.2.0",
"shelljs": "0.10.0",
"sinon": "20.0.0",
"sinon-chai": "4.0.1",
"stylelint": "16.25.0", "stylelint": "16.25.0",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^16.0.0", "stylelint-config-recommended": "^16.0.0",
@ -120,12 +99,13 @@
"vite": "^8.0.0", "vite": "^8.0.0",
"vite-plugin-eslint2": "^5.1.0", "vite-plugin-eslint2": "^5.1.0",
"vite-plugin-stylelint": "^6.1.0", "vite-plugin-stylelint": "^6.1.0",
"vitest": "^4.1.7", "vite-plugin-vue-devtools": "^8.2.1",
"vitest": "^4.1.11",
"vue-eslint-parser": "10.2.0" "vue-eslint-parser": "10.2.0"
}, },
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">= 16.0.0" "node": ">= 26.0.0"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View file

@ -17,8 +17,11 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account' const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
const ALIASES_URL = '/api/pleroma/aliases' const ALIASES_URL = '/api/pleroma/aliases'
const NOTIFICATION_SETTINGS_URL = ({ blockFromStrangers, hideNotificationContents }) => const NOTIFICATION_SETTINGS_URL = ({
`/api/pleroma/notification_settings${paramsString({ blockFromStrangers, hideNotificationContents })}` blockFromStrangers,
hideNotificationContents,
}) =>
`/api/pleroma/notification_settings${paramsString({ blockFromStrangers, hideNotificationContents })}`
export const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' export const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'

View file

@ -71,7 +71,6 @@ const chatNew = {
this.loading = true this.loading = true
this.userIds = [] this.userIds = []
this.$store
useSearchStore() useSearchStore()
.search({ q: query, resolve: true, type: 'accounts' }) .search({ q: query, resolve: true, type: 'accounts' })
.then((data) => { .then((data) => {

View file

@ -1,11 +1,10 @@
import { cloneDeep } from 'lodash'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Gallery from 'src/components/gallery/gallery.vue' import Gallery from 'src/components/gallery/gallery.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import StatusContent from 'src/components/status_content/status_content.vue' import StatusContent from 'src/components/status_content/status_content.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useDraftsStore } from 'src/stores/drafts.js'
import { useStatusesStore } from 'src/stores/statuses.js' import { useStatusesStore } from 'src/stores/statuses.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -33,7 +32,6 @@ const Draft = {
}, },
data() { data() {
return { return {
referenceDraft: cloneDeep(this.draft),
editing: false, editing: false,
showingConfirmDialog: false, showingConfirmDialog: false,
} }
@ -50,14 +48,6 @@ const Draft = {
return {} return {}
} }
}, },
safeToSave() {
return (
this.draft.status ||
this.draft.files?.length ||
this.draft.hasPoll ||
this.draft.hasQuote
)
},
postStatusFormProps() { postStatusFormProps() {
return { return {
draftId: this.draft.id, draftId: this.draft.id,
@ -69,18 +59,12 @@ const Draft = {
? useStatusesStore().allStatuses.get(this.draft.refId) ? useStatusesStore().allStatuses.get(this.draft.refId)
: undefined : undefined
}, },
localCollapseSubjectDefault() {
return useMergedConfigStore().mergedConfig.collapseMessageWithSubject
},
}, },
watch: { watch: {
editing(newVal) { editing(newVal) {
if (newVal) return if (newVal) return
if (this.safeToSave) { // (Post|Edit)StatusForm handles draft saving
this.$store.dispatch('addOrSaveDraft', { draft: this.draft }) this.$refs.form.saveDraft()
} else {
this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft })
}
}, },
}, },
methods: { methods: {
@ -91,9 +75,11 @@ const Draft = {
this.showingConfirmDialog = true this.showingConfirmDialog = true
}, },
doAbandon() { doAbandon() {
this.$store.dispatch('abandonDraft', { id: this.draft.id }).then(() => { useDraftsStore()
this.hideConfirmDialog() .abandonDraft(this.draft.id)
}) .then(() => {
this.hideConfirmDialog()
})
}, },
hideConfirmDialog() { hideConfirmDialog() {
this.showingConfirmDialog = false this.showingConfirmDialog = false

View file

@ -67,11 +67,13 @@
<div v-if="editing"> <div v-if="editing">
<PostStatusForm <PostStatusForm
v-if="draft.type !== 'edit'" v-if="draft.type !== 'edit'"
ref="form"
:hide-draft="true" :hide-draft="true"
v-bind="postStatusFormProps" v-bind="postStatusFormProps"
/> />
<EditStatusForm <EditStatusForm
v-else v-else
ref="form"
:hide-draft="true" :hide-draft="true"
:params="postStatusFormProps" :params="postStatusFormProps"
/> />

View file

@ -3,6 +3,8 @@ import { defineAsyncComponent } from 'vue'
import Draft from 'src/components/draft/draft.vue' import Draft from 'src/components/draft/draft.vue'
import List from 'src/components/list/list.vue' import List from 'src/components/list/list.vue'
import { useDraftsStore } from 'src/stores/drafts.js'
const Drafts = { const Drafts = {
components: { components: {
Draft, Draft,
@ -18,7 +20,7 @@ const Drafts = {
}, },
computed: { computed: {
drafts() { drafts() {
return this.$store.getters.draftsArray return useDraftsStore().draftsArray
}, },
}, },
methods: { methods: {
@ -26,8 +28,8 @@ const Drafts = {
this.showingConfirmDialog = true this.showingConfirmDialog = true
}, },
doAbandonAll() { doAbandonAll() {
this.$store useDraftsStore()
.dispatch('abandonAllDrafts') .abandonAllDrafts()
.then(() => this.hideConfirmDialog()) .then(() => this.hideConfirmDialog())
}, },
hideConfirmDialog() { hideConfirmDialog() {

View file

@ -15,9 +15,11 @@ const EditStatusForm = {
requestClose() { requestClose() {
this.$refs.postStatusForm.requestClose() this.$refs.postStatusForm.requestClose()
}, },
saveDraft() {
this.$refs.postStatusForm.saveDraft()
},
doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) { doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
const params = { const params = {
store: this.$store,
statusId: this.params.statusId, statusId: this.params.statusId,
status, status,
spoilerText, spoilerText,

View file

@ -30,7 +30,7 @@ const ExtraNotifications = {
return ( return (
this.mergedConfig.showExtraNotifications && this.mergedConfig.showExtraNotifications &&
this.mergedConfig.showAnnouncementsInExtraNotifications && this.mergedConfig.showAnnouncementsInExtraNotifications &&
this.unreadAnnouncementCount this.unreadAnnouncementsCount
) )
}, },
shouldShowFollowRequests() { shouldShowFollowRequests() {
@ -56,7 +56,7 @@ const ExtraNotifications = {
return useUsersStore().currentUser return useUsersStore().currentUser
}, },
...mapState(useAnnouncementsStore, { ...mapState(useAnnouncementsStore, {
unreadAnnouncementCount: 'unreadAnnouncementCount', unreadAnnouncementsCount: 'unreadAnnouncementsCount',
}), }),
...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState(useChatsStore, ['unreadChatsCount']), ...mapState(useChatsStore, ['unreadChatsCount']),

View file

@ -31,7 +31,7 @@
class="fa-scale-110 icon" class="fa-scale-110 icon"
icon="bullhorn" icon="bullhorn"
/> />
{{ $t('notifications.unread_announcements', { num: unreadAnnouncementCount }, unreadAnnouncementCount) }} {{ $t('notifications.unread_announcements', { num: unreadAnnouncementsCount }, unreadAnnouncementsCount) }}
</router-link> </router-link>
</div> </div>
<div <div

View file

@ -122,7 +122,6 @@ const mediaUpload = {
}, },
async uploadFile(file) { async uploadFile(file) {
const self = this const self = this
const store = this.$store
if (file.size > useInstanceStore().uploadlimit) { if (file.size > useInstanceStore().uploadlimit) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size) const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat( const allowedsize = fileSizeFormatService.fileSizeFormat(
@ -145,7 +144,7 @@ const mediaUpload = {
self.$emit('uploading') self.$emit('uploading')
self.uploadCount++ self.uploadCount++
statusPosterService.uploadMedia({ store, formData }).then( statusPosterService.uploadMedia({ formData }).then(
(fileData) => { (fileData) => {
self.$emit('uploaded', fileData) self.$emit('uploaded', fileData)
self.decreaseUploadCount() self.decreaseUploadCount()

View file

@ -70,7 +70,7 @@ const MobileNav = {
countExtraNotifications( countExtraNotifications(
useMergedConfigStore().mergedConfig, useMergedConfigStore().mergedConfig,
useChatsStore().unreadChatsCount, useChatsStore().unreadChatsCount,
useAnnouncementsStore().unreadAnnouncementCount, useAnnouncementsStore().unreadAnnouncementsCount,
useFollowRequestsStore().followRequestsCount, useFollowRequestsStore().followRequestsCount,
) )
) )
@ -96,7 +96,7 @@ const MobileNav = {
closingDrawerMarksAsSeen() { closingDrawerMarksAsSeen() {
return useMergedConfigStore().mergedConfig.closingDrawerMarksAsSeen return useMergedConfigStore().mergedConfig.closingDrawerMarksAsSeen
}, },
...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), ...mapState(useAnnouncementsStore, ['unreadAnnouncementsCount']),
...mapState(useMergedConfigStore, { ...mapState(useMergedConfigStore, {
pinnedItems: (store) => pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedNavItems).has('chats'), new Set(store.prefsStorage.collections.pinnedNavItems).has('chats'),

View file

@ -19,7 +19,7 @@
icon="bars" icon="bars"
/> />
<div <div
v-if="(unreadChatsCount && !chatsPinned) || unreadAnnouncementCount" v-if="(unreadChatsCount && !chatsPinned) || unreadAnnouncementsCount"
class="badge -dot -notification" class="badge -dot -notification"
/> />
</button> </button>

View file

@ -112,7 +112,7 @@ const NavPanel = {
}, },
computed: { computed: {
...mapState(useAnnouncementsStore, { ...mapState(useAnnouncementsStore, {
unreadAnnouncementCount: 'unreadAnnouncementCount', unreadAnnouncementsCount: 'unreadAnnouncementsCount',
supportsAnnouncements: (store) => store.supportsAnnouncements, supportsAnnouncements: (store) => store.supportsAnnouncements,
}), }),
...mapState(useInstanceCapabilitiesStore, [ ...mapState(useInstanceCapabilitiesStore, [

View file

@ -76,7 +76,7 @@ export const ROOT_ITEMS = {
icon: 'comments', icon: 'comments',
label: 'nav.chats', label: 'nav.chats',
badgeStyle: 'notification', badgeStyle: 'notification',
badgeGetter: 'unreadChatsCount', badgeGetter: 'unreadChats',
criteria: ['chats'], criteria: ['chats'],
}, },
friendRequests: { friendRequests: {
@ -85,7 +85,7 @@ export const ROOT_ITEMS = {
label: 'nav.friend_requests', label: 'nav.friend_requests',
badgeStyle: 'notification', badgeStyle: 'notification',
criteria: ['lockedUser'], criteria: ['lockedUser'],
badgeGetter: 'followRequestsCount', badgeGetter: 'followRequests',
}, },
about: { about: {
route: 'about', route: 'about',
@ -99,7 +99,7 @@ export const ROOT_ITEMS = {
label: 'nav.announcements', label: 'nav.announcements',
store: 'announcements', store: 'announcements',
badgeStyle: 'notification', badgeStyle: 'notification',
badgeGetter: 'unreadAnnouncementCount', badgeGetter: 'unreadAnnouncements',
criteria: ['announcements'], criteria: ['announcements'],
}, },
drafts: { drafts: {
@ -107,7 +107,7 @@ export const ROOT_ITEMS = {
icon: 'file-pen', icon: 'file-pen',
label: 'nav.drafts', label: 'nav.drafts',
badgeStyle: 'neutral', badgeStyle: 'neutral',
badgeGetter: 'draftCount', badgeGetter: 'drafts',
}, },
} }

View file

@ -1,9 +1,12 @@
import { mapState, mapStores } from 'pinia' import { mapState } from 'pinia'
import { routeTo } from 'src/components/navigation/navigation.js' import { routeTo } from 'src/components/navigation/navigation.js'
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useChatsStore } from 'src/stores/chats.js'
import { useDraftsStore } from 'src/stores/drafts.js'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
@ -40,11 +43,19 @@ const NavigationEntry = {
routeTo() { routeTo() {
return routeTo(this.item, this.currentUser) return routeTo(this.item, this.currentUser)
}, },
getters() { badges() {
return this.$store.getters return {
drafts: this.draftsCount,
unreadAnnouncements: this.unreadAnnouncementsCount,
followRequests: this.followRequestsCount,
unreadChats: this.unreadChatsCount,
}
}, },
...mapStores(useAnnouncementsStore), ...mapState(useAnnouncementsStore, ['unreadAnnouncementsCount']),
...mapState(useDraftsStore, ['draftsCount']),
...mapState(useUsersStore, ['currentUser']), ...mapState(useUsersStore, ['currentUser']),
...mapState(useChatsStore, ['unreadChatsCount']),
...mapState(useFollowRequestsStore, ['followRequestsCount']),
...mapState(useSyncConfigStore, { ...mapState(useSyncConfigStore, {
pinnedItems: (store) => pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedNavItems), new Set(store.prefsStorage.collections.pinnedNavItems),

View file

@ -47,17 +47,11 @@
</component> </component>
<slot /> <slot />
<div <div
v-if="item.badgeGetter && getters[item.badgeGetter]" v-if="item.badgeGetter && badges[item.badgeGetter]"
class="badge" class="badge"
:class="[`-${item.badgeStyle}`]" :class="[`-${item.badgeStyle}`]"
> >
{{ getters[item.badgeGetter] }} {{ badges[item.badgeGetter] }}
</div>
<div
v-else-if="item.badgeGetter && item.store && this[`${item.store}Store`][item.badgeGetter]"
class="badge badge-notification"
>
{{ this[`${item.store}Store`][item.badgeGetter] }}
</div> </div>
<button <button
v-if="showPin && currentUser" v-if="showPin && currentUser"

View file

@ -13,6 +13,8 @@ import {
import { useAnnouncementsStore } from 'src/stores/announcements' import { useAnnouncementsStore } from 'src/stores/announcements'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
import { useChatsStore } from 'src/stores/chats.js'
import { useDraftsStore } from 'src/stores/drafts.js'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@ -56,18 +58,27 @@ const NavPanel = {
}, },
components: {}, components: {},
computed: { computed: {
getters() { badges() {
return this.$store.getters return {
drafts: this.draftsCount,
unreadAnnouncements: this.unreadAnnouncementsCount,
followRequests: this.followRequestsCount,
unreadChats: this.unreadChatsCount,
}
}, },
...mapState(useListsStore, { ...mapState(useListsStore, {
lists: getListEntries, lists: getListEntries,
}), }),
...mapState(useAnnouncementsStore, { ...mapState(useAnnouncementsStore, {
supportsAnnouncements: (store) => store.supportsAnnouncements, supportsAnnouncements: (store) => store.supportsAnnouncements,
unreadAnnouncementsCount: 'unreadAnnouncementsCount',
}), }),
...mapState(useDraftsStore, ['draftsCount']),
...mapState(useFollowRequestsStore, ['followRequestsCount']),
...mapState(useBookmarkFoldersStore, { ...mapState(useBookmarkFoldersStore, {
bookmarks: getBookmarkFolderEntries, bookmarks: getBookmarkFolderEntries,
}), }),
...mapState(useChatsStore, ['unreadChatsCount']),
...mapState(useSyncConfigStore, { ...mapState(useSyncConfigStore, {
pinnedItems: (store) => pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedNavItems), new Set(store.prefsStorage.collections.pinnedNavItems),
@ -78,7 +89,6 @@ const NavPanel = {
'localBubble', 'localBubble',
]), ]),
...mapState(useUsersStore, ['currentUser']), ...mapState(useUsersStore, ['currentUser']),
...mapState(useFollowRequestsStore, ['followRequestsCount']),
pinnedList() { pinnedList() {
if (!this.currentUser) { if (!this.currentUser) {
return filterNavigation( return filterNavigation(

View file

@ -23,7 +23,7 @@
:src="item.iconEmojiUrl" :src="item.iconEmojiUrl"
/> />
<div <div
v-if="item.badgeGetter && getters[item.badgeGetter]" v-if="item.badgeGetter && badges[item.badgeGetter]"
class="badge -dot" class="badge -dot"
:class="[`-${item.badgeStyle}`]" :class="[`-${item.badgeStyle}`]"
/> />

View file

@ -13,6 +13,7 @@ import {
highlightStyle, highlightStyle,
} from '../../services/user_highlighter/user_highlighter.js' } from '../../services/user_highlighter/user_highlighter.js'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js' import { useNotificationsStore } from 'src/stores/notifications.js'
@ -148,8 +149,7 @@ const Notification = {
id: this.user.id, id: this.user.id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
// TODO Fix this useFollowRequestsStore().remove(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
useNotificationsStore().markSingleNotificationAsSeen(this.notification.id) useNotificationsStore().markSingleNotificationAsSeen(this.notification.id)
this.hideApproveConfirmDialog() this.hideApproveConfirmDialog()
}, },
@ -166,8 +166,7 @@ const Notification = {
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(() => { }).then(() => {
useNotificationsStore().dismissNotificationLocal(this.notification.id) useNotificationsStore().dismissNotificationLocal(this.notification.id)
// TODO Fix this useFollowRequestsStore().remove(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog() this.hideDenyConfirmDialog()
}, },

View file

@ -110,7 +110,7 @@ const Notifications = {
return countExtraNotifications( return countExtraNotifications(
useMergedConfigStore().mergedConfig, useMergedConfigStore().mergedConfig,
useChatsStore().unreadChatsCount, useChatsStore().unreadChatsCount,
useAnnouncementsStore().unreadAnnouncementCount, useAnnouncementsStore().unreadAnnouncementsCount,
useFollowRequestsStore().followRequestsCount, useFollowRequestsStore().followRequestsCount,
) )
}, },
@ -118,7 +118,7 @@ const Notifications = {
return ( return (
this.unseenNotifications.length + this.unseenNotifications.length +
this.unreadChatsCount + this.unreadChatsCount +
this.unreadAnnouncementCount this.unreadAnnouncementsCount
) )
}, },
loading() { loading() {
@ -157,7 +157,7 @@ const Notifications = {
showExtraNotifications() { showExtraNotifications() {
return !this.noExtra return !this.noExtra
}, },
...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), ...mapState(useAnnouncementsStore, ['unreadAnnouncementsCount']),
...mapState(useChatsStore, ['unreadChatsCount']), ...mapState(useChatsStore, ['unreadChatsCount']),
...mapState(useInterfaceStore, ['layoutType']), ...mapState(useInterfaceStore, ['layoutType']),
}, },

View file

@ -24,6 +24,7 @@ import { findOffset } from '../../services/offset_finder/offset_finder.service.j
import genRandomSeed from '../../services/random_seed/random_seed.service.js' import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import statusPoster from '../../services/status_poster/status_poster.service.js' import statusPoster from '../../services/status_poster/status_poster.service.js'
import { useDraftsStore } from 'src/stores/drafts.js'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@ -400,8 +401,6 @@ const PostStatusForm = {
contentType: this.newStatus.contentType, contentType: this.newStatus.contentType,
poll, poll,
idempotencyKey: this.idempotencyKey, idempotencyKey: this.idempotencyKey,
store: this.$store,
} }
}, },
@ -412,7 +411,6 @@ const PostStatusForm = {
...useEmojiStore().standardEmojiList, ...useEmojiStore().standardEmojiList,
...useEmojiStore().customEmoji, ...useEmojiStore().customEmoji,
], ],
store: this.$store,
}) })
}, },
emojiSuggestor() { emojiSuggestor() {
@ -574,7 +572,7 @@ const PostStatusForm = {
...mapState(useUsersStore, ['currentUser']), ...mapState(useUsersStore, ['currentUser']),
...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (store) => store.mobileLayout, mobileLayout: (state) => state.mobileLayout,
}), }),
}, },
watch: { watch: {
@ -751,7 +749,6 @@ const PostStatusForm = {
const description = this.newStatus.mediaDescriptions[id] const description = this.newStatus.mediaDescriptions[id]
if (!description || description.trim() === '') return if (!description || description.trim() === '') return
return statusPoster.setMediaDescription({ return statusPoster.setMediaDescription({
store: this.$store,
id, id,
description, description,
}) })
@ -985,13 +982,13 @@ const PostStatusForm = {
saveDraft() { saveDraft() {
if (!this.disableDraft && !this.saveInhibited) { if (!this.disableDraft && !this.saveInhibited) {
if (this.safeToSaveDraft) { if (this.safeToSaveDraft) {
return this.$store return useDraftsStore()
.dispatch('addOrSaveDraft', { .addOrSaveDraft({
draft: { type: this.statusType,
type: this.statusType, refId: this.refId,
refId: this.refId, ...this.newStatus,
...this.newStatus, // Draft ID overwrites status ID (which is undefined for fresh statuses)
}, id: this.draftId,
}) })
.then((id) => { .then((id) => {
if (this.newStatus.id !== id) { if (this.newStatus.id !== id) {
@ -1024,14 +1021,14 @@ const PostStatusForm = {
} }
}, },
abandonDraft() { abandonDraft() {
return this.$store.dispatch('abandonDraft', { id: this.draftId }) return useDraftsStore().abandonDraft(this.draftId)
}, },
getDraft() { getDraft() {
const maybeDraft = this.$store.state.drafts.drafts[this.draftId] const maybeDraft = useDraftsStore().drafts.get(this.draftId)
if (this.draftId && maybeDraft) { if (this.draftId && maybeDraft) {
return maybeDraft return maybeDraft
} else { } else {
const existingDrafts = this.$store.getters.draftsByTypeAndRefId( const existingDrafts = useDraftsStore().draftsByTypeAndRefId(
this.statusType, this.statusType,
this.refId, this.refId,
) )

View file

@ -8,8 +8,8 @@ import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useProfileConfigStore } from 'src/stores/profile_config.js' import { useProfileConfigStore } from 'src/stores/profile_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
export default { export default {
components: { components: {
@ -413,8 +413,8 @@ export default {
hardReset() { hardReset() {
switch (this.realSource) { switch (this.realSource) {
case 'admin': case 'admin':
return this.$store return useAdminSettingsStore()
.dispatch('resetAdminSetting', { path: this.path }) .resetAdminSetting({ path: this.path })
.then(() => { .then(() => {
this.draft = this.state this.draft = this.state
}) })

View file

@ -1,5 +1,3 @@
// eslint-disable-next-line no-unused
import { mapState } from 'pinia' import { mapState } from 'pinia'
import { Fragment } from 'vue' import { Fragment } from 'vue'

View file

@ -16,8 +16,8 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useProfileConfigStore } from 'src/stores/profile_config.js' import { useProfileConfigStore } from 'src/stores/profile_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { updateProfile } from 'src/api/user.js' import { updateProfile } from 'src/api/user.js'

View file

@ -1,5 +1,4 @@
import { mapActions, mapState } from 'pinia' import { mapActions, mapState } from 'pinia'
import { mapGetters } from 'vuex'
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
import UserCard from 'src/components/user_card/user_card.vue' import UserCard from 'src/components/user_card/user_card.vue'
@ -8,6 +7,7 @@ import { unseenNotifications } from '../../services/notification_utils/notificat
import { useAnnouncementsStore } from 'src/stores/announcements' import { useAnnouncementsStore } from 'src/stores/announcements'
import { useChatsStore } from 'src/stores/chats.js' import { useChatsStore } from 'src/stores/chats.js'
import { useDraftsStore } from 'src/stores/drafts.js'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@ -62,10 +62,6 @@ const SideDrawer = {
GestureService.DIRECTION_LEFT, GestureService.DIRECTION_LEFT,
this.toggleDrawer, this.toggleDrawer,
) )
if (this.currentUser?.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
}, },
components: { components: {
UserCard, UserCard,
@ -101,7 +97,7 @@ const SideDrawer = {
...mapState(useFollowRequestsStore, ['followRequestsCount']), ...mapState(useFollowRequestsStore, ['followRequestsCount']),
...mapState(useAnnouncementsStore, [ ...mapState(useAnnouncementsStore, [
'supportsAnnouncements', 'supportsAnnouncements',
'unreadAnnouncementCount', 'unreadAnnouncementsCount',
]), ]),
...mapState(useInstanceCapabilitiesStore, [ ...mapState(useInstanceCapabilitiesStore, [
'pleromaChatMessagesAvailable', 'pleromaChatMessagesAvailable',
@ -114,7 +110,7 @@ const SideDrawer = {
hideSitename: (store) => store.instanceIdentity.hideSitename, hideSitename: (store) => store.instanceIdentity.hideSitename,
}), }),
...mapState(useChatsStore, ['unreadChatsCount']), ...mapState(useChatsStore, ['unreadChatsCount']),
...mapGetters(['draftCount']), ...mapState(useDraftsStore, ['draftCount']),
}, },
methods: { methods: {
toggleDrawer() { toggleDrawer() {

View file

@ -248,10 +248,10 @@
icon="bullhorn" icon="bullhorn"
/> {{ $t("nav.announcements") }} /> {{ $t("nav.announcements") }}
<span <span
v-if="unreadAnnouncementCount" v-if="unreadAnnouncementsCount"
class="badge -notification" class="badge -notification"
> >
{{ unreadAnnouncementCount }} {{ unreadAnnouncementsCount }}
</span> </span>
</router-link> </router-link>
</li> </li>

View file

@ -97,9 +97,6 @@ const StatusActionButtons = {
replying: this.replying, replying: this.replying,
emojiPickerShown: this.emojiPickerShown, emojiPickerShown: this.emojiPickerShown,
emit: this.$emit, emit: this.$emit,
dispatch: this.$store.dispatch,
state: this.$store.state,
getters: this.$store.getters,
router: this.$router, router: this.$router,
currentUser: this.currentUser, currentUser: this.currentUser,
loggedIn: !!this.currentUser, loggedIn: !!this.currentUser,

View file

@ -29,14 +29,13 @@ const StickerPicker = {
} }
}, },
pick(sticker, name) { pick(sticker, name) {
const store = this.$store
// TODO remove this workaround by finding a way to bypass reuploads // TODO remove this workaround by finding a way to bypass reuploads
fetch(sticker).then((res) => { fetch(sticker).then((res) => {
res.blob().then((blob) => { res.blob().then((blob) => {
const file = new File([blob], name, { mimetype: 'image/png' }) const file = new File([blob], name, { mimetype: 'image/png' })
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
statusPosterService.uploadMedia({ store, formData }).then( statusPosterService.uploadMedia({ formData }).then(
(fileData) => { (fileData) => {
this.$emit('uploaded', fileData) this.$emit('uploaded', fileData)
this.clear() this.clear()

View file

@ -1,5 +1,3 @@
// eslint-disable-next-line no-unused
import { Fragment } from 'vue' import { Fragment } from 'vue'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'

View file

@ -418,7 +418,6 @@ export default {
...useEmojiStore().standardEmojiList, ...useEmojiStore().standardEmojiList,
...useEmojiStore().customEmoji, ...useEmojiStore().customEmoji,
], ],
store: this.$store,
}) })
}, },
emojiSuggestor() { emojiSuggestor() {

View file

@ -1,11 +1,7 @@
import { cloneDeep, each, get, merge, set } from 'lodash' import { cloneDeep, get, set } from 'lodash'
import { storage } from './storage.js' import { storage } from './storage.js'
import { useInterfaceStore } from 'src/stores/interface'
let loaded = false
const defaultReducer = (state, paths) => const defaultReducer = (state, paths) =>
paths.length === 0 paths.length === 0
? state ? state
@ -14,86 +10,10 @@ const defaultReducer = (state, paths) =>
return substate return substate
}, {}) }, {})
const saveImmedeatelyActions = [
'markNotificationsAsSeen',
'setHighlight',
'setOption',
'setClientData',
'setToken',
'clearToken',
]
const defaultStorage = (() => { const defaultStorage = (() => {
return storage return storage
})() })()
export default function createPersistedState({
key = 'vuex-lz',
paths = [],
getState = (key, storage) => {
const value = storage.getItem(key)
return value
},
setState = (key, state, storage) => {
if (!loaded) {
console.info('waiting for old state to be loaded...')
return Promise.resolve()
} else {
return storage.setItem(key, state)
}
},
reducer = defaultReducer,
storage = defaultStorage,
subscriber = (store) => (handler) => store.subscribe(handler),
} = {}) {
return getState(key, storage).then((savedState) => {
return (store) => {
try {
if (savedState !== null && typeof savedState === 'object') {
// build user cache
const usersState = savedState.users || {}
usersState.usersObject = {}
const users = usersState.users || []
each(users, (user) => {
usersState.usersObject[user.id] = user
})
savedState.users = usersState
store.replaceState(merge({}, store.state, savedState))
}
loaded = true
} catch (e) {
console.error("Couldn't load state")
console.error(e)
loaded = true
}
subscriber(store)((mutation, state) => {
try {
if (saveImmedeatelyActions.includes(mutation.type)) {
setState(key, reducer(cloneDeep(state), paths), storage).then(
(success) => {
if (success !== undefined) {
if (mutation.type === 'setOption') {
useInterfaceStore().settingsSaved({ success })
}
}
},
(error) => {
if (mutation.type === 'setOption') {
useInterfaceStore().settingsSaved({ error })
}
},
)
}
} catch (e) {
console.error("Couldn't persist state:")
console.error(e)
}
})
}
})
}
/** /**
* This persists state for pinia, which falls back to read from the vuex state * This persists state for pinia, which falls back to read from the vuex state
* if pinia persisted state does not exist. * if pinia persisted state does not exist.

View file

@ -1,9 +1,7 @@
/* global process */ /* global process */
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { createStore } from 'vuex'
import 'custom-event-polyfill'
import './lib/event_target_polyfill.js' import './lib/event_target_polyfill.js'
// Polyfill for Array.prototype.toSorted (ES2023) // Polyfill for Array.prototype.toSorted (ES2023)
@ -17,11 +15,8 @@ import { createI18n } from 'vue-i18n'
import afterStoreSetup from './boot/after_store.js' import afterStoreSetup from './boot/after_store.js'
import messages from './i18n/messages.js' import messages from './i18n/messages.js'
import createPersistedState, { import { piniaPersistPlugin } from './lib/persisted_state.js'
piniaPersistPlugin,
} from './lib/persisted_state.js'
import { piniaPushNotificationsPlugin } from './lib/push_notifications_plugin.js' import { piniaPushNotificationsPlugin } from './lib/push_notifications_plugin.js'
import vuexModules from './modules/index.js'
import { piniaLanguagePlugin } from 'src/lib/language.js' import { piniaLanguagePlugin } from 'src/lib/language.js'
import { piniaStylePlugin } from 'src/lib/style.js' import { piniaStylePlugin } from 'src/lib/style.js'
@ -37,10 +32,6 @@ const i18n = createI18n({
messages.setLanguage(i18n.global, currentLocale) messages.setLanguage(i18n.global, currentLocale)
const persistedStateOptions = {
paths: ['oauth', 'config'],
}
;(async () => { ;(async () => {
const isFox = Math.floor(Math.random() * 2) > 0 ? '_fox' : '' const isFox = Math.floor(Math.random() * 2) > 0 ? '_fox' : ''
@ -69,20 +60,12 @@ const persistedStateOptions = {
try { try {
let storageError let storageError
const plugins = []
const pinia = createPinia() const pinia = createPinia()
pinia.use(piniaPersistPlugin()) pinia.use(piniaPersistPlugin())
pinia.use(piniaLanguagePlugin) pinia.use(piniaLanguagePlugin)
pinia.use(piniaStylePlugin) pinia.use(piniaStylePlugin)
pinia.use(piniaPushNotificationsPlugin) pinia.use(piniaPushNotificationsPlugin)
try {
const persistedState = await createPersistedState(persistedStateOptions)
plugins.push(persistedState)
} catch (e) {
console.error('Storage error', e)
storageError = e
}
document.querySelector('#splash').classList.remove('initial-hidden') document.querySelector('#splash').classList.remove('initial-hidden')
document.querySelector('#mascot').src = document.querySelector('#mascot').src =
`/static/pleromatan_apology${isFox}_small.webp` `/static/pleromatan_apology${isFox}_small.webp`
@ -93,18 +76,8 @@ const persistedStateOptions = {
'update.art_by', 'update.art_by',
{ linkToArtist: 'pipivovott' }, { linkToArtist: 'pipivovott' },
) )
const store = createStore({ // Temporarily passing pinia stores along with storageError result until migration is fully complete.
modules: vuexModules, return await afterStoreSetup({ pinia, storageError, i18n })
plugins,
options: {
devtools: process.env.NODE_ENV !== 'production',
},
strict: false, // Socket modifies itself, let's ignore this for now.
// strict: process.env.NODE_ENV !== 'production'
})
window.vuex = store
// Temporarily passing pinia and vuex stores along with storageError result until migration is fully complete.
return await afterStoreSetup({ pinia, store, storageError, i18n })
} catch (e) { } catch (e) {
splashError(i18n, e) splashError(i18n, e)
} }

View file

@ -1,99 +0,0 @@
import { storage } from 'src/lib/storage.js'
export const defaultState = {
drafts: {},
}
export const mutations = {
addOrSaveDraft(state, { draft }) {
state.drafts[draft.id] = draft
},
abandonDraft(state, { id }) {
delete state.drafts[id]
},
loadDrafts(state, data) {
state.drafts = data
},
}
const storageKey = 'pleroma-fe-drafts'
/*
* Note: we do not use the persist state plugin because
* it is not impossible for a user to have two windows at
* the same time. The persist state plugin is just overriding
* everything with the current state. This isn't good because
* if a draft is created in one window and another draft is
* created in another, the draft in the first window will just
* be overriden.
* Here, we can't guarantee 100% atomicity unless one uses
* different keys, which will just pollute the whole storage.
* It is indeed best to have backend support for this.
*/
const getStorageData = async () =>
(await storage.getItem(storageKey)) ||
{
/* no-op */
}
const saveDraftToStorage = async (draft) => {
const currentData = await getStorageData()
currentData[draft.id] = JSON.parse(JSON.stringify(draft))
await storage.setItem(storageKey, currentData)
}
const deleteDraftFromStorage = async (ids) => {
const currentData = await getStorageData()
ids.forEach((id) => {
delete currentData[id]
})
await storage.setItem(storageKey, currentData)
}
export const actions = {
async addOrSaveDraft(store, { draft }) {
const id = draft.id || new Date().getTime().toString()
const draftWithId = { ...draft, id }
store.commit('addOrSaveDraft', { draft: draftWithId })
await saveDraftToStorage(draftWithId)
return id
},
async abandonDraft(store, { id }) {
store.commit('abandonDraft', { id })
await deleteDraftFromStorage([id])
},
async abandonAllDrafts(store) {
const ids = Object.keys(store.state.drafts)
ids.forEach((id) => store.commit('abandonDraft', { id }))
await deleteDraftFromStorage(ids)
},
async loadDrafts(store) {
const currentData = await getStorageData()
store.commit('loadDrafts', currentData)
},
}
export const getters = {
draftsByTypeAndRefId(state) {
return (type, refId) => {
return Object.values(state.drafts).filter(
(draft) => draft.type === type && draft.refId === refId,
)
}
},
draftsArray(state) {
return Object.values(state.drafts)
},
draftCount(state) {
return Object.values(state.drafts).length
},
}
const drafts = {
state: defaultState,
mutations,
getters,
actions,
}
export default drafts

View file

@ -1,5 +1 @@
import drafts from './drafts.js' export default {}
export default {
drafts,
}

View file

@ -40,7 +40,6 @@ export class RegistrationError extends Error {
// the error is probably a JSON object with a single key, "errors", whose value is another JSON object containing the real errors // the error is probably a JSON object with a single key, "errors", whose value is another JSON object containing the real errors
if (typeof error === 'string') { if (typeof error === 'string') {
error = JSON.parse(error) error = JSON.parse(error)
// eslint-disable-next-line
if (Object.hasOwn(error, 'error')) { if (Object.hasOwn(error, 'error')) {
error = JSON.parse(error.error) error = JSON.parse(error.error)
} }

View file

@ -16,7 +16,7 @@ export const useAnnouncementsStore = defineStore('announcements', {
userActions: {}, userActions: {},
}), }),
getters: { getters: {
unreadAnnouncementCount() { unreadAnnouncementsCount() {
if (!useUsersStore().currentUser) { if (!useUsersStore().currentUser) {
return 0 return 0
} }

76
src/stores/drafts.js Normal file
View file

@ -0,0 +1,76 @@
import { defineStore } from 'pinia'
import { storage } from 'src/lib/storage.js'
const storageKey = 'pleroma-fe-drafts'
/*
* Note: we do not use the persist state plugin because
* it is not impossible for a user to have two windows at
* the same time. The persist state plugin is just overriding
* everything with the current state. This isn't good because
* if a draft is created in one window and another draft is
* created in another, the draft in the first window will just
* be overriden.
* Here, we can't guarantee 100% atomicity unless one uses
* different keys, which will just pollute the whole storage.
* It is indeed best to have backend support for this.
*/
const getStorageData = async () => await storage.getItem(storageKey)
const saveDraftToStorage = async (draft) => {
const currentData = (await getStorageData()) ?? {}
currentData[draft.id] = JSON.parse(JSON.stringify(draft))
await storage.setItem(storageKey, currentData)
}
const deleteDraftFromStorage = async (ids) => {
const currentData = (await getStorageData()) ?? {}
ids.forEach((id) => {
delete currentData[id]
})
await storage.setItem(storageKey, currentData)
}
export const useDraftsStore = defineStore('drafts', {
state: () => ({
drafts: new Map(),
}),
getters: {
draftsByTypeAndRefId(state) {
return (type, refId) => {
return [...state.drafts.values()].filter(
(draft) => draft.type === type && draft.refId === refId,
)
}
},
draftsArray(state) {
return [...state.drafts.values()]
},
draftsCount(state) {
return state.drafts.size
},
},
actions: {
async abandonDraft(id) {
this.drafts.delete(id)
await deleteDraftFromStorage([id])
},
async loadDrafts() {
const currentData = await getStorageData()
this.drafts = new Map(Object.entries(currentData))
},
async addOrSaveDraft(draft) {
const id = draft.id ?? new Date().getTime().toString()
const draftWithId = { ...draft, id }
this.drafts.set(id, draftWithId)
await saveDraftToStorage(draftWithId)
return id
},
async abandonAllDrafts(store) {
const ids = [...this.drafts.keys()]
ids.forEach((id) => this.abandonDraft(id))
await deleteDraftFromStorage(ids)
},
},
})

View file

@ -38,10 +38,6 @@ export const useInstanceCapabilitiesStore = defineStore(
} }
this[capability] = value this[capability] = value
if (capability === 'shoutAvailable') {
window.vuex.dispatch('initializeSocket')
}
}, },
}, },
}, },

View file

@ -97,7 +97,9 @@ export const settingsMap = {
} }
export const defaultState = () => ({ export const defaultState = () => ({
config: Object.fromEntries(Object.keys(settingsMap).map((key) => [key, null])) config: Object.fromEntries(
Object.keys(settingsMap).map((key) => [key, null]),
),
}) })
export const useProfileConfigStore = defineStore('profileConfig', { export const useProfileConfigStore = defineStore('profileConfig', {
@ -123,7 +125,7 @@ export const useProfileConfigStore = defineStore('profileConfig', {
return return
} }
useUsersStore().addNewUsers(result) const [user] = useUsersStore().addNewUsers(result)
this.update(user) this.update(user)
} catch (e) { } catch (e) {
console.warn('Error setting server-side option:', e) console.warn('Error setting server-side option:', e)

View file

@ -5,6 +5,7 @@ import { defineStore } from 'pinia'
import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
import { useChatsStore } from 'src/stores/chats.js' import { useChatsStore } from 'src/stores/chats.js'
import { useDraftsStore } from 'src/stores/drafts.js'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js' import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
@ -14,11 +15,11 @@ import { useListsStore } from 'src/stores/lists.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js' import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useProfileConfigStore } from 'src/stores/profile_config.js'
import { useShoutStore } from 'src/stores/shout.js' import { useShoutStore } from 'src/stores/shout.js'
import { useStatusesStore } from 'src/stores/statuses.js' import { useStatusesStore } from 'src/stores/statuses.js'
import { useStreamingStore } from 'src/stores/streaming.js' import { useStreamingStore } from 'src/stores/streaming.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useProfileConfigStore } from 'src/stores/profile_config.js'
import { useTimelinesStore } from 'src/stores/timelines.js' import { useTimelinesStore } from 'src/stores/timelines.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js'
@ -611,13 +612,6 @@ export const useUsersStore = defineStore('users', {
// Login/Logout // Login/Logout
async loginUser(accessToken) { async loginUser(accessToken) {
const store = window.vuex
const dispatch =
store?.dispatch ??
(() => {
/* no-op */
}) // for tests
this.loggingIn = true this.loggingIn = true
try { try {
@ -670,7 +664,7 @@ export const useUsersStore = defineStore('users', {
useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 }) useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 })
/**/ /**/
if (user.token) { if (user.token && useInstanceCapabilitiesStore().shoutAvailable) {
// Shoutbox // Shoutbox
useShoutStore().initializeSocket() useShoutStore().initializeSocket()
useShoutStore().initializeShout() useShoutStore().initializeShout()
@ -689,7 +683,6 @@ export const useUsersStore = defineStore('users', {
useBookmarkFoldersStore().startFetching() useBookmarkFoldersStore().startFetching()
if (user.locked) { if (user.locked) {
dispatch('startFetchingFollowRequests')
useFollowRequestsStore().startFetching() useFollowRequestsStore().startFetching()
} }
@ -701,7 +694,7 @@ export const useUsersStore = defineStore('users', {
useAnnouncementsStore().startFetching() useAnnouncementsStore().startFetching()
this.fetchMutes() this.fetchMutes()
dispatch('loadDrafts') useDraftsStore().loadDrafts()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -723,7 +716,6 @@ export const useUsersStore = defineStore('users', {
} }
}, },
logout() { logout() {
const store = window.vuex
const oauth = useOAuthStore() const oauth = useOAuthStore()
// Pause fetching // Pause fetching
@ -739,8 +731,6 @@ export const useUsersStore = defineStore('users', {
useFollowRequestsStore().stopFetching() useFollowRequestsStore().stopFetching()
} }
store?.dispatch('stopFetchingFollowRequests')
// NOTE: No need to verify the app still exists, because if it doesn't, // NOTE: No need to verify the app still exists, because if it doesn't,
// the token will be invalid too // the token will be invalid too
return oauth return oauth
@ -798,7 +788,7 @@ export const useUsersStore = defineStore('users', {
useListsStore().startFetching() useListsStore().startFetching()
useBookmarkFoldersStore().startFetching() useBookmarkFoldersStore().startFetching()
useChatsStore().startFetching() useChatsStore().startFetching()
store?.dispatch('startFetchingFollowRequests') useFollowRequestsStore().startFetching()
}) })
.finally(() => { .finally(() => {
useNotificationsStore().resume() useNotificationsStore().resume()

View file

@ -1,22 +0,0 @@
import { cloneDeep } from 'lodash'
import { createStore } from 'vuex'
import vuexModules from 'src/modules/index.js'
const tweakModules = (modules) => {
const res = {}
Object.entries(modules).forEach(([name, module]) => {
const m = { ...module }
m.state = cloneDeep(module.state)
res[name] = m
})
return res
}
const makeMockStore = () => {
return createStore({
modules: tweakModules(vuexModules),
})
}
export default makeMockStore

View file

@ -5,26 +5,15 @@ import VueVirtualScroller from 'vue-virtual-scroller'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import Status from 'src/components/status/status.vue' import Status from 'src/components/status/status.vue'
import StillImage from 'src/components/still-image/still-image.vue' import StillImage from 'src/components/still-image/still-image.vue'
import makeMockStore from './mock_store'
import routes from 'src/boot/routes' import routes from 'src/boot/routes'
export const $t = (msg) => msg export const $t = (msg) => msg
const $i18n = { t: (msg) => msg } const $i18n = { t: (msg) => msg }
const applyAfterStore = (store, afterStore) => { const getDefaultOpts = () => ({
afterStore(store)
return store
}
const getDefaultOpts = ({
afterStore = () => {
/* no-op */
},
} = {}) => ({
global: { global: {
plugins: [ plugins: [
applyAfterStore(makeMockStore(), afterStore),
VueVirtualScroller, VueVirtualScroller,
createRouter({ createRouter({
history: createMemoryHistory(), history: createMemoryHistory(),
@ -87,9 +76,8 @@ const customBehaviors = () => {
config.plugins.VueWrapper.install(customBehaviors) config.plugins.VueWrapper.install(customBehaviors)
export const mountOpts = (allOpts = {}) => { export const mountOpts = (opts = {}) => {
const { afterStore, ...opts } = allOpts const defaultOpts = getDefaultOpts()
const defaultOpts = getDefaultOpts({ afterStore })
const mergedOpts = { const mergedOpts = {
...opts, ...opts,
global: { global: {

View file

@ -29,11 +29,6 @@ const message3 = {
const global = { const global = {
mocks: { mocks: {
$store: {
state: {
api: {},
},
},
$route: { $route: {
params: { params: {
recipient_id: 2, recipient_id: 2,

View file

@ -1,193 +0,0 @@
import { createTestingPinia } from '@pinia/testing'
import { flushPromises, mount } from '@vue/test-utils'
import { setActivePinia } from 'pinia'
import { nextTick } from 'vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import { $t, mountOpts, waitForEvent } from '../../../fixtures/setup_test'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useUsersStore } from 'src/stores/users.js'
const autoSaveOrNot = (caseFn, caseTitle, runFn) => {
caseFn(`${caseTitle} with auto-save`, function () {
return runFn.bind(this)(true)
})
caseFn(`${caseTitle} with no auto-save`, function () {
return runFn.bind(this)(false)
})
}
const saveManually = async (wrapper) => {
const morePostActions = wrapper.findByText(
'button',
$t('post_status.more_post_actions'),
)
await morePostActions.trigger('click')
const btn = wrapper.findByText(
'button',
$t('post_status.save_to_drafts_button'),
)
await btn.trigger('click')
}
const waitSaveTime = 4000
const currentUser = {
id: 'current-user',
default_scope: 'public',
locked: false,
}
describe('Draft saving', () => {
beforeEach(() => {
setActivePinia(createTestingPinia())
useUsersStore().currentUser = currentUser
})
afterEach(() => {
vi.useRealTimers()
})
autoSaveOrNot(
it,
'should save when the button is clicked',
async (autoSave) => {
const wrapper = mount(PostStatusForm, mountOpts())
const store = useMergedConfigStore()
store.mergedConfig = {
autoSaveDraft: autoSave,
}
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
await saveManually(wrapper)
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal(
'mew mew',
)
},
)
it('should auto-save if it is enabled', async function () {
vi.useFakeTimers()
const wrapper = mount(PostStatusForm, mountOpts())
const store = useMergedConfigStore()
store.mergedConfig = {
autoSaveDraft: true,
}
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
await vi.advanceTimersByTimeAsync(waitSaveTime)
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew')
})
it('should auto-save when close if auto-save is on', async () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
closeable: true,
},
}),
)
const store = useMergedConfigStore()
store.mergedConfig = {
autoSaveDraft: true,
}
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
wrapper.vm.requestClose()
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await waitForEvent(wrapper, 'close-accepted')
})
it('should save when close if auto-save is off, and unsavedPostAction is save', async () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
closeable: true,
},
}),
)
const store = useMergedConfigStore()
store.mergedConfig = {
autoSaveDraft: false,
unsavedPostAction: 'save',
}
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
wrapper.vm.requestClose()
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await waitForEvent(wrapper, 'close-accepted')
})
it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
closeable: true,
},
}),
)
const store = useMergedConfigStore()
store.mergedConfig = {
autoSaveDraft: false,
unsavedPostAction: 'discard',
}
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
wrapper.vm.requestClose()
await waitForEvent(wrapper, 'close-accepted')
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
})
it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
closeable: true,
},
}),
)
const store = useMergedConfigStore(createTestingPinia())
store.mergedConfig = {
autoSaveDraft: false,
unsavedPostAction: 'confirm',
}
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
wrapper.vm.requestClose()
await nextTick()
await flushPromises()
const saveButton = await vi.waitFor(() => {
const button = wrapper.findByText(
'button',
$t('post_status.close_confirm_save_button'),
)
if (!button) throw new Error('Save button not present')
return button
})
expect(saveButton).to.be.ok
await saveButton.trigger('click')
console.info('clicked')
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await flushPromises()
await waitForEvent(wrapper, 'close-accepted')
})
})

View file

@ -1,16 +1,44 @@
import { createTestingPinia } from '@pinia/testing' import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils' import { flushPromises, mount } from '@vue/test-utils'
import { setActivePinia } from 'pinia' import { setActivePinia } from 'pinia'
import { $t, mountOpts, waitForEvent } from 'test/fixtures/setup_test.js'
import { vi } from 'vitest' import { vi } from 'vitest'
import { nextTick } from 'vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import { mountOpts } from '../../../fixtures/setup_test'
import { useDraftsStore } from 'src/stores/drafts.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js' import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
const autoSaveOrNot = (caseFn, caseTitle, runFn) => {
caseFn(`${caseTitle} with auto-save`, function () {
return runFn.bind(this)(true)
})
caseFn(`${caseTitle} with no auto-save`, function () {
return runFn.bind(this)(false)
})
}
const saveManually = async (wrapper) => {
const morePostActions = wrapper.findByText(
'button',
$t('post_status.more_post_actions'),
)
await morePostActions.trigger('click')
const btn = wrapper.findByText(
'button',
$t('post_status.save_to_drafts_button'),
)
await btn.trigger('click')
}
const waitSaveTime = 4000
const currentUser = { const currentUser = {
id: 'current-user', id: 'current-user',
default_scope: 'public', default_scope: 'public',
@ -36,303 +64,476 @@ const repliedStatus2 = {
} }
describe('PostStatusForm', () => { describe('PostStatusForm', () => {
beforeEach(() => { describe('Basic functionality', () => {
vi.useFakeTimers() beforeEach(() => {
setActivePinia(createTestingPinia()) vi.useFakeTimers()
useUsersStore().currentUser = currentUser setActivePinia(createTestingPinia())
useStatusesStore().allStatuses = new Map([ useUsersStore().currentUser = currentUser
[repliedStatus.id, repliedStatus], useStatusesStore().allStatuses = new Map([
]) [repliedStatus.id, repliedStatus],
}) ])
})
it('Clean empty initial state', () => { afterEach(() => {
const wrapper = mount(PostStatusForm, mountOpts()) vi.useRealTimers()
})
expect(wrapper.vm.statusType).to.equal('new') it('Clean empty initial state', () => {
expect(wrapper.vm.newStatus.spoilerText).to.eql('') const wrapper = mount(PostStatusForm, mountOpts())
expect(wrapper.vm.newStatus.mentions).to.eql('')
expect(wrapper.vm.newStatus.status).to.eql('')
})
it('Reset cleans form to pristine state equal to state form was when created', () => { expect(wrapper.vm.statusType).to.equal('new')
const wrapper = mount(PostStatusForm, mountOpts()) expect(wrapper.vm.newStatus.spoilerText).to.eql('')
expect(wrapper.vm.newStatus.mentions).to.eql('')
expect(wrapper.vm.newStatus.status).to.eql('')
})
const initial = { ...wrapper.vm.newStatus } it('Reset cleans form to pristine state equal to state form was when created', () => {
wrapper.vm.clearStatus() const wrapper = mount(PostStatusForm, mountOpts())
expect(wrapper.vm.newStatus).to.eql(initial) const initial = { ...wrapper.vm.newStatus }
}) wrapper.vm.clearStatus()
it('Initializes a reply form', () => { expect(wrapper.vm.newStatus).to.eql(initial)
const wrapper = mount( })
PostStatusForm,
mountOpts({ it('Initializes a reply form', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
repliedStatus: repliedStatus,
},
}),
)
useInstanceCapabilitiesStore().quotingAvailable = true
expect(wrapper.vm.statusType).to.equal('reply')
expect(wrapper.vm.isReply).to.equal(true)
expect(wrapper.vm.refId).to.equal('status-1')
expect(wrapper.vm.quotable).to.equal(true)
expect(wrapper.vm.inReplyToStatusId).to.equal('status-1')
expect(wrapper.vm.newStatus.quote).to.be.null
expect(wrapper.vm.newStatus.poll).to.be.null
expect(wrapper.vm.newStatus.spoilerText).to.eql('')
expect(wrapper.vm.newStatus.mentions).to.eql('@replied')
expect(wrapper.vm.newStatus.status).to.eql('@replied ')
expect(wrapper.vm.newStatus.visibility).to.eql('public')
})
it('Copies scope and subject line, disables quoting for locked posts', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
repliedStatus: repliedStatus2,
},
}),
)
useInstanceCapabilitiesStore().quotingAvailable = true
expect(wrapper.vm.statusType).to.equal('reply')
expect(wrapper.vm.isReply).to.equal(true)
expect(wrapper.vm.quotable).to.equal(false)
expect(wrapper.vm.newStatus.quote).to.be.null
expect(wrapper.vm.newStatus.poll).to.be.null
expect(wrapper.vm.newStatus.spoilerText).to.eql('re: subject')
expect(wrapper.vm.newStatus.mentions).to.eql('@replied')
expect(wrapper.vm.newStatus.status).to.eql('@replied ')
expect(wrapper.vm.newStatus.visibility).to.eql('private')
expect(wrapper.vm.postingOptions.status).to.eql('@replied ')
expect(wrapper.vm.postingOptions.spoilerText).to.eql('re: subject')
expect(wrapper.vm.postingOptions.visibility).to.eql('private')
expect(wrapper.vm.postingOptions.sensitive).to.eql(false)
expect(wrapper.vm.postingOptions.media).to.eql([])
expect(wrapper.vm.postingOptions.inReplyToStatusId).to.eql('status-2')
expect(wrapper.vm.postingOptions.quoteId).to.be.null
expect(wrapper.vm.postingOptions.contentType).to.eql('text/plain')
expect(wrapper.vm.postingOptions.poll).to.be.null
})
it('Forces direct mode when replying to a DM, mastodon style subject handling', () => {
// We need to initialize pinia first which is happening here...
const options = mountOpts({
props: { props: {
repliedStatus: repliedStatus, repliedStatus: { ...repliedStatus2, visibility: 'direct' },
}, },
}), })
)
useInstanceCapabilitiesStore().quotingAvailable = true // ...set our settings...
useMergedConfigStore().mergedConfig = {
...useMergedConfigStore().mergedConfig,
subjectLineBehavior: 'masto',
}
expect(wrapper.vm.statusType).to.equal('reply') // ...and only then mount our component
expect(wrapper.vm.isReply).to.equal(true) const wrapper = mount(PostStatusForm, options)
expect(wrapper.vm.refId).to.equal('status-1')
expect(wrapper.vm.quotable).to.equal(true) // Otherwise we get multiple instances of pinia that don't talk to each other
expect(wrapper.vm.inReplyToStatusId).to.equal('status-1')
expect(wrapper.vm.newStatus.quote).to.be.null expect(wrapper.vm.statusType).to.equal('reply')
expect(wrapper.vm.newStatus.poll).to.be.null expect(wrapper.vm.isReply).to.equal(true)
expect(wrapper.vm.newStatus.spoilerText).to.eql('') expect(wrapper.vm.quotable).to.equal(false)
expect(wrapper.vm.newStatus.mentions).to.eql('@replied') expect(wrapper.vm.newStatus.quote).to.be.null
expect(wrapper.vm.newStatus.status).to.eql('@replied ') expect(wrapper.vm.newStatus.poll).to.be.null
expect(wrapper.vm.newStatus.visibility).to.eql('public') expect(wrapper.vm.newStatus.spoilerText).to.eql('subject')
expect(wrapper.vm.newStatus.mentions).to.eql('@replied')
expect(wrapper.vm.newStatus.status).to.eql('@replied ')
expect(wrapper.vm.newStatus.visibility).to.eql('direct')
})
it('Sets status to statusText without mentions if mentions line is enabled', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
repliedStatus: repliedStatus2,
statusText: 'testing',
mentionsLine: true,
},
}),
)
expect(wrapper.vm.statusType).to.equal('reply')
expect(wrapper.vm.isReply).to.equal(true)
expect(wrapper.vm.newStatus.mentions).to.eql('@replied')
expect(wrapper.vm.newStatus.status).to.eql('testing')
})
it('Sets mention when asked for it', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
profileMention: repliedUser,
},
}),
)
expect(wrapper.vm.statusType).to.equal('mention')
expect(wrapper.vm.isReply).to.equal(false)
expect(wrapper.vm.newStatus.mentions).to.eql('@replied')
expect(wrapper.vm.newStatus.status).to.eql('@replied ')
})
it('Initializes quote when reply/quote toggled to quote', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
repliedStatus: repliedStatus2,
},
}),
)
expect(wrapper.vm.statusType).to.equal('reply')
expect(wrapper.vm.isReply).to.equal(true)
wrapper.vm.quoteThreadToggled = true
expect(wrapper.vm.newStatus.quote).to.eql({
thread: true,
id: 'status-2',
})
})
it('Resets quote when reply/quote toggled to reply', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
repliedStatus: repliedStatus2,
},
}),
)
expect(wrapper.vm.statusType).to.equal('reply')
expect(wrapper.vm.isReply).to.equal(true)
wrapper.vm.quoteThreadToggled = true
wrapper.vm.quoteThreadToggled = false
expect(wrapper.vm.newStatus.quote).to.be.null
})
it('Initializes and reset quote when toggling quote attachment', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
repliedStatus: repliedStatus2,
},
}),
)
expect(wrapper.vm.statusType).to.equal('reply')
expect(wrapper.vm.isReply).to.equal(true)
wrapper.vm.toggleQuoteForm()
expect(wrapper.vm.newStatus.quote).to.eql({
thread: false,
id: null,
url: '',
})
wrapper.vm.toggleQuoteForm()
expect(wrapper.vm.newStatus.quote).to.be.null
})
it('Status editing', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
statusId: 'edited',
statusText: 'text',
statusSubject: 'heading',
statusIsSensitive: true,
statusPoll: {},
statusQuote: {},
statusFiles: [],
statusMediaDescriptions: {},
statusVisibility: 'unlisted',
statusContentType: 'text/markdown',
},
}),
)
expect(wrapper.vm.statusType).to.equal('edit')
expect(wrapper.vm.isReply).to.equal(false) // edits don't support changing reply-to so it's pretty much ignored
expect(wrapper.vm.isEdit).to.equal(true)
expect(wrapper.vm.newStatus.quote).to.eql({})
expect(wrapper.vm.newStatus.poll).to.eql({})
expect(wrapper.vm.newStatus.spoilerText).to.eql('heading')
expect(wrapper.vm.newStatus.mentions).to.eql('')
expect(wrapper.vm.newStatus.status).to.eql('text')
expect(wrapper.vm.newStatus.visibility).to.eql('unlisted')
expect(wrapper.vm.newStatus.contentType).to.eql('text/markdown')
expect(wrapper.vm.newStatus.nsfw).to.equal(true)
expect(wrapper.vm.newStatus.files).to.eql([])
})
it('Posting should reset idempotency key', async () => {
vi.setSystemTime(new Date(2027, 1, 1, 13))
const wrapper = mount(PostStatusForm, mountOpts())
const oldIdempotency = wrapper.vm.idempotencyKey
vi.setSystemTime(new Date(2028, 1, 1, 13))
wrapper.vm.newStatus.status = 'Testing'
await wrapper.vm.postStatus()
expect(wrapper.vm.idempotencyKey).to.not.eql(oldIdempotency)
})
}) })
it('Copies scope and subject line, disables quoting for locked posts', () => { describe('Attachments', () => {
const wrapper = mount( beforeEach(() => {
PostStatusForm, vi.useFakeTimers()
mountOpts({ setActivePinia(createTestingPinia())
props: { useUsersStore().currentUser = currentUser
repliedStatus: repliedStatus2, useStatusesStore().allStatuses = new Map([
}, [repliedStatus.id, repliedStatus],
}), ])
) })
useInstanceCapabilitiesStore().quotingAvailable = true afterEach(() => {
vi.useRealTimers()
})
expect(wrapper.vm.statusType).to.equal('reply') // TODO Probably better to separate attachment upload/manipulation into its own component?
expect(wrapper.vm.isReply).to.equal(true) // we need to upload-on-submit for compression setting anyway
expect(wrapper.vm.quotable).to.equal(false) it('Attachments manipulations (moving, adding, removing)', () => {
expect(wrapper.vm.newStatus.quote).to.be.null const wrapper = mount(PostStatusForm, mountOpts())
expect(wrapper.vm.newStatus.poll).to.be.null
expect(wrapper.vm.newStatus.spoilerText).to.eql('re: subject')
expect(wrapper.vm.newStatus.mentions).to.eql('@replied')
expect(wrapper.vm.newStatus.status).to.eql('@replied ')
expect(wrapper.vm.newStatus.visibility).to.eql('private')
expect(wrapper.vm.postingOptions.status).to.eql('@replied ') const i1 = { id: '1', url: 'a' }
expect(wrapper.vm.postingOptions.spoilerText).to.eql('re: subject') const i2 = { id: '2', url: 'b' }
expect(wrapper.vm.postingOptions.visibility).to.eql('private') const i3 = { id: '3', url: 'c' }
expect(wrapper.vm.postingOptions.sensitive).to.eql(false) const i4 = { id: '4', url: 'd' }
expect(wrapper.vm.postingOptions.media).to.eql([]) const iX = { id: 'x', url: 'x' }
expect(wrapper.vm.postingOptions.inReplyToStatusId).to.eql('status-2')
expect(wrapper.vm.postingOptions.quoteId).to.be.null wrapper.vm.newStatus.files = [i3, i1, iX, i2]
expect(wrapper.vm.postingOptions.contentType).to.eql('text/plain')
expect(wrapper.vm.postingOptions.poll).to.be.null wrapper.vm.removeMediaFile(iX)
expect(wrapper.vm.newStatus.files).to.eql([i3, i1, i2])
wrapper.vm.shiftUpMediaFile(i1)
expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2])
wrapper.vm.shiftUpMediaFile(i1) // should ignore
expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2])
wrapper.vm.shiftDnMediaFile(i3)
expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3])
wrapper.vm.shiftDnMediaFile(i3) // should ignore
expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3])
wrapper.vm.addMediaFile(i4)
expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3, i4])
})
it('Attachment descriptions', () => {
const wrapper = mount(PostStatusForm, mountOpts())
const i1 = { id: '1', url: 'a' }
wrapper.vm.addMediaFile(i1)
expect(wrapper.vm.newStatus.files).to.eql([i1])
wrapper.vm.editAttachment(i1, 'description')
expect(wrapper.vm.newStatus.mediaDescriptions['1']).to.eql('description')
})
}) })
it('Forces direct mode when replying to a DM, mastodon style subject handling', () => { describe('Draft saving', () => {
// We need to initialize pinia first which is happening here... beforeEach(() => {
const options = mountOpts({ setActivePinia(createTestingPinia({ stubActions: false }))
props: { useUsersStore().currentUser = currentUser
repliedStatus: { ...repliedStatus2, visibility: 'direct' }, vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
autoSaveOrNot(
it,
'should save when the button is clicked',
async (autoSave) => {
const wrapper = mount(PostStatusForm, mountOpts())
const store = useMergedConfigStore()
store.mergedConfig = {
autoSaveDraft: autoSave,
}
expect(useDraftsStore().draftsCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
await saveManually(wrapper)
expect(useDraftsStore().draftsCount).to.equal(1)
expect(useDraftsStore().draftsArray[0].status).to.equal('mew mew')
}, },
)
it('should auto-save if it is enabled', async function () {
vi.useFakeTimers()
const wrapper = mount(PostStatusForm, mountOpts())
const store = useMergedConfigStore()
store.mergedConfig = {
autoSaveDraft: true,
}
expect(useDraftsStore().draftsCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
expect(useDraftsStore().draftsCount).to.equal(0)
await vi.advanceTimersByTimeAsync(waitSaveTime)
expect(useDraftsStore().draftsCount).to.equal(1)
expect(useDraftsStore().draftsArray[0].status).to.equal('mew mew')
}) })
// ...set our settings... it('should auto-save when close if auto-save is on', async () => {
useMergedConfigStore().mergedConfig = { const wrapper = mount(
...useMergedConfigStore().mergedConfig, PostStatusForm,
subjectLineBehavior: 'masto', mountOpts({
} props: {
closeable: true,
// ...and only then mount our component },
const wrapper = mount(PostStatusForm, options) }),
)
// Otherwise we get multiple instances of pinia that don't talk to each other const store = useMergedConfigStore()
store.mergedConfig = {
expect(wrapper.vm.statusType).to.equal('reply') autoSaveDraft: true,
expect(wrapper.vm.isReply).to.equal(true) }
expect(wrapper.vm.quotable).to.equal(false) expect(useDraftsStore().draftsCount).to.equal(0)
expect(wrapper.vm.newStatus.quote).to.be.null const textarea = wrapper.get('textarea')
expect(wrapper.vm.newStatus.poll).to.be.null await textarea.setValue('mew mew')
expect(wrapper.vm.newStatus.spoilerText).to.eql('subject') wrapper.vm.requestClose()
expect(wrapper.vm.newStatus.mentions).to.eql('@replied') expect(useDraftsStore().draftsCount).to.equal(1)
expect(wrapper.vm.newStatus.status).to.eql('@replied ') await waitForEvent(wrapper, 'close-accepted')
expect(wrapper.vm.newStatus.visibility).to.eql('direct') })
})
it('should save when close if auto-save is off, and unsavedPostAction is save', async () => {
it('Sets status to statusText without mentions if mentions line is enabled', () => { const wrapper = mount(
const wrapper = mount( PostStatusForm,
PostStatusForm, mountOpts({
mountOpts({ props: {
props: { closeable: true,
repliedStatus: repliedStatus2, },
statusText: 'testing', }),
mentionsLine: true, )
}, const store = useMergedConfigStore()
}), store.mergedConfig = {
) autoSaveDraft: false,
unsavedPostAction: 'save',
expect(wrapper.vm.statusType).to.equal('reply') }
expect(wrapper.vm.isReply).to.equal(true) expect(useDraftsStore().draftsCount).to.equal(0)
expect(wrapper.vm.newStatus.mentions).to.eql('@replied') const textarea = wrapper.get('textarea')
expect(wrapper.vm.newStatus.status).to.eql('testing') await textarea.setValue('mew mew')
}) wrapper.vm.requestClose()
expect(useDraftsStore().draftsCount).to.equal(1)
it('Sets mention when asked for it', () => { await waitForEvent(wrapper, 'close-accepted')
const wrapper = mount( })
PostStatusForm,
mountOpts({ it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => {
props: { const wrapper = mount(
profileMention: repliedUser, PostStatusForm,
}, mountOpts({
}), props: {
) closeable: true,
},
expect(wrapper.vm.statusType).to.equal('mention') }),
expect(wrapper.vm.isReply).to.equal(false) )
expect(wrapper.vm.newStatus.mentions).to.eql('@replied') const store = useMergedConfigStore()
expect(wrapper.vm.newStatus.status).to.eql('@replied ') store.mergedConfig = {
}) autoSaveDraft: false,
unsavedPostAction: 'discard',
it('Initializes quote when reply/quote toggled to quote', () => { }
const wrapper = mount( expect(useDraftsStore().draftsCount).to.equal(0)
PostStatusForm, const textarea = wrapper.get('textarea')
mountOpts({ await textarea.setValue('mew mew')
props: { wrapper.vm.requestClose()
repliedStatus: repliedStatus2, await waitForEvent(wrapper, 'close-accepted')
}, expect(useDraftsStore().draftsCount).to.equal(0)
}), })
)
it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => {
expect(wrapper.vm.statusType).to.equal('reply') const store = useMergedConfigStore()
expect(wrapper.vm.isReply).to.equal(true) const wrapper = mount(
PostStatusForm,
wrapper.vm.quoteThreadToggled = true mountOpts({
props: {
expect(wrapper.vm.newStatus.quote).to.eql({ thread: true, id: 'status-2' }) closeable: true,
}) },
}),
it('Resets quote when reply/quote toggled to reply', () => { )
const wrapper = mount( store.mergedConfig = {
PostStatusForm, autoSaveDraft: false,
mountOpts({ unsavedPostAction: 'confirm',
props: { }
repliedStatus: repliedStatus2, expect(useDraftsStore().draftsCount).to.equal(0)
}, const textarea = wrapper.get('textarea')
}), await textarea.setValue('mew mew')
) wrapper.vm.requestClose()
await nextTick()
expect(wrapper.vm.statusType).to.equal('reply') await flushPromises()
expect(wrapper.vm.isReply).to.equal(true) const saveButton = await vi.waitFor(() => {
const button = wrapper.findByText(
wrapper.vm.quoteThreadToggled = true 'button',
wrapper.vm.quoteThreadToggled = false $t('post_status.close_confirm_save_button'),
)
expect(wrapper.vm.newStatus.quote).to.be.null if (!button) throw new Error('Save button not present')
}) return button
})
it('Initializes and reset quote when toggling quote attachment', () => { expect(saveButton).to.be.ok
const wrapper = mount( await saveButton.trigger('click')
PostStatusForm, console.info('clicked')
mountOpts({ expect(useDraftsStore().draftsCount).to.equal(1)
props: { await flushPromises()
repliedStatus: repliedStatus2, await waitForEvent(wrapper, 'close-accepted')
},
}),
)
expect(wrapper.vm.statusType).to.equal('reply')
expect(wrapper.vm.isReply).to.equal(true)
wrapper.vm.toggleQuoteForm()
expect(wrapper.vm.newStatus.quote).to.eql({
thread: false,
id: null,
url: '',
}) })
wrapper.vm.toggleQuoteForm()
expect(wrapper.vm.newStatus.quote).to.be.null
}) })
it('Status editing', () => {
const wrapper = mount(
PostStatusForm,
mountOpts({
props: {
statusId: 'edited',
statusText: 'text',
statusSubject: 'heading',
statusIsSensitive: true,
statusPoll: {},
statusQuote: {},
statusFiles: [],
statusMediaDescriptions: {},
statusVisibility: 'unlisted',
statusContentType: 'text/markdown',
},
}),
)
expect(wrapper.vm.statusType).to.equal('edit')
expect(wrapper.vm.isReply).to.equal(false) // edits don't support changing reply-to so it's pretty much ignored
expect(wrapper.vm.isEdit).to.equal(true)
expect(wrapper.vm.newStatus.quote).to.eql({})
expect(wrapper.vm.newStatus.poll).to.eql({})
expect(wrapper.vm.newStatus.spoilerText).to.eql('heading')
expect(wrapper.vm.newStatus.mentions).to.eql('')
expect(wrapper.vm.newStatus.status).to.eql('text')
expect(wrapper.vm.newStatus.visibility).to.eql('unlisted')
expect(wrapper.vm.newStatus.contentType).to.eql('text/markdown')
expect(wrapper.vm.newStatus.nsfw).to.equal(true)
expect(wrapper.vm.newStatus.files).to.eql([])
})
it('Posting should reset idempotency key', async () => {
vi.setSystemTime(new Date(2027, 1, 1, 13))
const wrapper = mount(PostStatusForm, mountOpts())
const oldIdempotency = wrapper.vm.idempotencyKey
vi.setSystemTime(new Date(2028, 1, 1, 13))
wrapper.vm.newStatus.status = 'Testing'
await wrapper.vm.postStatus()
expect(wrapper.vm.idempotencyKey).to.not.eql(oldIdempotency)
})
// TODO Probably better to separate attachment upload/manipulation into its own component?
// we need to upload-on-submit for compression setting anyway
it('Attachments manipulations (moving, adding, removing)', () => {
const wrapper = mount(PostStatusForm, mountOpts())
const i1 = { id: '1', url: 'a' }
const i2 = { id: '2', url: 'b' }
const i3 = { id: '3', url: 'c' }
const i4 = { id: '4', url: 'd' }
const iX = { id: 'x', url: 'x' }
wrapper.vm.newStatus.files = [i3, i1, iX, i2]
wrapper.vm.removeMediaFile(iX)
expect(wrapper.vm.newStatus.files).to.eql([i3, i1, i2])
wrapper.vm.shiftUpMediaFile(i1)
expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2])
wrapper.vm.shiftUpMediaFile(i1) // should ignore
expect(wrapper.vm.newStatus.files).to.eql([i1, i3, i2])
wrapper.vm.shiftDnMediaFile(i3)
expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3])
wrapper.vm.shiftDnMediaFile(i3) // should ignore
expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3])
wrapper.vm.addMediaFile(i4)
expect(wrapper.vm.newStatus.files).to.eql([i1, i2, i3, i4])
})
it('Attachment descriptions', () => {
const wrapper = mount(PostStatusForm, mountOpts())
const i1 = { id: '1', url: 'a' }
wrapper.vm.addMediaFile(i1)
expect(wrapper.vm.newStatus.files).to.eql([i1])
wrapper.vm.editAttachment(i1, 'description')
expect(wrapper.vm.newStatus.mediaDescriptions['1']).to.eql('description')
})
// TODO: Drafts (needs vuex to pinia migration)
}) })

View file

@ -1,9 +1,9 @@
import { createTestingPinia } from '@pinia/testing' import { createTestingPinia } from '@pinia/testing'
import { mount, shallowMount } from '@vue/test-utils' import { mount, shallowMount } from '@vue/test-utils'
import { setActivePinia } from 'pinia' import { setActivePinia } from 'pinia'
import { mountOpts } from 'test/fixtures/setup_test.js'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import { mountOpts } from '../../../fixtures/setup_test'
const attentions = [] const attentions = []

View file

@ -37,10 +37,7 @@ describe('The SyncConfig store', () => {
it('should initialize storage if none present', async () => { it('should initialize storage if none present', async () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
await store.initSyncConfig({ ...user }) await store.initSyncConfig({ ...user })
expect(store.cache._version).to.eql(VERSION) expect(store.cache._version).to.eql(VERSION)
expect(store.cache._timestamp).to.be.a('number') expect(store.cache._timestamp).to.be.a('number')
@ -50,10 +47,7 @@ describe('The SyncConfig store', () => {
it('should initialize storage with proper flags for new users if none present', async () => { it('should initialize storage with proper flags for new users if none present', async () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
await store.initSyncConfig({ ...user, created_at: new Date() }) await store.initSyncConfig({ ...user, created_at: new Date() })
expect(store.cache._version).to.eql(VERSION) expect(store.cache._version).to.eql(VERSION)
expect(store.cache._timestamp).to.be.a('number') expect(store.cache._timestamp).to.be.a('number')
@ -63,10 +57,7 @@ describe('The SyncConfig store', () => {
it('should merge flags even if remote timestamp is older', async () => { it('should merge flags even if remote timestamp is older', async () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
store.cache = { store.cache = {
_timestamp: Date.now(), _timestamp: Date.now(),
_version: VERSION, _version: VERSION,
@ -96,10 +87,7 @@ describe('The SyncConfig store', () => {
it('should trim journal to 500 entries', async () => { it('should trim journal to 500 entries', async () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
store.cache = { store.cache = {
_timestamp: Date.now(), _timestamp: Date.now(),
_version: VERSION, _version: VERSION,
@ -138,10 +126,7 @@ describe('The SyncConfig store', () => {
it('should reset local timestamp to remote if contents are the same', async () => { it('should reset local timestamp to remote if contents are the same', async () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
store.cache = null store.cache = null
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
await store.initSyncConfig({ await store.initSyncConfig({
...user, ...user,
@ -161,10 +146,7 @@ describe('The SyncConfig store', () => {
it('should use remote version if local missing', async () => { it('should use remote version if local missing', async () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
await store.initSyncConfig(store, user) await store.initSyncConfig(store, user)
expect(store.cache._version).to.eql(VERSION) expect(store.cache._version).to.eql(VERSION)
expect(store.cache._timestamp).to.be.a('number') expect(store.cache._timestamp).to.be.a('number')
@ -208,9 +190,7 @@ describe('The SyncConfig store', () => {
}) })
vi.spyOn(storage, 'setItem').mockResolvedValue() vi.spyOn(storage, 'setItem').mockResolvedValue()
const store = useSyncConfigStore() const store = useSyncConfigStore()
store.pushSyncConfig = () => { store.pushSyncConfig = vi.fn()
/* no-op */
}
await store.initSyncConfig({ await store.initSyncConfig({
...user, ...user,
@ -240,9 +220,7 @@ describe('The SyncConfig store', () => {
}) })
vi.spyOn(storage, 'setItem').mockResolvedValue() vi.spyOn(storage, 'setItem').mockResolvedValue()
const store = useSyncConfigStore() const store = useSyncConfigStore()
store.pushSyncConfig = () => { store.pushSyncConfig = vi.fn()
/* no-op */
}
await store.initSyncConfig({ await store.initSyncConfig({
...user, ...user,
@ -282,9 +260,7 @@ describe('The SyncConfig store', () => {
vi.spyOn(storage, 'setItem').mockResolvedValue() vi.spyOn(storage, 'setItem').mockResolvedValue()
const store = useSyncConfigStore() const store = useSyncConfigStore()
const setPreference = vi.spyOn(store, 'setPreference') const setPreference = vi.spyOn(store, 'setPreference')
store.pushSyncConfig = () => { store.pushSyncConfig = vi.fn()
/* no-op */
}
await store.initSyncConfig({ await store.initSyncConfig({
...user, ...user,
@ -318,9 +294,7 @@ describe('The SyncConfig store', () => {
}) })
vi.spyOn(storage, 'setItem').mockResolvedValue() vi.spyOn(storage, 'setItem').mockResolvedValue()
const store = useSyncConfigStore() const store = useSyncConfigStore()
store.pushSyncConfig = () => { store.pushSyncConfig = vi.fn()
/* no-op */
}
await store.initSyncConfig({ await store.initSyncConfig({
...user, ...user,
@ -357,9 +331,7 @@ describe('The SyncConfig store', () => {
const localStore = useLocalConfigStore() const localStore = useLocalConfigStore()
localStore.set({ path: 'fontInterface', value: 'Current interface' }) localStore.set({ path: 'fontInterface', value: 'Current interface' })
const store = useSyncConfigStore() const store = useSyncConfigStore()
store.pushSyncConfig = () => { store.pushSyncConfig = vi.fn()
/* no-op */
}
await store.initSyncConfig({ ...user }) await store.initSyncConfig({ ...user })
@ -372,10 +344,7 @@ describe('The SyncConfig store', () => {
describe('setPreference', () => { describe('setPreference', () => {
it('should set preference and update journal log accordingly', () => { it('should set preference and update journal log accordingly', () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
store.setPreference({ path: 'simple.palette', value: '1' }) store.setPreference({ path: 'simple.palette', value: '1' })
expect(store.prefsStorage.simple.palette).to.eql('1') expect(store.prefsStorage.simple.palette).to.eql('1')
expect(store.prefsStorage._journal).to.have.length(1) expect(store.prefsStorage._journal).to.have.length(1)
@ -390,10 +359,7 @@ describe('The SyncConfig store', () => {
it('should keep journal to a minimum', () => { it('should keep journal to a minimum', () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
store.setPreference({ path: 'simple.palette', value: 1 }) store.setPreference({ path: 'simple.palette', value: 1 })
store.setPreference({ path: 'simple.palette', value: 2 }) store.setPreference({ path: 'simple.palette', value: 2 })
store.addCollectionPreference({ path: 'collections.palette', value: 2 }) store.addCollectionPreference({ path: 'collections.palette', value: 2 })
@ -423,10 +389,7 @@ describe('The SyncConfig store', () => {
it('should remove duplicate entries from journal', () => { it('should remove duplicate entries from journal', () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
store.setPreference({ path: 'simple.palette', value: 1 }) store.setPreference({ path: 'simple.palette', value: 1 })
store.setPreference({ path: 'simple.palette', value: 1 }) store.setPreference({ path: 'simple.palette', value: 1 })
store.addCollectionPreference({ path: 'collections.palette', value: 2 }) store.addCollectionPreference({ path: 'collections.palette', value: 2 })
@ -440,10 +403,7 @@ describe('The SyncConfig store', () => {
// TODO We need a proper test for object-based stores // TODO We need a proper test for object-based stores
it.skip('should remove depth = 3 set/unset entries from journal', () => { it.skip('should remove depth = 3 set/unset entries from journal', () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
store.setPreference({ path: 'simple.fontInput', value: 'test' }) store.setPreference({ path: 'simple.fontInput', value: 'test' })
store.unsetPreference({ path: 'simple.fontInput' }) store.unsetPreference({ path: 'simple.fontInput' })
store.updateCache(store, { username: 'test' }) store.updateCache(store, { username: 'test' })
@ -455,10 +415,7 @@ describe('The SyncConfig store', () => {
it('should not allow unsetting depth <= 2', () => { it('should not allow unsetting depth <= 2', () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
store.setPreference({ path: 'simple.object.foo', value: 1 }) store.setPreference({ path: 'simple.object.foo', value: 1 })
expect(() => store.unsetPreference({ path: 'simple' })).to.throw() expect(() => store.unsetPreference({ path: 'simple' })).to.throw()
expect(() => expect(() =>
@ -468,10 +425,7 @@ describe('The SyncConfig store', () => {
it('should not allow (un)setting depth > 3', () => { it('should not allow (un)setting depth > 3', () => {
const store = useSyncConfigStore() const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data store.pushSyncConfig = vi.fn()
store.pushSyncConfig = () => {
/* no-op */
}
store.setPreference({ path: 'simple.object', value: {} }) store.setPreference({ path: 'simple.object', value: {} })
expect(() => expect(() =>
store.setPreference({ path: 'simple.object.lv3', value: 1 }), store.setPreference({ path: 'simple.object.lv3', value: 1 }),

View file

@ -2,6 +2,7 @@ import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import { playwright } from '@vitest/browser-playwright' import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import eslint from 'vite-plugin-eslint2' import eslint from 'vite-plugin-eslint2'
@ -120,6 +121,7 @@ export default defineConfig(async ({ mode, command }) => {
const swDest = 'sw-pleroma.js' const swDest = 'sw-pleroma.js'
const alias = { const alias = {
src: '/src', src: '/src',
test: '/test',
components: '/src/components', components: '/src/components',
...(mode === 'test' ? { vue: 'vue/dist/vue.esm-bundler.js' } : {}), ...(mode === 'test' ? { vue: 'vue/dist/vue.esm-bundler.js' } : {}),
} }
@ -141,6 +143,7 @@ export default defineConfig(async ({ mode, command }) => {
}, },
}, },
}), }),
vueDevTools(),
vueJsx(), vueJsx(),
buildSwPlugin({ swSrc, swDest }), buildSwPlugin({ swSrc, swDest }),
swMessagesPlugin(), swMessagesPlugin(),

4542
yarn.lock

File diff suppressed because it is too large Load diff