Compare commits
2 commits
shigusegub
...
shigusegub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8163366402 | ||
|
|
2884d6e9ba |
|
|
@ -1,12 +0,0 @@
|
|||
node_modules/
|
||||
dist/
|
||||
logs/
|
||||
.DS_Store
|
||||
.git/
|
||||
config/local.json
|
||||
pleroma-backend/
|
||||
test/e2e/reports/
|
||||
test/e2e-playwright/test-results/
|
||||
test/e2e-playwright/playwright-report/
|
||||
__screenshots__/
|
||||
|
||||
2
.eslintignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
build/*.js
|
||||
config/*.js
|
||||
27
.eslintrc.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow paren-less arrow functions
|
||||
'arrow-parens': 0,
|
||||
// allow async-await
|
||||
'generator-star-spacing': 0,
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'vue/require-prop-types': 0,
|
||||
'vue/multi-word-component-names': 0
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
name: 'Bug report'
|
||||
about: 'Bug report for Pleroma FE'
|
||||
labels:
|
||||
- Bug
|
||||
body:
|
||||
- type: input
|
||||
id: env-browser
|
||||
attributes:
|
||||
label: Browser and OS
|
||||
description: What browser are you using, including version, and what OS are you running?
|
||||
placeholder: Firefox 140, Arch Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-instance
|
||||
attributes:
|
||||
label: Instance URL
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: env-backend
|
||||
attributes:
|
||||
label: Backend version information
|
||||
description: Backend version being used. (See Settings->Show advanced->Developer)
|
||||
placeholder: Pleroma BE 2.10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-frontend
|
||||
attributes:
|
||||
label: Frontend version information
|
||||
description: Frontend version being used. (See Settings->Show advanced->Developer)
|
||||
placeholder: Pleroma FE 2.10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-extensions
|
||||
attributes:
|
||||
label: Browser extensions
|
||||
description: List of browser extensions you are using, like uBlock, rikaichamp etc. If none leave empty.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: env-modifications
|
||||
attributes:
|
||||
label: Known instance/user customizations
|
||||
description: Whether you are using a Pleroma FE fork, any mods mods or instance level styles among others.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: bug-text
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: A short description of the bug. Images can be helpful.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-reproducer
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Ordered list of reproduction steps needed to make the bug happen. If you don't have reproduction steps, leave this empty.
|
||||
placeholder: |
|
||||
1. Log in with a fresh browser session
|
||||
2. Open timeline X
|
||||
3. Click on button Y
|
||||
4. Z broke
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: bug-seriousness
|
||||
attributes:
|
||||
label: Bug seriousness
|
||||
value: |
|
||||
* How annoying it is:
|
||||
* How often does it happen:
|
||||
* How many people does it affect:
|
||||
* Is there a workaround for it:
|
||||
- type: checkboxes
|
||||
id: duplicate-issues
|
||||
attributes:
|
||||
label: Duplicate issues
|
||||
hide_label: true
|
||||
description: Before submitting this issue, search for same or similar issues on the [Pleroma FE bug tracker](https://git.pleroma.social/pleroma/pleroma-fe/issues).
|
||||
options:
|
||||
- label: I've searched for same or similar issues before submitting this issue.
|
||||
required: true
|
||||
visible: [form]
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
name: 'Feature request / Suggestion / Improvement'
|
||||
about: 'Feature requests, suggestions and improvements for Pleroma FE'
|
||||
labels:
|
||||
- Feature Request / Enhancement
|
||||
body:
|
||||
- type: textarea
|
||||
id: issue-text
|
||||
attributes:
|
||||
label: Proposal
|
||||
placeholder: Make groups happen!
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: duplicate-issues
|
||||
attributes:
|
||||
label: Duplicate issues
|
||||
hide_label: true
|
||||
description: Before submitting this issue, search for same or similar requests on the [Pleroma FE bug tracker](https://git.pleroma.social/pleroma/pleroma-fe/issues).
|
||||
options:
|
||||
- label: I've searched for same or similar requests before submitting this issue.
|
||||
required: true
|
||||
visible: [form]
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
### Checklist
|
||||
- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `<code>.<type>`.
|
||||
|
||||
<!--
|
||||
`<code>` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name.
|
||||
|
||||
`<type>` can be `add`, `change`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the MR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change.
|
||||
|
||||
In the file, write the changelog entry. For example, if an MR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it.
|
||||
|
||||
If one changelog entry is not enough, you may add more. But that might mean you can split it into two MRs. Only use more than one changelog entry if you really need to (for example, when one change in the code fix two different bugs, or when refactoring).
|
||||
-->
|
||||
2
.gitattributes
vendored
|
|
@ -1 +1 @@
|
|||
/build/commit_hash.js export-subst
|
||||
/build/webpack.prod.conf.js export-subst
|
||||
|
|
|
|||
6
.gitignore
vendored
|
|
@ -4,12 +4,8 @@ dist/
|
|||
npm-debug.log
|
||||
test/unit/coverage
|
||||
test/e2e/reports
|
||||
test/e2e-playwright/test-results
|
||||
test/e2e-playwright/playwright-report
|
||||
selenium-debug.log
|
||||
.idea/
|
||||
.gitlab-ci-local/
|
||||
config/local.json
|
||||
src/assets/emoji.json
|
||||
static/emoji.json
|
||||
logs/
|
||||
__screenshots__/
|
||||
|
|
|
|||
161
.gitlab-ci.yml
|
|
@ -1,7 +1,7 @@
|
|||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official framework image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/node/tags/
|
||||
image: node:18
|
||||
image: node:16
|
||||
|
||||
stages:
|
||||
- check-changelog
|
||||
|
|
@ -34,23 +34,12 @@ check-changelog:
|
|||
- apk add git
|
||||
- sh ./tools/check-changelog
|
||||
|
||||
lint-eslint:
|
||||
lint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-eslint
|
||||
|
||||
lint-biome:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-biome
|
||||
|
||||
lint-stylelint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-stylelint
|
||||
- npm run lint
|
||||
- npm run stylelint
|
||||
|
||||
test:
|
||||
stage: test
|
||||
|
|
@ -61,144 +50,10 @@ test:
|
|||
APT_CACHE_DIR: apt-cache
|
||||
script:
|
||||
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
||||
- apt install firefox-esr -y --no-install-recommends
|
||||
- firefox --version
|
||||
- yarn
|
||||
- yarn playwright install firefox
|
||||
- yarn playwright install-deps
|
||||
- yarn unit-ci
|
||||
artifacts:
|
||||
# When the test fails, upload screenshots for better context on why it fails
|
||||
paths:
|
||||
- test/**/__screenshots__
|
||||
when: on_failure
|
||||
|
||||
e2e-pleroma:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
services:
|
||||
- name: postgres:15-alpine
|
||||
alias: db
|
||||
- name: $PLEROMA_IMAGE
|
||||
alias: pleroma
|
||||
entrypoint: ["/bin/ash", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -eu
|
||||
|
||||
SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded
|
||||
CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs
|
||||
|
||||
echo '-- Waiting for database...'
|
||||
while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
echo '-- Writing E2E config overrides...'
|
||||
cat > $CONFIG_OVERRIDE_PATH <<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:5050/pleroma/pleroma:stable
|
||||
POSTGRES_USER: pleroma
|
||||
POSTGRES_PASSWORD: pleroma
|
||||
POSTGRES_DB: pleroma
|
||||
DB_USER: pleroma
|
||||
DB_PASS: pleroma
|
||||
DB_NAME: pleroma
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
DOMAIN: localhost
|
||||
INSTANCE_NAME: Pleroma E2E
|
||||
E2E_ADMIN_USERNAME: admin
|
||||
E2E_ADMIN_PASSWORD: adminadmin
|
||||
E2E_ADMIN_EMAIL: admin@example.com
|
||||
ADMIN_EMAIL: $E2E_ADMIN_EMAIL
|
||||
NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
|
||||
VITE_PROXY_TARGET: http://pleroma:4000
|
||||
VITE_PROXY_ORIGIN: http://localhost:4000
|
||||
E2E_BASE_URL: http://localhost:8080
|
||||
script:
|
||||
- npm install -g yarn@1.22.22
|
||||
- yarn --frozen-lockfile
|
||||
- |
|
||||
echo "-- Waiting for Pleroma API..."
|
||||
api_ok="false"
|
||||
for _i in $(seq 1 120); do
|
||||
if wget -qO- http://pleroma:4000/api/v1/instance >/dev/null 2>&1; then
|
||||
api_ok="true"
|
||||
break
|
||||
fi
|
||||
sleep 1s
|
||||
done
|
||||
if [ "$api_ok" != "true" ]; then
|
||||
echo "Timed out waiting for Pleroma API to become available"
|
||||
exit 1
|
||||
fi
|
||||
- yarn e2e:pw
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- test/e2e-playwright/test-results
|
||||
- test/e2e-playwright/playwright-report
|
||||
- yarn unit
|
||||
|
||||
build:
|
||||
stage: build
|
||||
|
|
@ -207,7 +62,7 @@ build:
|
|||
- himem
|
||||
script:
|
||||
- yarn
|
||||
- yarn build
|
||||
- npm run build
|
||||
artifacts:
|
||||
paths:
|
||||
- dist/
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
### Release checklist
|
||||
* [ ] Bump version in `package.json`
|
||||
* [ ] Compile a changelog with the `tools/collect-changelog` script
|
||||
* [ ] Create an MR with an announcement to pleroma.social
|
||||
#### post-merge
|
||||
* [ ] Tag the release on the merge commit
|
||||
* [ ] Make the tag into a Gitlab Release™
|
||||
* [ ] Merge `master` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs)
|
||||
|
|
@ -1 +1 @@
|
|||
18.20.8
|
||||
16.18.1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-rscss/config",
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-scss",
|
||||
"stylelint-config-html",
|
||||
|
|
@ -7,13 +8,20 @@
|
|||
],
|
||||
"rules": {
|
||||
"declaration-no-important": true,
|
||||
"rscss/no-descendant-combinator": false,
|
||||
"rscss/class-format": [
|
||||
false,
|
||||
{
|
||||
"component": "pascal-case",
|
||||
"variant": "^-[a-z]\\w+",
|
||||
"element": "^[a-z]\\w+"
|
||||
}
|
||||
],
|
||||
"selector-class-pattern": null,
|
||||
"import-notation": null,
|
||||
"custom-property-pattern": null,
|
||||
"keyframes-name-pattern": null,
|
||||
"scss/operator-no-newline-after": null,
|
||||
"declaration-property-value-no-unknown": true,
|
||||
"scss/declaration-property-value-no-unknown": true,
|
||||
"declaration-block-no-redundant-longhand-properties": [
|
||||
true,
|
||||
{
|
||||
|
|
|
|||
138
CHANGELOG.md
|
|
@ -3,144 +3,6 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## 2.10.1
|
||||
### Fixed
|
||||
- fixed being unable to set actor type from profile page
|
||||
- fixed error when clicking mute menu itself (instead of submenu items)
|
||||
- fixed mute -> domain status submenu not working
|
||||
|
||||
### Internal
|
||||
- Add playwright E2E-tests with an optional docker-based backend
|
||||
|
||||
## 2.10.0
|
||||
### Changed
|
||||
- Temporary changes modal now shows actual countdown instead of fixed timeout
|
||||
- Disabled elements are more disabled now
|
||||
- Rearranged and split settings to make more sense and be less of a wall of text
|
||||
- On mobile settings now take up full width and presented in navigation style
|
||||
improved styles for settings
|
||||
|
||||
### Added
|
||||
- Most of the remaining AdminFE tabs were added into Admin Dashboard
|
||||
- It's now possible to customize PWA Manfiest from PleromaFE
|
||||
- Make every configuration option default-overridable by instance admins
|
||||
|
||||
### Fixed
|
||||
- Fixed settings not appearing if user never touched "show advanced" toggle
|
||||
- Fix display of the broken/deleted/banned users
|
||||
- Fixed incorrect emoji display in post interaction lists
|
||||
- Fixed list title not being saved when editing
|
||||
- Fixed poll notifications not being expandable
|
||||
|
||||
|
||||
## 2.9.3
|
||||
### Fixed
|
||||
- Being unable to update profile
|
||||
|
||||
## 2.9.2
|
||||
### Changed
|
||||
- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible
|
||||
- User card/profile got an overhaul
|
||||
- Profile editing overhaul
|
||||
- Visually combined subject and content fields in post form
|
||||
- Moved post form's emoji button into input field
|
||||
- Minor visual changes and fixes
|
||||
- Clicking on fav/rt/emoji notifications' contents expands/collapses it
|
||||
- Reduced time taken processing theme by half
|
||||
- Splash screen only appears if loading takes more than 2 seconds
|
||||
|
||||
### Added
|
||||
- Mutes received an update, adding support for regex, muting based on username and expiration time.
|
||||
- Mutes are now synchronized across sessions
|
||||
- Support for expiring mutes and blocks (if available)
|
||||
- Clicking on emoji shows bigger version of it alongside with its shortcode
|
||||
- Admins also are able to copy it into a local pack
|
||||
- Added support for Akkoma and IceShrimp.NET backends
|
||||
- Compatibility with stricter CSP (Akkoma backend)
|
||||
- Added a way to upload new packs from a URL or ZIP file via the Admin Dashboard
|
||||
- Unify show/hide content buttons
|
||||
- Add support for detachable scrollTop button
|
||||
- Option to left-align user bio
|
||||
- Cache assets and emojis with service worker
|
||||
- Indicate currently active V3 theme as a body element class
|
||||
- Add arithmetic blend ISS function
|
||||
|
||||
### Fixed
|
||||
- Display counter for status action buttons when they are in the menu
|
||||
- Fix bookmark button alignment in the extra actions menu
|
||||
- Instance favicons are no longer stretched
|
||||
- A lot more scalable UI fixes
|
||||
- Emoji picker now should work fine when emoji size is increased
|
||||
|
||||
## 2.8.0
|
||||
### Changed
|
||||
- BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image
|
||||
- BREAKING: static/emoji.json is replaced with a properly hashed path under static/js in the production build, meaning server admins cannot provide their own set of unicode emojis by overriding this file (custom (image-based) emojis not affected)
|
||||
- Speed up initial boot.
|
||||
- Updated our build system to support browsers:
|
||||
Safari >= 15
|
||||
Firefox >= 115
|
||||
Android > 4
|
||||
no Opera Mini support
|
||||
no IE support
|
||||
no "dead" (unmaintained) browsers support
|
||||
|
||||
This does not guarantee that browsers will or will not work.
|
||||
|
||||
- Use /api/v1/accounts/:id/follow for account subscriptions instead of the deprecated routes
|
||||
- Modal layout for mobile has new layout to make it easy to use
|
||||
- Better display of mute reason on posts
|
||||
- Simplify the OAuth client_name to 'PleromaFE'
|
||||
- Partially migrated from vuex to pinia
|
||||
- Authenticate and subscribe to streaming after connection
|
||||
- Tabs now have indentation for better visibility of which tab is currently active
|
||||
- Upgraded Vue to version 3.5
|
||||
|
||||
### Added
|
||||
- Support bookmark folders
|
||||
- Some new default color schemes
|
||||
- Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree
|
||||
- Post actions customization
|
||||
- Support displaying time in absolute format
|
||||
- Add draft management system
|
||||
- Compress most kinds of images on upload.
|
||||
- Added option to always convert images to JPEG format instead of using WebP when compressing images.
|
||||
- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
|
||||
- Inform users that Smithereen public polls are public
|
||||
- Splash screen + loading indicator to make process of identifying initialization issues and load performance
|
||||
- UI for making v3 themes and palettes, support for bundling v3 themes
|
||||
- Make UserLink wrappable
|
||||
|
||||
### Fixed
|
||||
- Fixed occasional overflows in emoji picker and made header scrollable
|
||||
- Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name.
|
||||
- Checkbox vertical alignment
|
||||
- Check for canvas extract permission when initializing favicon service
|
||||
- Fix some of the color manipulation functions
|
||||
- Fix draft saving when auto-save is off
|
||||
- Switch from class hack to normalButton attribute for emoji count popover
|
||||
- Fix emoji inconsistencies in notifications,
|
||||
- Fix some emoji not scaling with interface
|
||||
- Make sure hover style is also applied to :focus-visible
|
||||
- Improved ToS and registration
|
||||
- Fix small markup inconsistencies
|
||||
- Fixed modals buttons overflow
|
||||
- Fix whitespaces for multiple status mute reasons, display bot status reason
|
||||
- Create an OAuth app only when needed
|
||||
- Fix CSS compatibility issues in style_setter.js for older browsers like Palemoon
|
||||
- Proper sticky header for conversations on user page
|
||||
- Add text label for more actions button in post status form
|
||||
- Reply-or-quote buttons now take less space
|
||||
- Allow repeats of own posts with private scopes
|
||||
- Bookmarks visible again on mobile
|
||||
- Remove focusability on hidden popover in subject input
|
||||
- Show only month and day instead of weird "day, hour" format.
|
||||
|
||||
### Removed
|
||||
- BREAKING: drop support for browsers that do not support `<script type="module">`
|
||||
- BREAKING: css source map does not work in production (see https://github.com/vitejs/vite/issues/2830 )
|
||||
- Remove emoji annotations code for unused languages from final build
|
||||
|
||||
## 2.7.1
|
||||
Bugfix release. Added small optimizations to emoji picker that should make it a bit more responsive, however it needs rather large change to make it more performant which might come in a major release.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
# For Translators
|
||||
|
||||
To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/languages.js).
|
||||
To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/languages.js).
|
||||
|
||||
Pleroma-FE will set your language by your browser locale, but you can change language in settings.
|
||||
|
||||
|
|
@ -32,10 +32,10 @@ yarn unit
|
|||
|
||||
# For Contributors:
|
||||
|
||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/src/config/local.example.json)) to enable some convenience dev options:
|
||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
|
||||
|
||||
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
|
||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/pleroma/frontend_configurations`. Only works in dev mode.
|
||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
|
||||
|
||||
FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE.
|
||||
|
||||
|
|
|
|||
149
biome.json
|
|
@ -1,149 +0,0 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"includes": ["**", "!!**/dist", "!!tools/emojis.json"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"domains": {
|
||||
"vue": "recommended"
|
||||
},
|
||||
"rules": {
|
||||
"recommended": false,
|
||||
"complexity": {
|
||||
"noAdjacentSpacesInRegex": "error",
|
||||
"noExtraBooleanCast": "error",
|
||||
"noUselessCatch": "error",
|
||||
"noUselessEscapeInRegex": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noConstAssign": "error",
|
||||
"noConstantCondition": "error",
|
||||
"noEmptyCharacterClassInRegex": "error",
|
||||
"noEmptyPattern": "error",
|
||||
"noGlobalObjectCalls": "error",
|
||||
"noInvalidBuiltinInstantiation": "error",
|
||||
"noInvalidConstructorSuper": "error",
|
||||
"noNonoctalDecimalEscape": "error",
|
||||
"noPrecisionLoss": "error",
|
||||
"noSelfAssign": "error",
|
||||
"noSetterReturn": "error",
|
||||
"noSwitchDeclarations": "error",
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnreachable": "error",
|
||||
"noUnreachableSuper": "error",
|
||||
"noUnsafeFinally": "error",
|
||||
"noUnsafeOptionalChaining": "error",
|
||||
"noUnusedLabels": "error",
|
||||
"noUnusedPrivateClassMembers": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"useIsNan": "error",
|
||||
"useValidForDirection": "error",
|
||||
"useValidTypeof": "error",
|
||||
"useYield": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noAsyncPromiseExecutor": "error",
|
||||
"noCatchAssign": "error",
|
||||
"noClassAssign": "error",
|
||||
"noCompareNegZero": "error",
|
||||
"noConstantBinaryExpressions": "error",
|
||||
"noControlCharactersInRegex": "error",
|
||||
"noDebugger": "error",
|
||||
"noDuplicateCase": "error",
|
||||
"noDuplicateClassMembers": "error",
|
||||
"noDuplicateElseIf": "error",
|
||||
"noDuplicateObjectKeys": "error",
|
||||
"noDuplicateParameters": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noFallthroughSwitchClause": "error",
|
||||
"noFunctionAssign": "error",
|
||||
"noGlobalAssign": "error",
|
||||
"noImportAssign": "error",
|
||||
"noIrregularWhitespace": "error",
|
||||
"noMisleadingCharacterClass": "error",
|
||||
"noPrototypeBuiltins": "error",
|
||||
"noRedeclare": "error",
|
||||
"noShadowRestrictedNames": "error",
|
||||
"noSparseArray": "error",
|
||||
"noUnsafeNegation": "error",
|
||||
"noUselessRegexBackrefs": "error",
|
||||
"noWith": "error",
|
||||
"useGetterReturn": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "asNeeded"
|
||||
},
|
||||
"globals": []
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.spec.js", "test/fixtures/*.js"],
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"vi",
|
||||
"describe",
|
||||
"it",
|
||||
"test",
|
||||
"expect",
|
||||
"before",
|
||||
"beforeEach",
|
||||
"after",
|
||||
"afterEach"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.vue"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "off",
|
||||
"noUnusedImports": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
[":NODE:", ":PACKAGE:", "!src/**", "!@fortawesome/**"],
|
||||
":BLANK_LINE:",
|
||||
[":PATH:", "src/components/**"],
|
||||
":BLANK_LINE:",
|
||||
[":PATH:", "src/stores/**"],
|
||||
":BLANK_LINE:",
|
||||
[":PATH:", "src/**", "src/stores/**", "src/components/**"],
|
||||
":BLANK_LINE:",
|
||||
"@fortawesome/fontawesome-svg-core",
|
||||
"@fortawesome/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
build/build.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// https://github.com/shelljs/shelljs
|
||||
require('./check-versions')()
|
||||
require('shelljs/global')
|
||||
env.NODE_ENV = 'production'
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var ora = require('ora')
|
||||
var webpack = require('webpack')
|
||||
var webpackConfig = require('./webpack.prod.conf')
|
||||
|
||||
console.log(
|
||||
' Tip:\n' +
|
||||
' Built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
)
|
||||
|
||||
var spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
var updateEmoji = require('./update-emoji').updateEmoji
|
||||
updateEmoji()
|
||||
|
||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||
rm('-rf', assetsPath)
|
||||
mkdir('-p', assetsPath)
|
||||
cp('-R', 'static/*', assetsPath)
|
||||
|
||||
webpack(webpackConfig, function (err, stats) {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n')
|
||||
if (stats.hasErrors()) {
|
||||
console.error('See above for errors.')
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
45
build/check-versions.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
var semver = require('semver')
|
||||
var chalk = require('chalk')
|
||||
var packageConfig = require('../package.json')
|
||||
var exec = function (cmd) {
|
||||
return require('child_process')
|
||||
.execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
var versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node
|
||||
},
|
||||
{
|
||||
name: 'npm',
|
||||
currentVersion: exec('npm --version'),
|
||||
versionRequirement: packageConfig.engines.npm
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = function () {
|
||||
var warnings = []
|
||||
for (var i = 0; i < versionRequirements.length; i++) {
|
||||
var mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log()
|
||||
for (var i = 0; i < warnings.length; i++) {
|
||||
var warning = warnings[i]
|
||||
console.log(' ' + warning)
|
||||
}
|
||||
console.log()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import chalk from 'chalk'
|
||||
import semver from 'semver'
|
||||
|
||||
import packageConfig from '../package.json' with { type: 'json' }
|
||||
|
||||
var versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node,
|
||||
},
|
||||
]
|
||||
|
||||
export default function () {
|
||||
const warnings = []
|
||||
for (let i = 0; i < versionRequirements.length; i++) {
|
||||
const mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(
|
||||
mod.name +
|
||||
': ' +
|
||||
chalk.red(mod.currentVersion) +
|
||||
' should be ' +
|
||||
chalk.green(mod.versionRequirement),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
'\nTo use this template, you must update following to modules:\n',
|
||||
),
|
||||
)
|
||||
for (let i = 0; i < warnings.length; i++) {
|
||||
const warning = warnings[i]
|
||||
console.warn(' ' + warning)
|
||||
}
|
||||
console.warn()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import childProcess from 'child_process'
|
||||
|
||||
export const getCommitHash = () => {
|
||||
const subst = '$Format:%h$'
|
||||
if (!subst.match(/Format:/)) {
|
||||
return subst
|
||||
} else {
|
||||
try {
|
||||
return childProcess
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString()
|
||||
.trim()
|
||||
} catch (e) {
|
||||
console.error('Failed run git:', e)
|
||||
return 'UNKNOWN'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { cp } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import serveStatic from 'serve-static'
|
||||
|
||||
const getPrefix = (s) => {
|
||||
const padEnd = s.endsWith('/') ? s : s + '/'
|
||||
return padEnd.startsWith('/') ? padEnd : '/' + padEnd
|
||||
}
|
||||
|
||||
const copyPlugin = ({ inUrl, inFs }) => {
|
||||
const prefix = getPrefix(inUrl)
|
||||
const subdir = prefix.slice(1)
|
||||
let copyTarget
|
||||
const handler = serveStatic(inFs)
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'copy-plugin-serve',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
server.middlewares.use(prefix, handler)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copy-plugin-build',
|
||||
apply: 'build',
|
||||
configResolved(config) {
|
||||
copyTarget = resolve(config.root, config.build.outDir, subdir)
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler() {
|
||||
console.info(`Copying '${inFs}' to ${copyTarget}...`)
|
||||
await cp(inFs, copyTarget, { recursive: true })
|
||||
console.info('Done.')
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export default copyPlugin
|
||||
9
build/dev-client.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* eslint-disable */
|
||||
require('eventsource-polyfill')
|
||||
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
|
||||
|
||||
hotClient.subscribe(function (event) {
|
||||
if (event.action === 'reload') {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
81
build/dev-server.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
require('./check-versions')()
|
||||
var config = require('../config')
|
||||
if (!process.env.NODE_ENV) process.env.NODE_ENV = config.dev.env
|
||||
var path = require('path')
|
||||
var express = require('express')
|
||||
var webpack = require('webpack')
|
||||
var opn = require('opn')
|
||||
var proxyMiddleware = require('http-proxy-middleware')
|
||||
var webpackConfig = process.env.NODE_ENV === 'testing'
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
|
||||
var updateEmoji = require('./update-emoji').updateEmoji
|
||||
updateEmoji()
|
||||
|
||||
// default port where dev server listens for incoming traffic
|
||||
var port = process.env.PORT || config.dev.port
|
||||
// Define HTTP proxies to your custom API backend
|
||||
// https://github.com/chimurai/http-proxy-middleware
|
||||
var proxyTable = config.dev.proxyTable
|
||||
|
||||
var app = express()
|
||||
var compiler = webpack(webpackConfig)
|
||||
|
||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
writeToDisk: true,
|
||||
stats: {
|
||||
colors: true,
|
||||
chunks: false
|
||||
}
|
||||
})
|
||||
|
||||
var hotMiddleware = require('webpack-hot-middleware')(compiler)
|
||||
|
||||
// FIXME: The statement below gives error about hooks being required in webpack 5.
|
||||
// force page reload when html-webpack-plugin template changes
|
||||
// compiler.plugin('compilation', function (compilation) {
|
||||
// compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||
// // FIXME: This supposed to reload whole page when index.html is changed,
|
||||
// // however now it reloads entire page on every breath, i suppose the order
|
||||
// // of plugins changed or something. It's a minor thing and douesn't hurt
|
||||
// // disabling it, constant reloads hurt much more
|
||||
|
||||
// // hotMiddleware.publish({ action: 'reload' })
|
||||
// // cb()
|
||||
// })
|
||||
// })
|
||||
|
||||
// proxy api requests
|
||||
Object.keys(proxyTable).forEach(function (context) {
|
||||
var options = proxyTable[context]
|
||||
if (typeof options === 'string') {
|
||||
options = { target: options }
|
||||
}
|
||||
app.use(proxyMiddleware.createProxyMiddleware(context, options))
|
||||
})
|
||||
|
||||
// handle fallback for HTML5 history API
|
||||
app.use(require('connect-history-api-fallback')())
|
||||
|
||||
// serve webpack bundle output
|
||||
app.use(devMiddleware)
|
||||
|
||||
// enable hot-reload and state-preserving
|
||||
// compilation error display
|
||||
app.use(hotMiddleware)
|
||||
|
||||
// serve pure static assets
|
||||
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
app.use(staticPath, express.static('./static'))
|
||||
|
||||
module.exports = app.listen(port, function (err) {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
return
|
||||
}
|
||||
var uri = 'http://localhost:' + port
|
||||
console.log('Listening at ' + uri + '\n')
|
||||
// opn(uri)
|
||||
})
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import { access } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { languages } from '../src/i18n/languages.js'
|
||||
|
||||
const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/'
|
||||
const specialAnnotationsLocale = {
|
||||
ja_easy: 'ja',
|
||||
}
|
||||
|
||||
const internalToAnnotationsLocale = (internal) =>
|
||||
specialAnnotationsLocale[internal] || internal
|
||||
|
||||
// This gets all the annotations that are accessible (whose language
|
||||
// can be chosen in the settings). Data for other languages are
|
||||
// discarded because there is no way for it to be fetched.
|
||||
const getAllAccessibleAnnotations = async (projectRoot) => {
|
||||
const imports = (
|
||||
await Promise.all(
|
||||
languages.map(async (lang) => {
|
||||
const destLang = internalToAnnotationsLocale(lang)
|
||||
const importModule = `${annotationsImportPrefix}${destLang}.json`
|
||||
const importFile = resolve(projectRoot, 'node_modules', importModule)
|
||||
try {
|
||||
await access(importFile)
|
||||
return `'${lang}': () => import('${importModule}')`
|
||||
} catch (e) {
|
||||
if (e.message.match(/ENOENT/)) {
|
||||
console.warn(`Missing emoji annotations locale: ${destLang}`)
|
||||
} else {
|
||||
console.error('test', e.message)
|
||||
}
|
||||
return
|
||||
}
|
||||
}),
|
||||
)
|
||||
)
|
||||
.filter((k) => k)
|
||||
.join(',\n')
|
||||
|
||||
return `
|
||||
export const annotationsLoader = {
|
||||
${imports}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
const emojiAnnotationsId = 'virtual:pleroma-fe/emoji-annotations'
|
||||
const emojiAnnotationsIdResolved = '\0' + emojiAnnotationsId
|
||||
|
||||
const emojisPlugin = () => {
|
||||
let projectRoot
|
||||
return {
|
||||
name: 'emojis-plugin',
|
||||
configResolved(conf) {
|
||||
projectRoot = conf.root
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === emojiAnnotationsId) {
|
||||
return emojiAnnotationsIdResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
async load(id) {
|
||||
if (id === emojiAnnotationsIdResolved) {
|
||||
return await getAllAccessibleAnnotations(projectRoot)
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default emojisPlugin
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const target = 'node_modules/msw/lib/mockServiceWorker.js'
|
||||
|
||||
const mswPlugin = () => {
|
||||
let projectRoot
|
||||
return {
|
||||
name: 'msw-plugin',
|
||||
apply: 'serve',
|
||||
configResolved(conf) {
|
||||
projectRoot = conf.root
|
||||
},
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
if (req.path === '/mockServiceWorker.js') {
|
||||
const file = await readFile(resolve(projectRoot, target))
|
||||
res.set('Content-Type', 'text/javascript')
|
||||
res.send(file)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default mswPlugin
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { langCodeToJsonName, languages } from '../src/i18n/languages.js'
|
||||
|
||||
const i18nDir = resolve(
|
||||
dirname(dirname(fileURLToPath(import.meta.url))),
|
||||
'src/i18n',
|
||||
)
|
||||
|
||||
export const i18nFiles = languages.reduce((acc, lang) => {
|
||||
const name = langCodeToJsonName(lang)
|
||||
const file = resolve(i18nDir, name + '.json')
|
||||
acc[lang] = file
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
export const generateServiceWorkerMessages = async () => {
|
||||
const msgArray = await Promise.all(
|
||||
Object.entries(i18nFiles).map(async ([lang, file]) => {
|
||||
const fileContent = await readFile(file, 'utf-8')
|
||||
const msg = {
|
||||
notifications: JSON.parse(fileContent).notifications || {},
|
||||
}
|
||||
return [lang, msg]
|
||||
}),
|
||||
)
|
||||
return msgArray.reduce((acc, [lang, msg]) => {
|
||||
acc[lang] = msg
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import * as esbuild from 'esbuild'
|
||||
import { build } from 'vite'
|
||||
|
||||
import {
|
||||
generateServiceWorkerMessages,
|
||||
i18nFiles,
|
||||
} from './service_worker_messages.js'
|
||||
|
||||
const getSWMessagesAsText = async () => {
|
||||
const messages = await generateServiceWorkerMessages()
|
||||
return `export default ${JSON.stringify(messages, undefined, 2)}`
|
||||
}
|
||||
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
|
||||
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
||||
const swEnvNameResolved = '\0' + swEnvName
|
||||
const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };`
|
||||
const getProdSwEnv = ({ assets }) =>
|
||||
`self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||
|
||||
export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => {
|
||||
const swFullSrc = resolve(projectRoot, swSrc)
|
||||
const esbuildAlias = {}
|
||||
Object.entries(alias).forEach(([source, dest]) => {
|
||||
esbuildAlias[source] = dest.startsWith('/') ? projectRoot + dest : dest
|
||||
})
|
||||
|
||||
return {
|
||||
name: 'dev-sw-plugin',
|
||||
apply: 'serve',
|
||||
configResolved() {
|
||||
/* no-op */
|
||||
},
|
||||
resolveId(id) {
|
||||
const name = id.startsWith('/') ? id.slice(1) : id
|
||||
if (name === swDest) {
|
||||
return swFullSrc
|
||||
} else if (name === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
async load(id) {
|
||||
if (id === swFullSrc) {
|
||||
return readFile(swFullSrc, 'utf-8')
|
||||
} else if (id === swEnvNameResolved) {
|
||||
return getDevSwEnv()
|
||||
}
|
||||
return null
|
||||
},
|
||||
/**
|
||||
* vite does not bundle the service worker
|
||||
* during dev, and firefox does not support ESM as service worker
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
|
||||
*/
|
||||
async transform(code, id) {
|
||||
if (id === swFullSrc && transformSW) {
|
||||
const res = await esbuild.build({
|
||||
entryPoints: [swSrc],
|
||||
bundle: true,
|
||||
write: false,
|
||||
outfile: 'sw-pleroma.js',
|
||||
alias: esbuildAlias,
|
||||
plugins: [
|
||||
{
|
||||
name: 'vite-like-root-resolve',
|
||||
setup(b) {
|
||||
b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({
|
||||
path: resolve(projectRoot, args.path.slice(1)),
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sw-messages',
|
||||
setup(b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swMessagesName + '$') },
|
||||
(args) => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-messages',
|
||||
}),
|
||||
)
|
||||
b.onLoad(
|
||||
{ filter: /.*/, namespace: 'sw-messages' },
|
||||
async () => ({
|
||||
contents: await getSWMessagesAsText(),
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sw-env',
|
||||
setup(b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swEnvName + '$') },
|
||||
(args) => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-env',
|
||||
}),
|
||||
)
|
||||
b.onLoad({ filter: /.*/, namespace: 'sw-env' }, () => ({
|
||||
contents: getDevSwEnv(),
|
||||
}))
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
const text = res.outputFiles[0].text
|
||||
return text
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Idea taken from
|
||||
// https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/plugins/build.ts
|
||||
// rollup does not support compiling to iife if we want to code-split;
|
||||
// however, we must compile the service worker to iife because of browser support.
|
||||
// Run another vite build just for the service worker targeting iife at
|
||||
// the end of the build.
|
||||
export const buildSwPlugin = ({ swSrc, swDest }) => {
|
||||
let config
|
||||
return {
|
||||
name: 'build-sw-plugin',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
configResolved(resolvedConfig) {
|
||||
config = {
|
||||
define: resolvedConfig.define,
|
||||
resolve: resolvedConfig.resolve,
|
||||
plugins: [swMessagesPlugin()],
|
||||
publicDir: false,
|
||||
build: {
|
||||
...resolvedConfig.build,
|
||||
lib: {
|
||||
entry: swSrc,
|
||||
formats: ['iife'],
|
||||
name: 'sw_pleroma',
|
||||
},
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: swDest,
|
||||
},
|
||||
},
|
||||
},
|
||||
configFile: false,
|
||||
}
|
||||
},
|
||||
generateBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler(_, bundle) {
|
||||
const assets = Object.keys(bundle)
|
||||
.filter((name) => !/\.map$/.test(name))
|
||||
.map((name) => '/' + name)
|
||||
config.plugins.push({
|
||||
name: 'build-sw-env-plugin',
|
||||
resolveId(id) {
|
||||
if (id === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
load(id) {
|
||||
if (id === swEnvNameResolved) {
|
||||
return getProdSwEnv({ assets })
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler() {
|
||||
console.info('Building service worker for production')
|
||||
await build(config)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const swMessagesName = 'virtual:pleroma-fe/service_worker_messages'
|
||||
const swMessagesNameResolved = '\0' + swMessagesName
|
||||
|
||||
export const swMessagesPlugin = () => {
|
||||
return {
|
||||
name: 'sw-messages-plugin',
|
||||
resolveId(id) {
|
||||
if (id === swMessagesName) {
|
||||
Object.values(i18nFiles).forEach((f) => {
|
||||
this.addWatchFile(f)
|
||||
})
|
||||
return swMessagesNameResolved
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
if (id === swMessagesNameResolved) {
|
||||
return await getSWMessagesAsText()
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,27 @@
|
|||
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with {
|
||||
type: 'json',
|
||||
|
||||
module.exports = {
|
||||
updateEmoji () {
|
||||
const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group')
|
||||
const fs = require('fs')
|
||||
|
||||
Object.keys(emojis)
|
||||
.map(k => {
|
||||
emojis[k].map(e => {
|
||||
delete e.unicode_version
|
||||
delete e.emoji_version
|
||||
delete e.skin_tone_support_unicode_version
|
||||
})
|
||||
})
|
||||
|
||||
const res = {}
|
||||
Object.keys(emojis)
|
||||
.map(k => {
|
||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||
res[groupId] = emojis[k]
|
||||
})
|
||||
|
||||
console.info('Updating emojis...')
|
||||
fs.writeFileSync('static/emoji.json', JSON.stringify(res))
|
||||
console.info('Done.')
|
||||
}
|
||||
}
|
||||
import fs from 'fs'
|
||||
|
||||
Object.keys(emojis).map((k) => {
|
||||
emojis[k].map((e) => {
|
||||
delete e.unicode_version
|
||||
delete e.emoji_version
|
||||
delete e.skin_tone_support_unicode_version
|
||||
})
|
||||
})
|
||||
|
||||
const res = {}
|
||||
Object.keys(emojis).map((k) => {
|
||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||
res[groupId] = emojis[k]
|
||||
})
|
||||
|
||||
console.info('Updating emojis...')
|
||||
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res))
|
||||
console.info('Done.')
|
||||
|
|
|
|||
63
build/utils.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var sass = require('sass')
|
||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
}
|
||||
|
||||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
function generateLoaders (loaders) {
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return [MiniCssExtractPlugin.loader].concat(loaders)
|
||||
} else {
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
|
||||
return [
|
||||
{
|
||||
test: /\.(post)?css$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader']),
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']),
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
use: generateLoaders([
|
||||
'css-loader',
|
||||
'postcss-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
indentedSyntax: true
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'sass-loader'])
|
||||
},
|
||||
{
|
||||
test: /\.styl(us)?$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Generate loaders for standalone style files (outside of .vue)
|
||||
exports.styleLoaders = function (options) {
|
||||
return exports.cssLoaders(options)
|
||||
}
|
||||
129
build/webpack.base.conf.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin')
|
||||
var CopyPlugin = require('copy-webpack-plugin');
|
||||
var { VueLoaderPlugin } = require('vue-loader')
|
||||
var ESLintPlugin = require('eslint-webpack-plugin');
|
||||
var StylelintPlugin = require('stylelint-webpack-plugin');
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
// various preprocessor loaders added to vue-loader at the end of this file
|
||||
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
|
||||
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
|
||||
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
|
||||
|
||||
var now = Date.now()
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].js'
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.jsx', '.vue'],
|
||||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
alias: {
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components'),
|
||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||
},
|
||||
fallback: {
|
||||
'querystring': require.resolve('querystring-es3'),
|
||||
'url': require.resolve('url/')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
||||
rules: [
|
||||
{
|
||||
enforce: 'post',
|
||||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||
type: 'javascript/auto',
|
||||
loader: '@intlify/vue-i18n-loader',
|
||||
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||
path.resolve(__dirname, '../src/i18n')
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
isCustomElement(tag) {
|
||||
if (tag === 'pinch-zoom') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
include: projectRoot,
|
||||
exclude: /node_modules\/(?!tributejs)/,
|
||||
use: 'babel-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
type: 'asset',
|
||||
generator: {
|
||||
filename: utils.assetsPath('img/[name].[hash:7][ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
type: 'asset',
|
||||
generator: {
|
||||
filename: utils.assetsPath('fonts/[name].[hash:7][ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new ServiceWorkerWebpackPlugin({
|
||||
entry: path.join(__dirname, '..', 'src/sw.js'),
|
||||
filename: 'sw-pleroma.js'
|
||||
}),
|
||||
new ESLintPlugin({
|
||||
extensions: ['js', 'vue'],
|
||||
formatter: require('eslint-formatter-friendly')
|
||||
}),
|
||||
new StylelintPlugin({}),
|
||||
new VueLoaderPlugin(),
|
||||
// This copies Ruffle's WASM to a directory so that JS side can access it
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "node_modules/@ruffle-rs/ruffle/**/*",
|
||||
to: "static/ruffle/[name][ext]"
|
||||
},
|
||||
],
|
||||
options: {
|
||||
concurrency: 100,
|
||||
},
|
||||
})
|
||||
]
|
||||
}
|
||||
37
build/webpack.dev.conf.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
var config = require('../config')
|
||||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
var utils = require('./utils')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
|
||||
// add hot-reload related code to entry chunks
|
||||
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
|
||||
})
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
},
|
||||
mode: 'development',
|
||||
// eval-source-map is faster for development
|
||||
devtool: 'eval-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
'COMMIT_HASH': JSON.stringify('DEV'),
|
||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
inject: true
|
||||
})
|
||||
]
|
||||
})
|
||||
104
build/webpack.prod.conf.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
: config.build.env
|
||||
|
||||
let commitHash = (() => {
|
||||
const subst = "$Format:%h$";
|
||||
if(!subst.match(/Format:/)) {
|
||||
return subst;
|
||||
} else {
|
||||
return require('child_process')
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString();
|
||||
}
|
||||
})();
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
mode: 'production',
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? 'source-map' : false,
|
||||
optimization: {
|
||||
minimize: true,
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
},
|
||||
minimizer: [
|
||||
`...`,
|
||||
new CssMinimizerPlugin()
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
||||
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env,
|
||||
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||
'DEV_OVERRIDES': JSON.stringify(undefined),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
}),
|
||||
// extract css into its own file
|
||||
new MiniCssExtractPlugin({
|
||||
filename: utils.assetsPath('css/[name].[contenthash].css')
|
||||
}),
|
||||
// generate dist index.html with correct asset hash for caching.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: process.env.NODE_ENV === 'testing'
|
||||
? 'index.html'
|
||||
: config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeAttributeQuotes: true,
|
||||
ignoreCustomComments: [/server-generated-meta/]
|
||||
// more options:
|
||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||
}
|
||||
}),
|
||||
// split vendor js into its own file
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
// new webpack.optimize.SplitChunksPlugin({
|
||||
// name: ['app', 'vendor']
|
||||
// }),
|
||||
]
|
||||
})
|
||||
|
||||
if (config.build.productionGzip) {
|
||||
var CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
new CompressionWebpackPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix HTML attribute parsing for escaped quotes
|
||||
9
changelog.d/browsers-support.change
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Updated our build system to support browsers:
|
||||
Safari >= 15
|
||||
Firefox >= 115
|
||||
Android > 4
|
||||
no Opera Mini support
|
||||
no IE support
|
||||
no "dead" (unmaintained) browsers support
|
||||
|
||||
This does not guarantee that browsers will or will not work.
|
||||
1
changelog.d/date-absolute.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Support displaying time in absolute format
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix emojis breaking user bio/description editing
|
||||
1
changelog.d/non-anonymous-polls.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Inform users that Smithereen public polls are public
|
||||
1
changelog.d/oauth-app-name.change
Normal file
|
|
@ -0,0 +1 @@
|
|||
Simplify the OAuth client_name to 'PleromaFE'
|
||||
6
config/dev.env.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
var merge = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
||||
73
config/index.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// see http://vuejs-templates.github.io/webpack for documentation.
|
||||
const path = require('path')
|
||||
let settings = {}
|
||||
try {
|
||||
settings = require('./local.json')
|
||||
if (settings.target && settings.target.endsWith('/')) {
|
||||
// replacing trailing slash since it can conflict with some apis
|
||||
// and that's how actual BE reports its url
|
||||
settings.target = settings.target.replace(/\/$/, '')
|
||||
}
|
||||
console.log('Using local dev server settings (/config/local.json):')
|
||||
console.log(JSON.stringify(settings, null, 2))
|
||||
} catch (e) {
|
||||
console.log('Local dev server settings not found (/config/local.json)')
|
||||
}
|
||||
|
||||
const target = settings.target || 'http://localhost:4000/'
|
||||
|
||||
module.exports = {
|
||||
build: {
|
||||
env: require('./prod.env'),
|
||||
index: path.resolve(__dirname, '../dist/index.html'),
|
||||
assetsRoot: path.resolve(__dirname, '../dist'),
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
productionSourceMap: true,
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
// Surge or Netlify already gzip all static assets for you.
|
||||
// Before setting to `true`, make sure to:
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css']
|
||||
},
|
||||
dev: {
|
||||
env: require('./dev.env'),
|
||||
port: 8080,
|
||||
settings,
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
'/api': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/nodeinfo': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/socket': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
ws: true,
|
||||
headers: {
|
||||
'Origin': target
|
||||
}
|
||||
},
|
||||
'/oauth/revoke': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
}
|
||||
},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
// (https://github.com/webpack/css-loader#sourcemaps)
|
||||
// In our experience, they generally work as expected,
|
||||
// just be aware of this issue when enabling this option.
|
||||
cssSourceMap: false
|
||||
}
|
||||
}
|
||||
3
config/prod.env.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
6
config/test.env.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
var merge = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_USER: pleroma
|
||||
POSTGRES_PASSWORD: pleroma
|
||||
POSTGRES_DB: pleroma
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U pleroma -d pleroma"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 30
|
||||
|
||||
pleroma:
|
||||
image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable}
|
||||
environment:
|
||||
DB_USER: pleroma
|
||||
DB_PASS: pleroma
|
||||
DB_NAME: pleroma
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
DOMAIN: localhost
|
||||
INSTANCE_NAME: Pleroma E2E
|
||||
ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
|
||||
NOTIFY_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
|
||||
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
|
||||
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
|
||||
E2E_ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./docker/pleroma/entrypoint.e2e.sh:/opt/pleroma/entrypoint.e2e.sh:ro
|
||||
entrypoint: ["/bin/ash", "/opt/pleroma/entrypoint.e2e.sh"]
|
||||
healthcheck:
|
||||
# NOTE: "localhost" may resolve to ::1 in some images (IPv6) while Pleroma only
|
||||
# listens on IPv4 in this container. Use 127.0.0.1 to avoid false negatives.
|
||||
test: ["CMD-SHELL", "test -f /var/lib/pleroma/.e2e_seeded && wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 60
|
||||
|
||||
e2e:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/e2e/Dockerfile.e2e
|
||||
depends_on:
|
||||
pleroma:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
CI: "1"
|
||||
VITE_PROXY_TARGET: http://pleroma:4000
|
||||
VITE_PROXY_ORIGIN: http://localhost:4000
|
||||
E2E_BASE_URL: http://localhost:8080
|
||||
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
|
||||
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
|
||||
command: ["yarn", "e2e:pw"]
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
FROM mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
RUN npm install -g yarn@1.22.22
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV CI=1
|
||||
|
||||
CMD ["yarn", "e2e:pw"]
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
#!/bin/ash
|
||||
|
||||
set -eu
|
||||
|
||||
SEED_SENTINEL_PATH="/var/lib/pleroma/.e2e_seeded"
|
||||
CONFIG_OVERRIDE_PATH="/var/lib/pleroma/config.exs"
|
||||
|
||||
echo "-- Waiting for database..."
|
||||
while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
echo "-- Writing E2E config overrides..."
|
||||
cat > "$CONFIG_OVERRIDE_PATH" <<'EOF'
|
||||
import Config
|
||||
|
||||
config :pleroma, Pleroma.Captcha,
|
||||
enabled: false
|
||||
|
||||
config :pleroma, :instance,
|
||||
registrations_open: true,
|
||||
account_activation_required: false,
|
||||
approval_required: false
|
||||
EOF
|
||||
|
||||
echo "-- Running migrations..."
|
||||
/opt/pleroma/bin/pleroma_ctl migrate
|
||||
|
||||
echo "-- Starting!"
|
||||
/opt/pleroma/bin/pleroma start &
|
||||
PLEROMA_PID="$!"
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${PLEROMA_PID:-}" ] && kill -0 "$PLEROMA_PID" 2>/dev/null; then
|
||||
kill -TERM "$PLEROMA_PID"
|
||||
wait "$PLEROMA_PID" || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup INT TERM
|
||||
|
||||
echo "-- Waiting for API..."
|
||||
api_ok="false"
|
||||
for _i in $(seq 1 120); do
|
||||
if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then
|
||||
api_ok="true"
|
||||
break
|
||||
fi
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
if [ "$api_ok" != "true" ]; then
|
||||
echo "Timed out waiting for Pleroma API to become available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$SEED_SENTINEL_PATH" ]; then
|
||||
if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then
|
||||
echo "-- Seeding admin user (${E2E_ADMIN_USERNAME})..."
|
||||
if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then
|
||||
echo "-- User already exists (or creation failed), ensuring admin + confirmed..."
|
||||
/opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed
|
||||
fi
|
||||
else
|
||||
echo "-- Skipping admin seeding (missing E2E_ADMIN_* env)"
|
||||
fi
|
||||
|
||||
touch "$SEED_SENTINEL_PATH"
|
||||
fi
|
||||
|
||||
wait "$PLEROMA_PID"
|
||||
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
||||
|
||||
1. `/api/pleroma/frontend_configurations` - this is generated by backend and includes FE/Client-specific data. PleromaFE uses the `pleroma_fe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
|
||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/src/public/static/config.json).
|
||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/src/src/stores/instance.js) )
|
||||
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
|
||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json).
|
||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) )
|
||||
|
||||
## Instance-defaults
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ server {
|
|||
|
||||
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](../backend/API/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
||||
|
||||
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/src/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||
|
||||
For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import js from '@eslint/js'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import vue from 'eslint-plugin-vue'
|
||||
import globals from 'globals'
|
||||
|
||||
export default defineConfig([
|
||||
...vue.configs['flat/recommended'],
|
||||
globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']),
|
||||
{
|
||||
files: ['src/**/*.vue'],
|
||||
plugins: { js },
|
||||
extends: ['js/recommended'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.vitest,
|
||||
...globals.chai,
|
||||
...globals.commonjs,
|
||||
...globals.serviceworker,
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'vue/require-prop-types': 0,
|
||||
'vue/multi-word-component-names': 0,
|
||||
},
|
||||
},
|
||||
])
|
||||
45
index.html
|
|
@ -3,49 +3,16 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<link rel="preload" href="/static/config.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/api/pleroma/frontend_configurations" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/nodeinfo/2.0.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/nodeinfo/2.1.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/api/v1/instance" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/static/pleromatan_apology_fox_small.webp" as="image" />
|
||||
<!-- putting styles here to avoid having to wait for styles to load up -->
|
||||
<link rel="stylesheet" id="splashscreen" href="/static/splash.css" />
|
||||
<link rel="stylesheet" id="custom-styles-holder" type="text/css" href="/static/empty.css" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<style id="pleroma-eager-styles" type="text/css"></style>
|
||||
<style id="pleroma-lazy-styles" type="text/css"></style>
|
||||
<!--server-generated-meta-->
|
||||
</head>
|
||||
<body>
|
||||
<body class="hidden">
|
||||
<noscript>To use Pleroma, please enable JavaScript.</noscript>
|
||||
<div id="splash" class="initial-hidden">
|
||||
<!-- we are hiding entire graphic so no point showing credit -->
|
||||
<div aria-hidden="true" id="splash-credit">
|
||||
Art by pipivovott
|
||||
</div>
|
||||
<div id="splash-container">
|
||||
<div aria-hidden="true" id="mascot-container">
|
||||
<div id="throbber">
|
||||
<div class="chunk" id="chunk-P">
|
||||
</div>
|
||||
<div class="chunk" id="chunk-L">
|
||||
</div>
|
||||
<div class="chunk" id="chunk-E">
|
||||
</div>
|
||||
</div>
|
||||
<img id="mascot" src="/static/pleromatan_apology_small.webp">
|
||||
</div>
|
||||
<div id="status" class="css-ok">
|
||||
<!-- (。>﹏<) -->
|
||||
<!-- it's a pseudographic, don't want screenreader read out nonsense -->
|
||||
<span aria-hidden="true" class="initial-text">(。>﹏<)</span>
|
||||
</div>
|
||||
<code id="statusError"></code>
|
||||
<pre id="statusStack"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app" class="hidden"></div>
|
||||
<div id="app"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<div id="popovers"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<div id="popovers" />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
185
package.json
|
|
@ -1,127 +1,136 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "2.10.1",
|
||||
"version": "2.7.1",
|
||||
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/src/CONTRIBUTORS.md>",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "node build/update-emoji.js && vite dev",
|
||||
"build": "node build/update-emoji.js && vite build",
|
||||
"unit": "node build/update-emoji.js && vitest --run",
|
||||
"unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
|
||||
"unit:watch": "node build/update-emoji.js && vitest",
|
||||
"e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs",
|
||||
"e2e": "sh ./tools/e2e/run.sh",
|
||||
"test": "yarn run unit && yarn run e2e",
|
||||
"ci-biome": "yarn exec biome check",
|
||||
"ci-eslint": "yarn exec eslint",
|
||||
"ci-stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
|
||||
"lint": "yarn ci-biome; yarn ci-eslint; yarn ci-stylelint",
|
||||
"lint-fix": "yarn exec eslint -- --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write"
|
||||
"dev": "node build/dev-server.js",
|
||||
"build": "node build/build.js",
|
||||
"unit": "karma start test/unit/karma.conf.js --single-run",
|
||||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"stylelint": "npx stylelint '**/*.scss' '**/*.vue'",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.4",
|
||||
"@babel/runtime": "7.21.5",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.1.0",
|
||||
"@fortawesome/vue-fontawesome": "3.1.2",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2024.8.21",
|
||||
"@vuelidate/core": "2.0.3",
|
||||
"@vuelidate/validators": "2.0.4",
|
||||
"@web3-storage/parse-link-header": "^3.1.0",
|
||||
"body-scroll-lock": "3.1.5",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "2.0.1",
|
||||
"cropperjs": "1.5.13",
|
||||
"escape-html": "1.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.8.1",
|
||||
"pinia": "^3.0.0",
|
||||
"punycode.js": "2.3.1",
|
||||
"qrcode": "1.5.4",
|
||||
"phoenix": "1.7.7",
|
||||
"punycode.js": "2.3.0",
|
||||
"qrcode": "1.5.3",
|
||||
"querystring-es3": "0.2.1",
|
||||
"url": "0.11.4",
|
||||
"url": "0.11.0",
|
||||
"utf8": "3.0.0",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.5.22",
|
||||
"vue-i18n": "11",
|
||||
"vue-router": "4.6.4",
|
||||
"vue": "3.2.45",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.6",
|
||||
"vue-template-compiler": "2.7.14",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.7",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.5",
|
||||
"@babel/eslint-parser": "7.28.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@babel/register": "7.28.3",
|
||||
"@biomejs/biome": "2.3.11",
|
||||
"@pinia/testing": "1.0.3",
|
||||
"@babel/core": "7.21.8",
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@babel/plugin-transform-runtime": "7.21.4",
|
||||
"@babel/preset-env": "7.21.5",
|
||||
"@babel/register": "7.21.0",
|
||||
"@intlify/vue-i18n-loader": "5.0.1",
|
||||
"@ungap/event-target": "0.2.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vitest/browser": "^3.0.7",
|
||||
"@vitest/ui": "^3.0.7",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||
"@vue/babel-plugin-jsx": "1.5.0",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.21",
|
||||
"@vue/babel-plugin-jsx": "1.2.2",
|
||||
"@vue/compiler-sfc": "3.2.45",
|
||||
"@vue/test-utils": "2.2.8",
|
||||
"autoprefixer": "10.4.19",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "5.3.3",
|
||||
"chalk": "5.6.2",
|
||||
"chromedriver": "135.0.4",
|
||||
"chai": "4.3.7",
|
||||
"chalk": "1.1.3",
|
||||
"chromedriver": "108.0.0",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"css-loader": "6.10.0",
|
||||
"css-minimizer-webpack-plugin": "4.2.2",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-standard": "17.0.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-n": "17.23.1",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-n": "15.6.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"eslint-webpack-plugin": "3.2.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "5.1.0",
|
||||
"function-bind": "1.1.2",
|
||||
"http-proxy-middleware": "3.0.5",
|
||||
"iso-639-1": "3.1.5",
|
||||
"express": "4.18.2",
|
||||
"function-bind": "1.1.1",
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"iso-639-1": "2.1.15",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "6.4.4",
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-firefox-launcher": "2.1.3",
|
||||
"karma-mocha": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-sinon-chai": "2.0.2",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"msw": "2.10.5",
|
||||
"nightwatch": "3.12.2",
|
||||
"playwright": "1.57.0",
|
||||
"postcss": "8.5.6",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"mocha": "10.2.0",
|
||||
"nightwatch": "2.6.25",
|
||||
"opn": "5.5.0",
|
||||
"ora": "0.4.1",
|
||||
"postcss": "8.4.23",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "7.0.2",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"sass": "1.93.2",
|
||||
"selenium-server": "3.141.59",
|
||||
"semver": "7.7.3",
|
||||
"serve-static": "2.2.0",
|
||||
"shelljs": "0.10.0",
|
||||
"sinon": "20.0.0",
|
||||
"sinon-chai": "4.0.1",
|
||||
"stylelint": "16.25.0",
|
||||
"sass": "1.60.0",
|
||||
"sass-loader": "13.2.2",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "7.3.8",
|
||||
"serviceworker-webpack5-plugin": "2.0.0",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "15.0.4",
|
||||
"sinon-chai": "3.7.0",
|
||||
"stylelint": "14.16.1",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.6.0",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-eslint2": "^5.0.3",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
"vitest": "^3.0.7",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
"stylelint-config-recommended-scss": "^8.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "29.0.0",
|
||||
"stylelint-rscss": "0.4.0",
|
||||
"stylelint-webpack-plugin": "^3.3.0",
|
||||
"vue-loader": "17.0.1",
|
||||
"vue-style-loader": "4.1.3",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-dev-middleware": "3.7.3",
|
||||
"webpack-hot-middleware": "2.25.3",
|
||||
"webpack-merge": "0.20.0"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
"node": ">= 16.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import autoprefixer from 'autoprefixer'
|
||||
|
||||
export default {
|
||||
plugins: [autoprefixer],
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
}
|
||||
|
|
|
|||
1
public/static/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
*.custom.*
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
{
|
||||
"pleroma-dark": [
|
||||
"Pleroma Dark",
|
||||
"#121a24",
|
||||
"#182230",
|
||||
"#b9b9ba",
|
||||
"#d8a070",
|
||||
"#d31014",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#ffa500"
|
||||
],
|
||||
"pleroma-light": [
|
||||
"Pleroma Light",
|
||||
"#f2f4f6",
|
||||
"#dbe0e8",
|
||||
"#304055",
|
||||
"#f86f0f",
|
||||
"#d31014",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#ffa500"
|
||||
],
|
||||
"classic-dark": {
|
||||
"name": "Classic Dark",
|
||||
"bg": "#161c20",
|
||||
"fg": "#282e32",
|
||||
"text": "#b9b9b9",
|
||||
"link": "#baaa9c",
|
||||
"cRed": "#d31014",
|
||||
"cGreen": "#0fa00f",
|
||||
"cBlue": "#0095ff",
|
||||
"cOrange": "#ffa500"
|
||||
},
|
||||
"bird": [
|
||||
"Bird",
|
||||
"#f8fafd",
|
||||
"#e6ecf0",
|
||||
"#14171a",
|
||||
"#0084b8",
|
||||
"#e0245e",
|
||||
"#17bf63",
|
||||
"#1b95e0",
|
||||
"#fab81e"
|
||||
],
|
||||
"pleroma-amoled": [
|
||||
"Pleroma Dark AMOLED",
|
||||
"#000000",
|
||||
"#111111",
|
||||
"#b0b0b1",
|
||||
"#d8a070",
|
||||
"#aa0000",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#d59500"
|
||||
],
|
||||
"tomorrow-night": {
|
||||
"name": "Tomorrow Night",
|
||||
"bg": "#1d1f21",
|
||||
"fg": "#373b41",
|
||||
"link": "#81a2be",
|
||||
"text": "#c5c8c6",
|
||||
"cRed": "#cc6666",
|
||||
"cBlue": "#8abeb7",
|
||||
"cGreen": "#b5bd68",
|
||||
"cOrange": "#de935f"
|
||||
},
|
||||
"dracula": {
|
||||
"name": "Dracula",
|
||||
"bg": "#282A36",
|
||||
"fg": "#44475A",
|
||||
"link": "#BC92F9",
|
||||
"text": "#f8f8f2",
|
||||
"cRed": "#FF5555",
|
||||
"cBlue": "#8BE9FD",
|
||||
"cGreen": "#50FA7B",
|
||||
"cOrange": "#FFB86C"
|
||||
},
|
||||
"ir-black": [
|
||||
"Ir Black",
|
||||
"#000000",
|
||||
"#242422",
|
||||
"#b5b3aa",
|
||||
"#ff6c60",
|
||||
"#FF6C60",
|
||||
"#A8FF60",
|
||||
"#96CBFE",
|
||||
"#FFFFB6"
|
||||
],
|
||||
"monokai": [
|
||||
"Monokai",
|
||||
"#272822",
|
||||
"#383830",
|
||||
"#f8f8f2",
|
||||
"#f92672",
|
||||
"#F92672",
|
||||
"#a6e22e",
|
||||
"#66d9ef",
|
||||
"#f4bf75"
|
||||
],
|
||||
"purple-stream": {
|
||||
"name": "Purple stream",
|
||||
"bg": "#17171A",
|
||||
"fg": "#450F92",
|
||||
"link": "#8769B4",
|
||||
"text": "#C0C0C5",
|
||||
"cRed": "#EB0300",
|
||||
"cBlue": "#4656FF",
|
||||
"cGreen": "#B0E020",
|
||||
"cOrange": "#FF9046"
|
||||
},
|
||||
"feud": {
|
||||
"name": "Feud",
|
||||
"bg": "#323337",
|
||||
"fg": "#1D1E21",
|
||||
"link": "#18A0E3",
|
||||
"accent": "#6671E2",
|
||||
"text": "#DBDDE0",
|
||||
"cRed": "#E05053",
|
||||
"cBlue": "#6671E2",
|
||||
"cGreen": "#3A8D5D",
|
||||
"cOrange": "#DCAA45"
|
||||
},
|
||||
"constabulary": {
|
||||
"name": "Constabulary",
|
||||
"bg": "#FFFFFF",
|
||||
"fg": "#3B5897",
|
||||
"link": "#28487C",
|
||||
"text": "#333333",
|
||||
"cRed": "#FA3C4C",
|
||||
"cBlue": "#0083FF",
|
||||
"cGreen": "#44BDC6",
|
||||
"cOrange": "#FFC200"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
|
@ -1,133 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#splash {
|
||||
--scale: 1;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto;
|
||||
align-content: center;
|
||||
place-items: center;
|
||||
flex-direction: column;
|
||||
background: #0f161e;
|
||||
font-family: sans-serif;
|
||||
color: #b9b9ba;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
opacity: 1;
|
||||
transition: opacity 500ms ease-out 2s;
|
||||
}
|
||||
|
||||
#splash.hidden,
|
||||
#splash.initial-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#splash-credit {
|
||||
position: absolute;
|
||||
font-size: 1em;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#mascot-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
perspective: 60em;
|
||||
perspective-origin: 0 -15em;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
#mascot {
|
||||
width: calc(10em * var(--scale));
|
||||
height: calc(10em * var(--scale));
|
||||
object-fit: contain;
|
||||
object-position: bottom;
|
||||
transform: translateZ(-2em);
|
||||
}
|
||||
|
||||
#throbber {
|
||||
display: grid;
|
||||
width: calc(5em * 0.5 * var(--scale));
|
||||
height: calc(8em * 0.5 * var(--scale));
|
||||
margin-left: 4.1em;
|
||||
z-index: 2;
|
||||
grid-template-rows: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-areas:
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . . ."
|
||||
"P P . . ."
|
||||
"P P . E E"
|
||||
"P P . E E";
|
||||
|
||||
--logoChunkSize: calc(2em * 0.5 * var(--scale));
|
||||
}
|
||||
|
||||
.chunk {
|
||||
background-color: #e2b188;
|
||||
box-shadow: 0.01em 0.01em 0.1em 0 #e2b188;
|
||||
}
|
||||
|
||||
#chunk-P {
|
||||
grid-area: P;
|
||||
border-top-left-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-L {
|
||||
grid-area: L;
|
||||
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-E {
|
||||
grid-area: E;
|
||||
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
line-height: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#statusError {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
line-height: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#statusStack {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: calc((1vw + 1vh + 1vmin) / 2.5);
|
||||
width: calc(100vw - 5em);
|
||||
padding: 1em;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
#throbber {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"pleroma-dark": "/static/themes/pleroma-dark.json",
|
||||
"pleroma-light": "/static/themes/pleroma-light.json",
|
||||
"redmond-xx": "/static/themes/redmond-xx.json",
|
||||
"redmond-xx-se": "/static/themes/redmond-xx-se.json",
|
||||
"redmond-xxi": "/static/themes/redmond-xxi.json",
|
||||
"breezy-dark": "/static/themes/breezy-dark.json",
|
||||
"breezy-light": "/static/themes/breezy-light.json",
|
||||
"mammal": "/static/themes/mammal.json",
|
||||
"paper": "/static/themes/paper.json"
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
@meta {
|
||||
name: Breezy DX;
|
||||
author: HJ;
|
||||
license: WTFPL;
|
||||
website: ebin.club;
|
||||
}
|
||||
|
||||
@palette.Dark {
|
||||
bg: #292C32;
|
||||
fg: #292C32;
|
||||
text: #ffffff;
|
||||
link: #1CA4F3;
|
||||
accent: #1CA4F3;
|
||||
cRed: #f41a51;
|
||||
cBlue: #1CA4F3;
|
||||
cGreen: #1af46e;
|
||||
cOrange: #f4af1a;
|
||||
}
|
||||
|
||||
@palette.Light {
|
||||
bg: #EFF0F2;
|
||||
fg: #EFF0F2;
|
||||
text: #1B1F22;
|
||||
underlay: #5d6086;
|
||||
accent: #1CA4F3;
|
||||
cBlue: #1CA4F3;
|
||||
cRed: #f41a51;
|
||||
cGreen: #0b6a30;
|
||||
cOrange: #f4af1a;
|
||||
border: #d8e6f9;
|
||||
link: #1CA4F3;
|
||||
}
|
||||
|
||||
@palette.Panda {
|
||||
bg: #EFF0F2;
|
||||
fg: #292C32;
|
||||
text: #1B1F22;
|
||||
link: #1CA4F3;
|
||||
accent: #1CA4F3;
|
||||
cRed: #f41a51;
|
||||
cBlue: #1CA4F3;
|
||||
cGreen: #0b6a30;
|
||||
cOrange: #f4af1a;
|
||||
}
|
||||
|
||||
Root {
|
||||
--badgeNotification: color | --cRed;
|
||||
--buttonDefaultHoverGlow: shadow | inset 0 0 0 1 --accent / 1;
|
||||
--buttonDefaultFocusGlow: shadow | inset 0 0 0 1 --accent / 1;
|
||||
--buttonDefaultShadow: shadow | inset 0 0 0 1 --text / 0.35, 0 5 5 -5 #000000 / 0.35;
|
||||
--buttonDefaultBevel: shadow | inset 0 14 14 -14 #FFFFFF / 0.1;
|
||||
--buttonPressedBevel: shadow | inset 0 -20 20 -20 #000000 / 0.05;
|
||||
--defaultInputBevel: shadow | inset 0 0 0 1 --text / 0.35;
|
||||
--defaultInputHoverGlow: shadow | 0 0 0 1 --accent / 1;
|
||||
--defaultInputFocusGlow: shadow | 0 0 0 1 --link / 1;
|
||||
}
|
||||
|
||||
Button {
|
||||
background: --parent;
|
||||
}
|
||||
|
||||
Button:disabled {
|
||||
shadow: --buttonDefaultBevel, --buttonDefaultShadow
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
background: --inheritedBackground;
|
||||
shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow
|
||||
}
|
||||
|
||||
Button:toggled {
|
||||
background: $blend(--inheritedBackground 0.3 --accent)
|
||||
}
|
||||
|
||||
Button:pressed {
|
||||
background: $blend(--inheritedBackground 0.8 --accent)
|
||||
}
|
||||
|
||||
Button:pressed:toggled {
|
||||
background: $blend(--inheritedBackground 0.2 --accent)
|
||||
}
|
||||
|
||||
Button:toggled:hover {
|
||||
background: $blend(--inheritedBackground 0.3 --accent)
|
||||
}
|
||||
|
||||
Input {
|
||||
shadow: --defaultInputBevel;
|
||||
background: $mod(--bg -10);
|
||||
}
|
||||
|
||||
PanelHeader {
|
||||
shadow: inset 0 30 30 -30 #ffffff / 0.25
|
||||
}
|
||||
|
||||
Tab:hover {
|
||||
shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow
|
||||
}
|
||||
|
||||
Tab {
|
||||
background: --bg;
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
@meta {
|
||||
name: Redmond DX;
|
||||
author: HJ;
|
||||
license: WTFPL;
|
||||
website: ebin.club;
|
||||
}
|
||||
|
||||
@palette.Modern {
|
||||
bg: #D3CFC7;
|
||||
fg: #092369;
|
||||
text: #000000;
|
||||
link: #0000FF;
|
||||
accent: #A5C9F0;
|
||||
cRed: #FF3000;
|
||||
cBlue: #009EFF;
|
||||
cGreen: #309E00;
|
||||
cOrange: #FFCE00;
|
||||
}
|
||||
|
||||
@palette.Classic {
|
||||
bg: #BFBFBF;
|
||||
fg: #000180;
|
||||
text: #000000;
|
||||
link: #0000FF;
|
||||
accent: #A5C9F0;
|
||||
cRed: #FF0000;
|
||||
cBlue: #2E2ECE;
|
||||
cGreen: #007E00;
|
||||
cOrange: #CE8F5F;
|
||||
}
|
||||
|
||||
@palette.Vapor {
|
||||
bg: #F0ADCD;
|
||||
fg: #bca4ee;
|
||||
text: #602040;
|
||||
link: #064745;
|
||||
accent: #9DF7C8;
|
||||
cRed: #86004a;
|
||||
cBlue: #0e5663;
|
||||
cGreen: #0a8b51;
|
||||
cOrange: #787424;
|
||||
}
|
||||
|
||||
Root {
|
||||
--gradientColor: color | --accent;
|
||||
--inputColor: color | #FFFFFF;
|
||||
--bevelLight: color | $brightness(--bg 50);
|
||||
--bevelDark: color | $brightness(--bg -20);
|
||||
--bevelExtraDark: color | #404040;
|
||||
--buttonDefaultBevel: shadow | $borderSide(--bevelExtraDark bottom-right 1 1), $borderSide(--bevelLight top-left 1 1), $borderSide(--bevelDark bottom-right 1 2);
|
||||
--buttonPressedFocusedBevel: shadow | inset 0 0 0 1 #000000 / 1 #Outer , inset 0 0 0 2 --bevelExtraDark / 1 #inner;
|
||||
--buttonPressedBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2);
|
||||
--defaultInputBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2), $borderSide(--bg bottom-right 1 2);
|
||||
}
|
||||
|
||||
Button:toggled {
|
||||
background: --bg;
|
||||
shadow: --buttonPressedBevel
|
||||
}
|
||||
|
||||
Button:focused {
|
||||
shadow: --buttonDefaultBevel, 0 0 0 1 #000000 / 1
|
||||
}
|
||||
|
||||
Button:pressed {
|
||||
shadow: --buttonPressedBevel
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
shadow: --buttonDefaultBevel;
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Button {
|
||||
shadow: --buttonDefaultBevel;
|
||||
background: --bg;
|
||||
roundness: 0
|
||||
}
|
||||
|
||||
Button:pressed:hover {
|
||||
shadow: --buttonPressedBevel
|
||||
}
|
||||
|
||||
Button:hover:pressed:focused {
|
||||
shadow: --buttonPressedFocusedBevel
|
||||
}
|
||||
|
||||
Button:pressed:focused {
|
||||
shadow: --buttonPressedFocusedBevel
|
||||
}
|
||||
|
||||
Button:toggled:pressed {
|
||||
shadow: --buttonPressedFocusedBevel
|
||||
}
|
||||
|
||||
Input {
|
||||
background: $boost(--bg 20);
|
||||
shadow: --defaultInputBevel;
|
||||
roundness: 0
|
||||
}
|
||||
|
||||
Input:focused {
|
||||
shadow: inset 0 0 0 1 #000000 / 1, --defaultInputBevel
|
||||
}
|
||||
|
||||
Input:focused:hover {
|
||||
shadow: --defaultInputBevel
|
||||
}
|
||||
|
||||
Input:focused:hover:disabled {
|
||||
shadow: --defaultInputBevel
|
||||
}
|
||||
|
||||
Input:hover {
|
||||
shadow: --defaultInputBevel
|
||||
}
|
||||
|
||||
Input:disabled {
|
||||
shadow: --defaultInputBevel
|
||||
}
|
||||
|
||||
Panel {
|
||||
shadow: --buttonDefaultBevel;
|
||||
roundness: 0
|
||||
}
|
||||
|
||||
PanelHeader {
|
||||
shadow: inset -1100 0 1000 -1000 --gradientColor / 1 #Gradient ;
|
||||
background: --fg
|
||||
}
|
||||
|
||||
PanelHeader ButtonUnstyled Icon {
|
||||
textColor: --text;
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
|
||||
PanelHeader Button Icon {
|
||||
textColor: --text;
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
|
||||
PanelHeader Button Text {
|
||||
textColor: --text;
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
|
||||
Tab:hover {
|
||||
background: --bg;
|
||||
shadow: --buttonDefaultBevel
|
||||
}
|
||||
|
||||
Tab:active {
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Tab:active:hover {
|
||||
background: --bg;
|
||||
shadow: --defaultButtonBevel
|
||||
}
|
||||
|
||||
Tab:active:hover:disabled {
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Tab:hover:disabled {
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Tab:disabled {
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Tab {
|
||||
background: --bg;
|
||||
shadow: --buttonDefaultBevel
|
||||
}
|
||||
|
||||
Tab:hover:active {
|
||||
shadow: --buttonDefaultBevel
|
||||
}
|
||||
|
||||
TopBar Link {
|
||||
textColor: #ffffff
|
||||
}
|
||||
|
||||
MenuItem:hover {
|
||||
background: --fg
|
||||
}
|
||||
|
||||
MenuItem:active {
|
||||
background: --fg
|
||||
}
|
||||
|
||||
MenuItem:active:hover {
|
||||
background: --fg
|
||||
}
|
||||
|
||||
Popover {
|
||||
shadow: --buttonDefaultBevel, 5 5 0 0 #000000 / 0.2;
|
||||
roundness: 0
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"RedmondDX": "/static/styles/Redmond DX.iss",
|
||||
"BreezyDX": "/static/styles/Breezy DX.iss"
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"]
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
286
src/App.js
|
|
@ -1,48 +1,29 @@
|
|||
import { throttle } from 'lodash'
|
||||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent, toValue } from 'vue'
|
||||
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import UserPanel from './components/user_panel/user_panel.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import { getOrCreateServiceWorker } from './services/sw/sw'
|
||||
import { windowHeight, windowWidth } from './services/window_utils/window_utils'
|
||||
|
||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
import { useI18nStore } from 'src/stores/i18n.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useShoutStore } from 'src/stores/shout.js'
|
||||
|
||||
import messages from 'src/i18n/messages'
|
||||
import localeService from 'src/services/locale/locale.service.js'
|
||||
|
||||
// Helper to unwrap reactive proxies
|
||||
window.toValue = toValue
|
||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(
|
||||
() => import('./components/notifications/notifications.vue'),
|
||||
),
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
|
@ -52,232 +33,103 @@ export default {
|
|||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal: defineAsyncComponent(
|
||||
() => import('./components/settings_modal/settings_modal.vue'),
|
||||
),
|
||||
UpdateNotification: defineAsyncComponent(
|
||||
() => import('./components/update_notification/update_notification.vue'),
|
||||
),
|
||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
StatusHistoryModal,
|
||||
GlobalNoticeList,
|
||||
GlobalNoticeList
|
||||
},
|
||||
data: () => ({
|
||||
mobileActivePanel: 'timeline',
|
||||
mobileActivePanel: 'timeline'
|
||||
}),
|
||||
watch: {
|
||||
themeApplied() {
|
||||
this.removeSplash()
|
||||
},
|
||||
currentTheme() {
|
||||
this.setThemeBodyClass()
|
||||
},
|
||||
layoutType() {
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
},
|
||||
},
|
||||
created() {
|
||||
created () {
|
||||
// Load the locale from the storage
|
||||
const value = useMergedConfigStore().mergedConfig.interfaceLanguage
|
||||
useI18nStore().setLanguage(value)
|
||||
useEmojiStore().loadUnicodeEmojiData(value)
|
||||
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
|
||||
// Create bound handlers
|
||||
this.updateScrollState = throttle(this.scrollHandler, 200)
|
||||
this.updateMobileState = throttle(this.resizeHandler, 200)
|
||||
},
|
||||
mounted() {
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.addEventListener('scroll', this.updateScrollState)
|
||||
|
||||
if (this.themeApplied) {
|
||||
this.setThemeBodyClass()
|
||||
this.removeSplash()
|
||||
}
|
||||
getOrCreateServiceWorker()
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
|
||||
},
|
||||
computed: {
|
||||
currentTheme() {
|
||||
if (this.styleDataUsed) {
|
||||
const styleMeta = this.styleDataUsed.find(
|
||||
(x) => x.component === '@meta',
|
||||
)
|
||||
|
||||
if (styleMeta !== undefined) {
|
||||
return styleMeta.directives.name.replaceAll(' ', '-').toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
return 'stock'
|
||||
},
|
||||
layoutModalClass() {
|
||||
return '-' + this.layoutType
|
||||
},
|
||||
classes() {
|
||||
classes () {
|
||||
return [
|
||||
{
|
||||
'-reverse': this.reverseLayout,
|
||||
'-no-sticky-headers': this.noSticky,
|
||||
'-has-new-post-button': this.newPostButtonShown,
|
||||
'-has-new-post-button': this.newPostButtonShown
|
||||
},
|
||||
'-' + this.layoutType,
|
||||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
navClasses() {
|
||||
const { navbarColumnStretch } = useMergedConfigStore().mergedConfig
|
||||
navClasses () {
|
||||
const { navbarColumnStretch } = this.$store.getters.mergedConfig
|
||||
return [
|
||||
'-' + this.layoutType,
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : []),
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : [])
|
||||
]
|
||||
},
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
userBackground() {
|
||||
return this.currentUser.background_image
|
||||
},
|
||||
instanceBackground() {
|
||||
return useMergedConfigStore().mergedConfig.hideInstanceWallpaper
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
return this.mergedConfig.hideInstanceWallpaper
|
||||
? null
|
||||
: this.instanceBackgroundUrl
|
||||
: this.$store.state.instance.background
|
||||
},
|
||||
background() {
|
||||
return this.userBackground || this.instanceBackground
|
||||
},
|
||||
bgStyle() {
|
||||
background () { return this.userBackground || this.instanceBackground },
|
||||
bgStyle () {
|
||||
if (this.background) {
|
||||
return {
|
||||
'--body-background-image': `url(${this.background})`,
|
||||
'--body-background-image': `url(${this.background})`
|
||||
}
|
||||
}
|
||||
},
|
||||
shoutJoined() {
|
||||
return useShoutStore().joined
|
||||
shout () { return this.$store.state.shout.joined },
|
||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
},
|
||||
isChats() {
|
||||
isChats () {
|
||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||
},
|
||||
isListEdit() {
|
||||
isListEdit () {
|
||||
return this.$route.name === 'lists-edit'
|
||||
},
|
||||
newPostButtonShown() {
|
||||
newPostButtonShown () {
|
||||
if (this.isChats) return false
|
||||
if (this.isListEdit) return false
|
||||
return (
|
||||
useMergedConfigStore().mergedConfig.alwaysShowNewPostButton ||
|
||||
this.layoutType === 'mobile'
|
||||
)
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
},
|
||||
shoutboxPosition() {
|
||||
return (
|
||||
useMergedConfigStore().mergedConfig.alwaysShowNewPostButton || false
|
||||
)
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
shoutboxPosition () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
||||
},
|
||||
hideShoutbox() {
|
||||
return useMergedConfigStore().mergedConfig.hideShoutbox
|
||||
hideShoutbox () {
|
||||
return this.$store.getters.mergedConfig.hideShoutbox
|
||||
},
|
||||
reverseLayout() {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } =
|
||||
useMergedConfigStore().mergedConfig
|
||||
layoutType () { return this.$store.state.interface.layoutType },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
reverseLayout () {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
||||
if (this.layoutType !== 'wide') {
|
||||
return reverseSetting
|
||||
} else {
|
||||
return thirdColumnMode === 'notifications'
|
||||
? reverseSetting
|
||||
: !reverseSetting
|
||||
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
||||
}
|
||||
},
|
||||
noSticky() {
|
||||
return useMergedConfigStore().mergedConfig.disableStickyHeaders
|
||||
},
|
||||
showScrollbars() {
|
||||
return useMergedConfigStore().mergedConfig.showScrollbars
|
||||
},
|
||||
scrollParent() {
|
||||
return window /* this.$refs.appContentRef */
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.instanceSpecificPanelPresent &&
|
||||
!useMergedConfigStore().mergedConfig.hideISP
|
||||
)
|
||||
},
|
||||
...mapState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapState(useInterfaceStore, [
|
||||
'themeApplied',
|
||||
'styleDataUsed',
|
||||
'layoutType',
|
||||
]),
|
||||
...mapState(useInstanceStore, ['styleDataUsed']),
|
||||
...mapState(useInstanceCapabilitiesStore, [
|
||||
'suggestionsEnabled',
|
||||
'editingAvailable',
|
||||
]),
|
||||
...mapState(useInstanceStore, {
|
||||
instanceBackgroundUrl: (store) => store.instanceIdentity.background,
|
||||
showFeaturesPanel: (store) => store.instanceIdentity.showFeaturesPanel,
|
||||
instanceSpecificPanelPresent: (store) =>
|
||||
store.instanceIdentity.showInstanceSpecificPanel &&
|
||||
store.instanceIdentity.instanceSpecificPanelContent,
|
||||
}),
|
||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
resizeHandler() {
|
||||
useInterfaceStore().setLayoutWidth(windowWidth())
|
||||
useInterfaceStore().setLayoutHeight(windowHeight())
|
||||
},
|
||||
scrollHandler() {
|
||||
const scrollPosition =
|
||||
this.scrollParent === window
|
||||
? window.scrollY
|
||||
: this.scrollParent.scrollTop
|
||||
|
||||
if (scrollPosition != 0) {
|
||||
this.$refs.appContentRef.classList.add(['-scrolled'])
|
||||
} else {
|
||||
this.$refs.appContentRef.classList.remove(['-scrolled'])
|
||||
}
|
||||
},
|
||||
setThemeBodyClass() {
|
||||
const themeName = this.currentTheme
|
||||
const classList = Array.from(document.body.classList)
|
||||
const oldTheme = classList.filter((c) => c.startsWith('theme-'))
|
||||
|
||||
if (themeName !== null && themeName !== '') {
|
||||
const newTheme = `theme-${themeName.toLowerCase()}`
|
||||
|
||||
// remove old theme reference if there are any
|
||||
if (oldTheme.length) {
|
||||
document.body.classList.replace(oldTheme[0], newTheme)
|
||||
} else {
|
||||
document.body.classList.add(newTheme)
|
||||
}
|
||||
} else {
|
||||
// remove theme reference if non-V3 theme is used
|
||||
document.body.classList.remove(...oldTheme)
|
||||
}
|
||||
},
|
||||
removeSplash() {
|
||||
document.querySelector('#status').textContent = this.$t(
|
||||
'splash.fun_' + Math.ceil(Math.random() * 4),
|
||||
)
|
||||
const splashscreenRoot = document.querySelector('#splash')
|
||||
splashscreenRoot.addEventListener('transitionend', () => {
|
||||
splashscreenRoot.remove()
|
||||
})
|
||||
setTimeout(() => {
|
||||
splashscreenRoot.remove() // forcibly remove it, should fix my plasma browser widget t. HJ
|
||||
}, 600)
|
||||
splashscreenRoot.classList.add('hidden')
|
||||
document.querySelector('#app').classList.remove('hidden')
|
||||
},
|
||||
},
|
||||
updateMobileState () {
|
||||
this.$store.dispatch('setLayoutWidth', windowWidth())
|
||||
this.$store.dispatch('setLayoutHeight', windowHeight())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
349
src/App.scss
|
|
@ -1,9 +1,6 @@
|
|||
// stylelint-disable rscss/class-format
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
@use "panel";
|
||||
|
||||
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
@import "./panel";
|
||||
|
||||
:root {
|
||||
--status-margin: 0.75em;
|
||||
|
|
@ -21,7 +18,7 @@
|
|||
}
|
||||
|
||||
html {
|
||||
font-size: var(--textSize, 1rem);
|
||||
font-size: var(--textSize, 14px);
|
||||
|
||||
--navbar-height: var(--navbarSize, 3.5rem);
|
||||
--emoji-size: var(--emojiSize, 32px);
|
||||
|
|
@ -33,12 +30,12 @@ body {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overscroll-behavior-y: none;
|
||||
overflow: clip scroll;
|
||||
overflow-x: clip;
|
||||
overflow-y: scroll;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
|
|
@ -227,8 +224,9 @@ nav {
|
|||
grid-template-rows: 1fr;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
place-content: flex-start center;
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
|
||||
|
|
@ -264,7 +262,8 @@ nav {
|
|||
position: sticky;
|
||||
top: var(--navbar-height);
|
||||
max-height: calc(100vh - var(--navbar-height));
|
||||
overflow: hidden auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-left: calc(var(--___paddingIncrease) * -1);
|
||||
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
|
||||
|
|
@ -382,10 +381,6 @@ nav {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
|
||||
&.-transparent {
|
||||
backdrop-filter: blur(0.125em) contrast(60%);
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -393,35 +388,39 @@ nav {
|
|||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(1px, 1px);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
line-height: var(--__line-height);
|
||||
.menu-item,
|
||||
.list-item {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: initial;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
font-size: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
a,
|
||||
button:not(.button-default) {
|
||||
color: var(--text);
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item {
|
||||
color: inherit;
|
||||
clear: both;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
border-color: var(--border);
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
border-top-width: 1px;
|
||||
width: 100%;
|
||||
line-height: var(--__line-height);
|
||||
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
||||
background: transparent;
|
||||
|
||||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__vertical-gap: 0.5em;
|
||||
|
||||
&.-non-interactive {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
&.-active,
|
||||
&:hover {
|
||||
|
|
@ -443,6 +442,20 @@ nav {
|
|||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
a,
|
||||
button:not(.button-default) {
|
||||
text-align: initial;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: inline;
|
||||
font-size: 100%;
|
||||
font-family: inherit;
|
||||
line-height: unset;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: var(--roundness);
|
||||
border-top-left-radius: var(--roundness);
|
||||
|
|
@ -456,42 +469,6 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
.menu-item,
|
||||
.list-item {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: initial;
|
||||
color: inherit;
|
||||
clear: both;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
||||
background: transparent;
|
||||
|
||||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__vertical-gap: 0.5em;
|
||||
|
||||
&.-non-interactive {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
a,
|
||||
button:not(.button-default) {
|
||||
text-align: initial;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: inline;
|
||||
font-family: inherit;
|
||||
line-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.button-unstyled {
|
||||
border: none;
|
||||
outline: none;
|
||||
|
|
@ -513,12 +490,6 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
label {
|
||||
&.-disabled {
|
||||
color: var(--textFaint);
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border: none;
|
||||
|
|
@ -535,10 +506,6 @@ textarea {
|
|||
height: unset;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--textFaint)
|
||||
}
|
||||
|
||||
--_padding: 0.5em;
|
||||
|
||||
border: none;
|
||||
|
|
@ -559,10 +526,6 @@ textarea {
|
|||
&[disabled="disabled"],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--textFaint);
|
||||
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&[type="range"] {
|
||||
|
|
@ -588,8 +551,6 @@ textarea {
|
|||
& + label::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
+ label::before {
|
||||
|
|
@ -689,8 +650,7 @@ option {
|
|||
list-style: none;
|
||||
display: grid;
|
||||
grid-auto-flow: row dense;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20em, 1fr));
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
li {
|
||||
border: 1px solid var(--border);
|
||||
|
|
@ -700,6 +660,11 @@ option {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
|
@ -711,6 +676,7 @@ option {
|
|||
--_roundness-right: 0;
|
||||
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
> *:first-child,
|
||||
|
|
@ -757,15 +723,17 @@ option {
|
|||
}
|
||||
|
||||
&.-dot {
|
||||
min-height: 0.6em;
|
||||
max-height: 0.6em;
|
||||
min-width: 0.6em;
|
||||
max-width: 0.6em;
|
||||
left: calc(50% + 0.5em);
|
||||
top: calc(50% - 1em);
|
||||
line-height: 0;
|
||||
min-height: 8px;
|
||||
max-height: 8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 0;
|
||||
font-size: 0;
|
||||
left: calc(50% - 4px);
|
||||
top: calc(50% - 4px);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
&.-counter {
|
||||
|
|
@ -796,18 +764,21 @@ option {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.notice-dismissible {
|
||||
display: flex;
|
||||
padding: 0.75em 1em;
|
||||
align-items: baseline;
|
||||
line-height: 1.5;
|
||||
.visibility-notice {
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--textFaint);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.notice-dismissible {
|
||||
padding-right: 4rem;
|
||||
position: relative;
|
||||
|
||||
.dismiss {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.5em;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
|
@ -843,7 +814,7 @@ option {
|
|||
.login-hint {
|
||||
text-align: center;
|
||||
|
||||
@media all and (width >= 801px) {
|
||||
@media all and (min-width: 801px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -865,7 +836,7 @@ option {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
@media all and (width <= 800px) {
|
||||
@media all and (max-width: 800px) {
|
||||
.mobile-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -943,169 +914,3 @@ option {
|
|||
color: var(--selectionText);
|
||||
background-color: var(--selectionBackground);
|
||||
}
|
||||
|
||||
#splash {
|
||||
pointer-events: none;
|
||||
// transition: opacity 0.5s;
|
||||
|
||||
#status {
|
||||
&.css-ok {
|
||||
&::before {
|
||||
display: inline-block;
|
||||
content: "CSS OK";
|
||||
}
|
||||
}
|
||||
|
||||
.initial-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#throbber {
|
||||
animation-duration: 3s;
|
||||
animation-name: bounce;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: normal;
|
||||
transform-origin: bottom center;
|
||||
|
||||
&.dead {
|
||||
animation-name: dead;
|
||||
animation-duration: 0.5s;
|
||||
animation-iteration-count: 1;
|
||||
transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
|
||||
}
|
||||
|
||||
@keyframes dead {
|
||||
0% {
|
||||
transform: rotateX(0) rotateY(0) rotateZ(0);
|
||||
}
|
||||
|
||||
5% {
|
||||
transform: rotateX(0) rotateY(0) rotateZ(1deg);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: rotateX(0) rotateY(0) rotateZ(-2deg);
|
||||
}
|
||||
|
||||
15% {
|
||||
transform: rotateX(0) rotateY(0) rotateZ(3deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: rotateX(0) rotateY(0) rotateZ(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotateX(0) rotateY(0) rotateZ(0);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotateX(10deg) rotateY(0) rotateZ(0);
|
||||
}
|
||||
|
||||
35% {
|
||||
transform: rotateX(-10deg) rotateY(0) rotateZ(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: rotateX(10deg) rotateY(0) rotateZ(0);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: rotateX(-10deg) rotateY(0) rotateZ(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotateX(10deg) rotateY(0) rotateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
|
||||
transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); /* easeInQuint */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
scale: 1 1;
|
||||
translate: 0 0;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
10% {
|
||||
scale: 1.2 0.8;
|
||||
translate: 0 0;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
30% {
|
||||
scale: 0.9 1.1;
|
||||
translate: 0 -40%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
40% {
|
||||
scale: 1.1 0.9;
|
||||
translate: 0 -50%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
45% {
|
||||
scale: 0.9 1.1;
|
||||
translate: 0 -45%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
50% {
|
||||
scale: 1.05 0.95;
|
||||
translate: 0 -40%;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
55% {
|
||||
scale: 0.985 1.025;
|
||||
translate: 0 -35%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
60% {
|
||||
scale: 1.0125 0.9985;
|
||||
translate: 0 -30%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
80% {
|
||||
scale: 1.0063 0.9938;
|
||||
translate: 0 -10%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
90% {
|
||||
scale: 1.2 0.8;
|
||||
translate: 0 0;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
100% {
|
||||
scale: 1 1;
|
||||
translate: 0 0;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@property --shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="themeApplied"
|
||||
v-show="$store.state.interface.themeApplied"
|
||||
id="app-loaded"
|
||||
:style="bgStyle"
|
||||
>
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
<Notifications v-if="currentUser" />
|
||||
<div
|
||||
id="content"
|
||||
ref="appContentRef"
|
||||
class="app-layout container"
|
||||
:class="classes"
|
||||
>
|
||||
|
|
@ -60,8 +59,8 @@
|
|||
/>
|
||||
</div>
|
||||
<MediaModal />
|
||||
<ShoutPanel
|
||||
v-if="currentUser && !hideShoutbox && shoutJoined"
|
||||
<shout-panel
|
||||
v-if="currentUser && shout && !hideShoutbox"
|
||||
:floating="true"
|
||||
class="floating-shout mobile-hidden"
|
||||
:class="{ '-left': shoutboxPosition }"
|
||||
|
|
@ -71,7 +70,7 @@
|
|||
<PostStatusModal />
|
||||
<EditStatusModal v-if="editingAvailable" />
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal :class="layoutModalClass" />
|
||||
<SettingsModal />
|
||||
<UpdateNotification />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 521 KiB After Width: | Height: | Size: 521 KiB |
|
|
@ -1,48 +1,21 @@
|
|||
/* global process */
|
||||
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeLayers,
|
||||
} from '@fortawesome/vue-fontawesome'
|
||||
|
||||
config.autoAddCss = false
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import App from '../App.vue'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||
import { applyStyleConfig } from '../services/style_setter/style_setter.js'
|
||||
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
||||
import {
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
} from '../services/window_utils/window_utils'
|
||||
import routes from './routes'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
import { useI18nStore } from 'src/stores/i18n'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth'
|
||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
|
||||
|
||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||
import {
|
||||
INSTANCE_DEFAULT_CONFIG_DEFINITIONS,
|
||||
INSTANCE_IDENTITY_DEFAULT_DEFINITIONS,
|
||||
} from 'src/modules/default_config_state.js'
|
||||
|
||||
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
|
||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { applyConfig } from '../services/style_setter/style_setter.js'
|
||||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
||||
|
||||
let staticInitialResults = null
|
||||
|
||||
|
|
@ -51,9 +24,7 @@ const parsedInitialResults = () => {
|
|||
return null
|
||||
}
|
||||
if (!staticInitialResults) {
|
||||
staticInitialResults = JSON.parse(
|
||||
document.getElementById('initial-results').textContent,
|
||||
)
|
||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||
}
|
||||
return staticInitialResults
|
||||
}
|
||||
|
|
@ -75,7 +46,7 @@ const preloadFetch = async (request) => {
|
|||
return {
|
||||
ok: true,
|
||||
json: () => requestData,
|
||||
text: () => requestData,
|
||||
text: () => requestData
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,60 +55,37 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const res = await preloadFetch('/api/v1/instance')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const textLimit = data.max_toot_chars
|
||||
const textlimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaExtensionsAvailable',
|
||||
data.pleroma,
|
||||
)
|
||||
useInstanceStore().set({
|
||||
path: 'limits.textLimit',
|
||||
value: textLimit,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'accountApprovalRequired',
|
||||
value: data.approval_required,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'birthdayRequired',
|
||||
value: !!data.pleroma?.metadata.birthday_required,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'birthdayMinAge',
|
||||
value: data.pleroma?.metadata.birthday_min_age || 0,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma.metadata.birthday_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma.metadata.birthday_min_age || 0 })
|
||||
|
||||
if (vapidPublicKey) {
|
||||
useInstanceStore().set({
|
||||
path: 'vapidPublicKey',
|
||||
value: vapidPublicKey,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
}
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load instance config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
// We should check for scrobbles support here but it requires userId
|
||||
// so instead we check for it where it's fetched (statuses.js)
|
||||
}
|
||||
|
||||
const getBackendProvidedConfig = async () => {
|
||||
const getBackendProvidedConfig = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/api/pleroma/frontend_configurations')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
return data.pleroma_fe
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Could not load backend-provided frontend config, potentially fatal',
|
||||
)
|
||||
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -148,7 +96,7 @@ const getStaticConfig = async () => {
|
|||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
|
|
@ -170,21 +118,48 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
config = Object.assign({}, staticConfig, apiConfig)
|
||||
}
|
||||
|
||||
Object.keys(INSTANCE_IDENTITY_DEFAULT_DEFINITIONS).forEach((source) =>
|
||||
useInstanceStore().set({
|
||||
value: config[source],
|
||||
path: `instanceIdentity.${source}`,
|
||||
}),
|
||||
)
|
||||
const copyInstanceOption = (name) => {
|
||||
store.dispatch('setInstanceOption', { name, value: config[name] })
|
||||
}
|
||||
|
||||
Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) =>
|
||||
useInstanceStore().set({
|
||||
value: config[source],
|
||||
path: `prefsStorage.${source}`,
|
||||
}),
|
||||
)
|
||||
copyInstanceOption('theme')
|
||||
copyInstanceOption('nsfwCensorImage')
|
||||
copyInstanceOption('background')
|
||||
copyInstanceOption('hidePostStats')
|
||||
copyInstanceOption('hideBotIndication')
|
||||
copyInstanceOption('hideUserStats')
|
||||
copyInstanceOption('hideFilteredStatuses')
|
||||
copyInstanceOption('logo')
|
||||
|
||||
useAuthFlowStore().setInitialStrategy(config.loginMethod)
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
value: typeof config.logoMask === 'undefined'
|
||||
? true
|
||||
: config.logoMask
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMargin',
|
||||
value: typeof config.logoMargin === 'undefined'
|
||||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
copyInstanceOption('showInstanceSpecificPanel')
|
||||
copyInstanceOption('minimalScopesMode')
|
||||
copyInstanceOption('hideMutedPosts')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('sidebarRight')
|
||||
}
|
||||
|
||||
const getTOS = async ({ store }) => {
|
||||
|
|
@ -192,12 +167,13 @@ const getTOS = async ({ store }) => {
|
|||
const res = await window.fetch('/static/terms-of-service.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
useInstanceStore().set({ path: 'instanceIdentity.tos', value: html })
|
||||
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load TOS\n", e)
|
||||
console.warn("Can't load TOS")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -206,15 +182,13 @@ const getInstancePanel = async ({ store }) => {
|
|||
const res = await preloadFetch('/instance/panel.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
useInstanceStore().set({
|
||||
path: 'instanceIdentity.instanceSpecificPanelContent',
|
||||
value: html,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load instance panel\n", e)
|
||||
console.warn("Can't load instance panel")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,218 +197,118 @@ const getStickers = async ({ store }) => {
|
|||
const res = await window.fetch('/static/stickers.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const stickers = (
|
||||
await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta,
|
||||
}
|
||||
}),
|
||||
)
|
||||
).sort((a, b) => {
|
||||
const stickers = (await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
return a.meta.title.localeCompare(b.meta.title)
|
||||
})
|
||||
useEmojiStore().setStickers(stickers)
|
||||
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load stickers\n", e)
|
||||
console.warn("Can't load stickers")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
const getAppSecret = async ({ store }) => {
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.userToken) {
|
||||
store.commit(
|
||||
'setBackendInteractor',
|
||||
backendInteractorService(oauth.getToken),
|
||||
)
|
||||
}
|
||||
const { state, commit } = store
|
||||
const { oauth, instance } = state
|
||||
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
|
||||
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
||||
.then((token) => {
|
||||
commit('setAppToken', token.access_token)
|
||||
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
})
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||
useInstanceStore().set({
|
||||
path: 'staffAccounts',
|
||||
value: nicknames,
|
||||
})
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
}
|
||||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
try {
|
||||
let res = await preloadFetch('/nodeinfo/2.1.json')
|
||||
if (!res.ok) res = await preloadFetch('/nodeinfo/2.0.json')
|
||||
const res = await preloadFetch('/nodeinfo/2.0.json')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
const features = metadata.features
|
||||
useInstanceStore().set({
|
||||
path: 'name',
|
||||
value: metadata.nodeName,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'registrationOpen',
|
||||
value: data.openRegistrations,
|
||||
})
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'mediaProxyAvailable',
|
||||
features.includes('media_proxy'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'safeDM',
|
||||
features.includes('safe_dm_mentions'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'shoutAvailable',
|
||||
features.includes('chat'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaChatMessagesAvailable',
|
||||
features.includes('pleroma_chat_messages'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaCustomEmojiReactionsAvailable',
|
||||
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
|
||||
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
|
||||
|
||||
features.includes('pleroma_custom_emoji_reactions') ||
|
||||
features.includes('custom_emoji_reactions'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaBookmarkFoldersAvailable',
|
||||
features.includes('pleroma:bookmark_folders'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'gopherAvailable',
|
||||
features.includes('gopher'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pollsAvailable',
|
||||
features.includes('polls'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'editingAvailable',
|
||||
features.includes('editing'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'mailerEnabled',
|
||||
metadata.mailerEnabled,
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'quotingAvailable',
|
||||
features.includes('quote_posting'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'groupActorAvailable',
|
||||
features.includes('pleroma:group_actors'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'blockExpiration',
|
||||
features.includes('pleroma:block_expiration'),
|
||||
)
|
||||
useInstanceStore().set({
|
||||
path: 'localBubbleInstances',
|
||||
value: metadata.localBubbleInstances ?? [],
|
||||
})
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'localBubble',
|
||||
(metadata.localBubbleInstances ?? []).length > 0,
|
||||
)
|
||||
|
||||
useInstanceStore().set({
|
||||
path: 'limits.pollLimits',
|
||||
value: metadata.pollLimits,
|
||||
})
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
useInstanceStore().set({
|
||||
path: 'limits.uploadlimit',
|
||||
value: parseInt(uploadLimits.general),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.avatarlimit',
|
||||
value: parseInt(uploadLimits.avatar),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.backgroundlimit',
|
||||
value: parseInt(uploadLimits.background),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.bannerlimit',
|
||||
value: parseInt(uploadLimits.banner),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.fieldsLimits',
|
||||
value: metadata.fieldsLimits,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
||||
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
||||
|
||||
useInstanceStore().set({
|
||||
path: 'restrictedNicknames',
|
||||
value: metadata.restrictedNicknames,
|
||||
})
|
||||
useInstanceCapabilitiesStore().set('postFormats', metadata.postFormats)
|
||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||
|
||||
const suggestions = metadata.suggestions
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'suggestionsEnabled',
|
||||
suggestions.enabled,
|
||||
)
|
||||
// this is unused, why?
|
||||
useInstanceCapabilitiesStore().set('suggestionsWeb', suggestions.web)
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
||||
|
||||
const software = data.software
|
||||
useInstanceStore().set({
|
||||
path: 'backendVersion',
|
||||
value: software.version,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'backendRepository',
|
||||
value: software.repository,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
|
||||
|
||||
const priv = metadata.private
|
||||
useInstanceStore().set({ path: 'privateMode', value: priv })
|
||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
useInstanceStore().set({
|
||||
path: 'frontendVersion',
|
||||
value: frontendVersion,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
|
||||
const federation = metadata.federation
|
||||
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'tagPolicyAvailable',
|
||||
typeof federation.mrf_policies === 'undefined'
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'tagPolicyAvailable',
|
||||
value: typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy'),
|
||||
)
|
||||
|
||||
useInstanceStore().set({
|
||||
path: 'federationPolicy',
|
||||
value: federation,
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'federating',
|
||||
value:
|
||||
typeof federation.enabled === 'undefined' ? true : federation.enabled,
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
value: typeof federation.enabled === 'undefined'
|
||||
? true
|
||||
: federation.enabled
|
||||
})
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
useInstanceStore().set({
|
||||
path: 'accountActivationRequired',
|
||||
value: accountActivationRequired,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load nodeinfo')
|
||||
|
|
@ -444,93 +318,27 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const setConfig = async ({ store }) => {
|
||||
// apiConfig, staticConfig
|
||||
const configInfos = await Promise.all([
|
||||
getBackendProvidedConfig({ store }),
|
||||
getStaticConfig(),
|
||||
])
|
||||
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
getAppSecret({ store })
|
||||
await setSettings({ store, apiConfig, staticConfig })
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.getUserToken) {
|
||||
return store.dispatch('loginUser', oauth.getUserToken)
|
||||
if (store.getters.getUserToken()) {
|
||||
try {
|
||||
await store.dispatch('loginUser', store.getters.getUserToken())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
||||
const app = createApp(App)
|
||||
// Must have app use pinia before we do anything that touches the store
|
||||
// https://pinia.vuejs.org/core-concepts/plugins.html#Introduction
|
||||
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
|
||||
app.use(pinia)
|
||||
|
||||
const waitForAllStoresToLoad = async () => {
|
||||
// the stores that do not persist technically do not need to be awaited here,
|
||||
// but that involves either hard-coding the stores in some place (prone to errors)
|
||||
// or writing another vite plugin to analyze which stores needs persisting (++load time)
|
||||
const allStores = import.meta.glob('../stores/*.js', { eager: true })
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// do some checks to avoid common errors
|
||||
if (!Object.keys(allStores).length) {
|
||||
throw new Error(
|
||||
'No stores are available. Check the code in src/boot/after_store.js',
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Object.entries(allStores).map(async ([name, mod]) => {
|
||||
const isStoreName = (name) => name.startsWith('use')
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (Object.keys(mod).filter(isStoreName).length !== 1) {
|
||||
throw new Error(
|
||||
'Each store file must export exactly one store as a named export. Check your code in src/stores/',
|
||||
)
|
||||
}
|
||||
}
|
||||
const storeFuncName = Object.keys(mod).find(isStoreName)
|
||||
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
||||
const p = mod[storeFuncName]().$persistLoaded
|
||||
if (!(p instanceof Promise)) {
|
||||
throw new Error(
|
||||
`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`,
|
||||
)
|
||||
}
|
||||
await p
|
||||
} else {
|
||||
throw new Error(
|
||||
`Store module ${name} does not export a 'use...' function`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await waitForAllStoresToLoad()
|
||||
} catch (e) {
|
||||
console.error('Cannot load stores:', e)
|
||||
storageError = e
|
||||
}
|
||||
|
||||
if (storageError) {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'errors.storage_unavailable',
|
||||
level: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
useInterfaceStore().setLayoutWidth(windowWidth())
|
||||
useInterfaceStore().setLayoutHeight(windowHeight())
|
||||
window.syncConfig = useSyncConfigStore()
|
||||
window.mergedConfig = useMergedConfigStore()
|
||||
window.localConfig = useLocalConfigStore()
|
||||
window.highlightConfig = useUserHighlightStore()
|
||||
const afterStoreSetup = async ({ store, i18n }) => {
|
||||
store.dispatch('setLayoutWidth', windowWidth())
|
||||
store.dispatch('setLayoutHeight', windowHeight())
|
||||
|
||||
FaviconService.initFaviconService()
|
||||
initServiceWorker(store)
|
||||
|
|
@ -538,25 +346,13 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
window.addEventListener('focus', () => updateFocus())
|
||||
|
||||
const overrides = window.___pleromafe_dev_overrides || {}
|
||||
const server =
|
||||
typeof overrides.target !== 'undefined'
|
||||
? overrides.target
|
||||
: window.location.origin
|
||||
useInstanceStore().set({ path: 'server', value: server })
|
||||
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
|
||||
await setConfig({ store })
|
||||
try {
|
||||
await useInterfaceStore()
|
||||
.applyTheme()
|
||||
.catch((e) => {
|
||||
console.error('Error setting theme', e)
|
||||
})
|
||||
} catch (e) {
|
||||
window.splashError(e)
|
||||
return Promise.reject(e)
|
||||
}
|
||||
await store.dispatch('setTheme')
|
||||
|
||||
applyStyleConfig(useMergedConfigStore().mergedConfig, i18n.global)
|
||||
applyConfig(store.state.config)
|
||||
|
||||
// Now we can try getting the server settings and logging in
|
||||
// Most of these are preloaded into the index.html so blocking is minimized
|
||||
|
|
@ -564,13 +360,12 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
checkOAuthToken({ store }),
|
||||
getInstancePanel({ store }),
|
||||
getNodeInfo({ store }),
|
||||
getInstanceConfig({ store }),
|
||||
]).catch((e) => Promise.reject(e))
|
||||
getInstanceConfig({ store })
|
||||
])
|
||||
|
||||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
store.dispatch('loadDrafts')
|
||||
useAnnouncementsStore().startFetchingAnnouncements()
|
||||
store.dispatch('startFetchingAnnouncements')
|
||||
getTOS({ store })
|
||||
getStickers({ store })
|
||||
|
||||
|
|
@ -578,26 +373,19 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
history: createWebHistory(),
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some((m) => m.meta.dontScroll)) {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
return false
|
||||
}
|
||||
return savedPosition || { left: 0, top: 0 }
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
useI18nStore().setI18n(i18n)
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(i18n)
|
||||
|
||||
// Little thing to get out of invalid theme state
|
||||
window.resetThemes = () => {
|
||||
useInterfaceStore().resetThemeV3()
|
||||
useInterfaceStore().resetThemeV3Palette()
|
||||
useInterfaceStore().resetThemeV2()
|
||||
}
|
||||
|
||||
app.use(vClickOutside)
|
||||
app.use(VBodyScrollLock)
|
||||
app.use(VueVirtualScroller)
|
||||
|
|
@ -609,6 +397,7 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
app.config.unwrapInjectedRef = true
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +1,38 @@
|
|||
import About from 'components/about/about.vue'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
import AuthForm from 'components/auth_form/auth_form.js'
|
||||
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
||||
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
|
||||
import Chat from 'components/chat/chat.vue'
|
||||
import ChatList from 'components/chat_list/chat_list.vue'
|
||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import Drafts from 'components/drafts/drafts.vue'
|
||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||
import Interactions from 'components/interactions/interactions.vue'
|
||||
import Lists from 'components/lists/lists.vue'
|
||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import PasswordReset from 'components/password_reset/password_reset.vue'
|
||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
|
||||
import Registration from 'components/registration/registration.vue'
|
||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||
import Interactions from 'components/interactions/interactions.vue'
|
||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import ChatList from 'components/chat_list/chat_list.vue'
|
||||
import Chat from 'components/chat/chat.vue'
|
||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import Registration from 'components/registration/registration.vue'
|
||||
import PasswordReset from 'components/password_reset/password_reset.vue'
|
||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import AuthForm from 'components/auth_form/auth_form.js'
|
||||
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
|
||||
import About from 'components/about/about.vue'
|
||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import Lists from 'components/lists/lists.vue'
|
||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
if (store.state.users.currentUser) {
|
||||
next()
|
||||
} else {
|
||||
next(
|
||||
useInstanceStore().instanceIdentity.redirectRootNoLogin || '/main/all',
|
||||
)
|
||||
next(store.state.instance.redirectRootNoLogin || '/main/all')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,178 +40,59 @@ export default (store) => {
|
|||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect: () => {
|
||||
return (
|
||||
(store.state.users.currentUser
|
||||
? useInstanceStore().instanceIdentity.redirectRootLogin
|
||||
: useInstanceStore().instanceIdentity.redirectRootNoLogin) ||
|
||||
'/main/all'
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'public-external-timeline',
|
||||
path: '/main/all',
|
||||
component: PublicAndExternalTimeline,
|
||||
},
|
||||
{
|
||||
name: 'public-timeline',
|
||||
path: '/main/public',
|
||||
component: PublicTimeline,
|
||||
},
|
||||
{
|
||||
name: 'friends',
|
||||
path: '/main/friends',
|
||||
component: FriendsTimeline,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
redirect: _to => {
|
||||
return (store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
}
|
||||
},
|
||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'bubble', path: '/bubble', component: BubbleTimeline },
|
||||
{
|
||||
name: 'conversation',
|
||||
path: '/notice/:id',
|
||||
component: ConversationPage,
|
||||
meta: { dontScroll: true },
|
||||
},
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
|
||||
{
|
||||
name: 'remote-user-profile-acct',
|
||||
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'remote-user-profile',
|
||||
path: '/remote-users/:hostname/:username',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'external-user-profile',
|
||||
path: '/users/$:id',
|
||||
component: UserProfile,
|
||||
},
|
||||
{
|
||||
name: 'interactions',
|
||||
path: '/users/:username/interactions',
|
||||
component: Interactions,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'dms',
|
||||
path: '/users/:username/dms',
|
||||
component: DMs,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{
|
||||
name: 'password-reset',
|
||||
path: '/password-reset',
|
||||
component: PasswordReset,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'registration-token',
|
||||
path: '/registration/:token',
|
||||
component: Registration,
|
||||
},
|
||||
{
|
||||
name: 'friend-requests',
|
||||
path: '/friend-requests',
|
||||
component: FollowRequests,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
path: '/:username/notifications',
|
||||
component: Notifications,
|
||||
props: () => ({ disableTeleport: true }),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{
|
||||
name: 'shout-panel',
|
||||
path: '/shout-panel',
|
||||
component: ShoutPanel,
|
||||
props: () => ({ floating: false }),
|
||||
},
|
||||
{
|
||||
name: 'oauth-callback',
|
||||
path: '/oauth-callback',
|
||||
component: OAuthCallback,
|
||||
props: (route) => ({ code: route.query.code }),
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: '/search',
|
||||
component: Search,
|
||||
props: (route) => ({ query: route.query.query }),
|
||||
},
|
||||
{
|
||||
name: 'who-to-follow',
|
||||
path: '/who-to-follow',
|
||||
component: WhoToFollow,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{
|
||||
name: 'announcements',
|
||||
path: '/announcements',
|
||||
component: AnnouncementsPage,
|
||||
},
|
||||
{ name: 'drafts', path: '/drafts', component: Drafts },
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
||||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||
{ name: 'lists', path: '/lists', component: Lists },
|
||||
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
|
||||
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
|
||||
{ name: 'lists-new', path: '/lists/new', component: ListsEdit },
|
||||
{
|
||||
name: 'edit-navigation',
|
||||
path: '/nav-edit',
|
||||
component: NavPanel,
|
||||
props: () => ({ forceExpand: true, forceEditMode: true }),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folders',
|
||||
path: '/bookmark_folders',
|
||||
component: BookmarkFolders,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder-new',
|
||||
path: '/bookmarks/new-folder',
|
||||
component: BookmarkFolderEdit,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder',
|
||||
path: '/bookmarks/:id',
|
||||
component: BookmarkTimeline,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder-edit',
|
||||
path: '/bookmarks/:id/edit',
|
||||
component: BookmarkFolderEdit,
|
||||
},
|
||||
{ name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute }
|
||||
]
|
||||
|
||||
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
|
||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||
routes = routes.concat([
|
||||
{
|
||||
name: 'chat',
|
||||
path: '/users/:username/chats/:recipient_id',
|
||||
component: Chat,
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'chats',
|
||||
path: '/users/:username/chats',
|
||||
component: ChatList,
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
|
||||
const About = {
|
||||
components: {
|
||||
|
|
@ -13,20 +10,16 @@ const About = {
|
|||
FeaturesPanel,
|
||||
TermsOfServicePanel,
|
||||
StaffPanel,
|
||||
MRFTransparencyPanel,
|
||||
MRFTransparencyPanel
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel() {
|
||||
return useInstanceStore().instanceIdentity.showFeaturesPanel
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&
|
||||
!useMergedConfigStore().mergedConfig.hideISP &&
|
||||
useInstanceStore().instanceIdentity.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default About
|
||||
|
|
|
|||
|
|
@ -1,105 +1,98 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useReportsStore } from 'src/stores/reports'
|
||||
|
||||
import Popover from '../popover/popover.vue'
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faEllipsisV
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisV)
|
||||
library.add(
|
||||
faEllipsisV
|
||||
)
|
||||
|
||||
const AccountActions = {
|
||||
props: ['user', 'relationship'],
|
||||
data() {
|
||||
props: [
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingConfirmBlock: false,
|
||||
showingConfirmRemoveFollower: false,
|
||||
showingConfirmRemoveFollower: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover,
|
||||
UserListMenu,
|
||||
ConfirmModal,
|
||||
UserTimedFilterModal,
|
||||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
showConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
hideConfirmBlock() {
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats() {
|
||||
showConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
showRepeats () {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
hideRepeats() {
|
||||
hideRepeats () {
|
||||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser() {
|
||||
if (this.$refs.timedBlockDialog) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
blockUser () {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showingConfirmBlock = true
|
||||
}
|
||||
this.showConfirmBlock()
|
||||
}
|
||||
},
|
||||
doBlockUser() {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
doBlockUser () {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser() {
|
||||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers() {
|
||||
removeUserFromFollowers () {
|
||||
if (!this.shouldConfirmRemoveUserFromFollowers) {
|
||||
this.doRemoveUserFromFollowers()
|
||||
} else {
|
||||
this.showConfirmRemoveUserFromFollowers()
|
||||
}
|
||||
},
|
||||
doRemoveUserFromFollowers() {
|
||||
doRemoveUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
this.hideConfirmRemoveUserFromFollowers()
|
||||
},
|
||||
reportUser() {
|
||||
useReportsStore().openUserReportingModal({ userId: this.user.id })
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
},
|
||||
openChat() {
|
||||
openChat () {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: {
|
||||
username: this.$store.state.users.currentUser.screen_name,
|
||||
recipient_id: this.user.id,
|
||||
},
|
||||
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock() {
|
||||
return useMergedConfigStore().mergedConfig.modalOnBlock
|
||||
shouldConfirmBlock () {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
shouldConfirmRemoveUserFromFollowers() {
|
||||
return useMergedConfigStore().mergedConfig.modalOnRemoveUserFromFollowers
|
||||
shouldConfirmRemoveUserFromFollowers () {
|
||||
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
|
||||
},
|
||||
...mapState(useInstanceCapabilitiesStore, [
|
||||
'blockExpiration',
|
||||
'pleromaChatMessagesAvailable',
|
||||
]),
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountActions
|
||||
|
|
|
|||
|
|
@ -3,85 +3,66 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="relationship.following">
|
||||
<div
|
||||
<button
|
||||
v-if="relationship.showing_reblogs"
|
||||
class="menu-item dropdown-item"
|
||||
class="dropdown-item menu-item"
|
||||
@click="hideRepeats"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click="hideRepeats"
|
||||
>
|
||||
{{ $t('user_card.hide_repeats') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
{{ $t('user_card.hide_repeats') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!relationship.showing_reblogs"
|
||||
class="menu-item dropdown-item"
|
||||
class="dropdown-item menu-item"
|
||||
@click="showRepeats"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click="showRepeats"
|
||||
>
|
||||
{{ $t('user_card.show_repeats') }}
|
||||
</button>
|
||||
</div>
|
||||
{{ $t('user_card.show_repeats') }}
|
||||
</button>
|
||||
<div
|
||||
role="separator"
|
||||
class="dropdown-divider"
|
||||
/>
|
||||
</template>
|
||||
<UserListMenu :user="user" />
|
||||
<div
|
||||
<button
|
||||
v-if="relationship.followed_by"
|
||||
class="menu-item dropdown-item"
|
||||
class="dropdown-item menu-item"
|
||||
@click="removeUserFromFollowers"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click="removeUserFromFollowers"
|
||||
>
|
||||
{{ $t('user_card.remove_follower') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="menu-item dropdown-item">
|
||||
<button
|
||||
v-if="relationship.blocking"
|
||||
class="main-button"
|
||||
@click="unblockUser"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="main-button"
|
||||
@click="blockUser"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="menu-item dropdown-item">
|
||||
<button
|
||||
class="main-button"
|
||||
@click="reportUser"
|
||||
>
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
{{ $t('user_card.remove_follower') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="relationship.blocking"
|
||||
class="dropdown-item menu-item"
|
||||
@click="unblockUser"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="dropdown-item menu-item"
|
||||
@click="blockUser"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item menu-item"
|
||||
@click="reportUser"
|
||||
>
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="pleromaChatMessagesAvailable"
|
||||
class="menu-item dropdown-item"
|
||||
class="dropdown-item menu-item"
|
||||
@click="openChat"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click="openChat"
|
||||
>
|
||||
{{ $t('user_card.message') }}
|
||||
</button>
|
||||
</div>
|
||||
{{ $t('user_card.message') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
|
|
@ -95,8 +76,7 @@
|
|||
</Popover>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
v-if="showingConfirmBlock && !blockExpiration"
|
||||
ref="blockDialog"
|
||||
v-if="showingConfirmBlock"
|
||||
:title="$t('user_card.block_confirm_title')"
|
||||
:confirm-text="$t('user_card.block_confirm_accept_button')"
|
||||
:cancel-text="$t('user_card.block_confirm_cancel_button')"
|
||||
|
|
@ -106,7 +86,6 @@
|
|||
<i18n-t
|
||||
keypath="user_card.block_confirm"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #user>
|
||||
<span
|
||||
|
|
@ -128,7 +107,6 @@
|
|||
<i18n-t
|
||||
keypath="user_card.remove_follower_confirm"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #user>
|
||||
<span
|
||||
|
|
@ -137,12 +115,6 @@
|
|||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
<UserTimedFilterModal
|
||||
v-if="blockExpiration"
|
||||
ref="timedBlockDialog"
|
||||
:is-mute="false"
|
||||
:user="user"
|
||||
/>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,51 +1,53 @@
|
|||
export default {
|
||||
name: 'Alert',
|
||||
selector: '.alert',
|
||||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Link',
|
||||
'Border',
|
||||
'ButtonUnstyled'
|
||||
],
|
||||
variants: {
|
||||
normal: '.neutral',
|
||||
error: '.error',
|
||||
warning: '.warning',
|
||||
success: '.success',
|
||||
},
|
||||
editor: {
|
||||
border: 1,
|
||||
aspect: '3 / 1',
|
||||
success: '.success'
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--text',
|
||||
opacity: 0.5,
|
||||
blur: '9px',
|
||||
},
|
||||
blur: '9px'
|
||||
}
|
||||
},
|
||||
{
|
||||
parent: {
|
||||
component: 'Alert',
|
||||
component: 'Alert'
|
||||
},
|
||||
component: 'Border',
|
||||
directives: {
|
||||
textColor: '--parent',
|
||||
},
|
||||
textColor: '--parent'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'error',
|
||||
directives: {
|
||||
background: '--cRed',
|
||||
},
|
||||
background: '--cRed'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'warning',
|
||||
directives: {
|
||||
background: '--cOrange',
|
||||
},
|
||||
background: '--cOrange'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'success',
|
||||
directives: {
|
||||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
],
|
||||
background: '--cGreen'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,130 +1,108 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import RichContent from '../rich_content/rich_content.jsx'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent,
|
||||
RichContent
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: undefined,
|
||||
allDay: undefined
|
||||
},
|
||||
editError: '',
|
||||
editError: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
announcement: Object
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
canEditAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
)
|
||||
canEditAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
},
|
||||
content() {
|
||||
content () {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead() {
|
||||
isRead () {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt() {
|
||||
publishedAt () {
|
||||
const time = this.announcement.published_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
startsAt() {
|
||||
startsAt () {
|
||||
const time = this.announcement.starts_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
endsAt() {
|
||||
endsAt () {
|
||||
const time = this.announcement.ends_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
inactive() {
|
||||
inactive () {
|
||||
return this.announcement.inactive
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsRead() {
|
||||
markAsRead () {
|
||||
if (!this.isRead) {
|
||||
return useAnnouncementsStore().markAnnouncementAsRead(
|
||||
this.announcement.id,
|
||||
)
|
||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement() {
|
||||
return useAnnouncementsStore().deleteAnnouncement(this.announcement.id)
|
||||
deleteAnnouncement () {
|
||||
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate(time, locale) {
|
||||
formatTimeOrDate (time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement.all_day
|
||||
? d.toLocaleDateString(locale)
|
||||
: d.toLocaleString(locale)
|
||||
return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode() {
|
||||
enterEditMode () {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma.raw_content
|
||||
this.editedAnnouncement.startsAt = this.announcement.starts_at
|
||||
this.editedAnnouncement.endsAt = this.announcement.ends_at
|
||||
this.editedAnnouncement.allDay = this.announcement.all_day
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit() {
|
||||
useAnnouncementsStore()
|
||||
.editAnnouncement({
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement,
|
||||
})
|
||||
submitEdit () {
|
||||
this.$store.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit() {
|
||||
cancelEdit () {
|
||||
this.editing = false
|
||||
},
|
||||
clearError() {
|
||||
clearError () {
|
||||
this.editError = undefined
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Announcement
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import Checkbox from '../checkbox/checkbox.vue'
|
|||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
Checkbox,
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
disabled: Boolean,
|
||||
},
|
||||
disabled: Boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementEditor
|
||||
|
|
|
|||
|
|
@ -34,9 +34,8 @@
|
|||
id="announcement-all-day"
|
||||
v-model="announcement.allDay"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ $t('announcements.all_day_prompt') }}
|
||||
</Checkbox>
|
||||
/>
|
||||
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -56,7 +55,7 @@
|
|||
.post-textarea {
|
||||
resize: vertical;
|
||||
height: 10em;
|
||||
overflow: visible;
|
||||
overflow: none;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,58 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
|
||||
const AnnouncementsPage = {
|
||||
components: {
|
||||
Announcement,
|
||||
AnnouncementEditor,
|
||||
AnnouncementEditor
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: false,
|
||||
allDay: false
|
||||
},
|
||||
posting: false,
|
||||
error: undefined,
|
||||
error: undefined
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
useAnnouncementsStore().fetchAnnouncements()
|
||||
mounted () {
|
||||
this.$store.dispatch('fetchAnnouncements')
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
announcements() {
|
||||
return useAnnouncementsStore().announcements
|
||||
},
|
||||
canPostAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
)
|
||||
announcements () {
|
||||
return this.$store.state.announcements.announcements
|
||||
},
|
||||
canPostAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement() {
|
||||
postAnnouncement () {
|
||||
this.posting = true
|
||||
useAnnouncementsStore()
|
||||
.postAnnouncement(this.newAnnouncement)
|
||||
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError() {
|
||||
clearError () {
|
||||
this.error = undefined
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementsPage
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="panel panel-default announcements-page">
|
||||
<div class="panel-heading">
|
||||
<h1 class="title">
|
||||
<span>
|
||||
{{ $t('announcements.page_header') }}
|
||||
</h1>
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
export default {
|
||||
emits: ['resetAsyncComponent'],
|
||||
methods: {
|
||||
retry() {
|
||||
retry () {
|
||||
this.$emit('resetAsyncComponent')
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,22 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAlignRight,
|
||||
faFile,
|
||||
faImage,
|
||||
faMusic,
|
||||
faPencilAlt,
|
||||
faPlayCircle,
|
||||
faSearchPlus,
|
||||
faStop,
|
||||
faTimes,
|
||||
faTrashAlt,
|
||||
faImage,
|
||||
faVideo,
|
||||
faPlayCircle,
|
||||
faTimes,
|
||||
faStop,
|
||||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
|
|
@ -36,7 +30,7 @@ library.add(
|
|||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight,
|
||||
faAlignRight
|
||||
)
|
||||
|
||||
const Attachment = {
|
||||
|
|
@ -51,72 +45,72 @@ const Attachment = {
|
|||
'remove',
|
||||
'shiftUp',
|
||||
'shiftDn',
|
||||
'edit',
|
||||
'edit'
|
||||
],
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
nsfwImage:
|
||||
useInstanceStore().instanceIdentity.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: useMergedConfigStore().mergedConfig.hideNsfw,
|
||||
preloadImage: useMergedConfigStore().mergedConfig.preloadImage,
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img: this.attachment.type === 'image' && document.createElement('img'),
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
showDescription: false,
|
||||
showDescription: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment,
|
||||
VideoAttachment
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
classNames () {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
'-nsfw-placeholder': this.hidden,
|
||||
'-editable': this.edit !== undefined,
|
||||
'-compact': this.compact,
|
||||
'-compact': this.compact
|
||||
},
|
||||
'-type-' + this.attachment.type,
|
||||
'-type-' + this.type,
|
||||
this.size && '-size-' + this.size,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
]
|
||||
},
|
||||
usePlaceholder() {
|
||||
usePlaceholder () {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit() {
|
||||
return this.mergedConfig.useContainFit
|
||||
useContainFit () {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
placeholderName() {
|
||||
placeholderName () {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
return this.attachment.type.toUpperCase()
|
||||
return this.type.toUpperCase()
|
||||
}
|
||||
return this.attachment.description
|
||||
},
|
||||
placeholderIconClass() {
|
||||
if (this.attachment.type === 'image') return 'image'
|
||||
if (this.attachment.type === 'video') return 'video'
|
||||
if (this.attachment.type === 'audio') return 'music'
|
||||
placeholderIconClass () {
|
||||
if (this.type === 'image') return 'image'
|
||||
if (this.type === 'video') return 'video'
|
||||
if (this.type === 'audio') return 'music'
|
||||
return 'file'
|
||||
},
|
||||
referrerpolicy() {
|
||||
return useInstanceCapabilitiesStore().mediaProxyAvailable
|
||||
? ''
|
||||
: 'no-referrer'
|
||||
referrerpolicy () {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
hidden() {
|
||||
type () {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
},
|
||||
hidden () {
|
||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty() {
|
||||
return this.attachment.type === 'html' && !this.attachment.oembed
|
||||
isEmpty () {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
},
|
||||
useModal() {
|
||||
useModal () {
|
||||
let modalTypes = []
|
||||
switch (this.size) {
|
||||
case 'hide':
|
||||
|
|
@ -129,66 +123,64 @@ const Attachment = {
|
|||
: ['image']
|
||||
break
|
||||
}
|
||||
return modalTypes.includes(this.attachment.type)
|
||||
return modalTypes.includes(this.type)
|
||||
},
|
||||
videoTag() {
|
||||
videoTag () {
|
||||
return this.useModal ? 'button' : 'span'
|
||||
},
|
||||
...mapState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
watch: {
|
||||
'attachment.description'(newVal) {
|
||||
'attachment.description' (newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription(newVal) {
|
||||
localDescription (newVal) {
|
||||
this.onEdit(newVal)
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkClicked({ target }) {
|
||||
linkClicked ({ target }) {
|
||||
if (target.tagName === 'A') {
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
openModal() {
|
||||
openModal (event) {
|
||||
if (this.useModal) {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
} else if (this.attachment.type === 'unknown') {
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
} else if (this.type === 'unknown') {
|
||||
window.open(this.attachment.url)
|
||||
}
|
||||
},
|
||||
openModalForce() {
|
||||
openModalForce (event) {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
},
|
||||
onEdit(event) {
|
||||
onEdit (event) {
|
||||
this.edit && this.edit(this.attachment, event)
|
||||
},
|
||||
onRemove() {
|
||||
onRemove () {
|
||||
this.remove && this.remove(this.attachment)
|
||||
},
|
||||
onShiftUp() {
|
||||
onShiftUp () {
|
||||
this.shiftUp && this.shiftUp(this.attachment)
|
||||
},
|
||||
onShiftDn() {
|
||||
onShiftDn () {
|
||||
this.shiftDn && this.shiftDn(this.attachment)
|
||||
},
|
||||
stopFlash() {
|
||||
stopFlash () {
|
||||
this.$refs.flash.closePlayer()
|
||||
},
|
||||
setFlashLoaded(event) {
|
||||
setFlashLoaded (event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription() {
|
||||
toggleDescription () {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden(event) {
|
||||
toggleHidden (event) {
|
||||
if (
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
!this.showHidden &&
|
||||
(this.attachment.type !== 'video' ||
|
||||
this.mergedConfig.playVideosInModal)
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
return
|
||||
|
|
@ -208,12 +200,12 @@ const Attachment = {
|
|||
this.showHidden = !this.showHidden
|
||||
}
|
||||
},
|
||||
onImageLoad(image) {
|
||||
onImageLoad (image) {
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Attachment
|
||||
|
|
|
|||
|
|
@ -107,9 +107,9 @@
|
|||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 4.5em;
|
||||
top: calc(50% - 2.25rem);
|
||||
left: calc(50% - 2.25rem);
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
|
||||
|
||||
|
|
@ -177,8 +177,7 @@
|
|||
.text {
|
||||
flex: 2;
|
||||
margin: 8px;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
word-break: break-all;
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
|
|
|
|||
24
src/components/attachment/attachment.style.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export default {
|
||||
name: 'Attachment',
|
||||
selector: '.Attachment',
|
||||
validInnerComponents: [
|
||||
'Border',
|
||||
'ButtonUnstyled',
|
||||
'Input'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'ButtonUnstyled',
|
||||
parent: { component: 'Attachment' },
|
||||
directives: {
|
||||
background: '#FFFFFF',
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="usePlaceholder"
|
||||
class="Attachment -placeholder button-default"
|
||||
class="Attachment -placeholder button-unstyled"
|
||||
:class="classNames"
|
||||
@click="openModal"
|
||||
>
|
||||
<a
|
||||
v-if="attachment.type !== 'html'"
|
||||
v-if="type !== 'html'"
|
||||
class="placeholder"
|
||||
target="_blank"
|
||||
:href="attachment.url"
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
<FAIcon icon="trash-alt" />
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
:src="nsfwImage"
|
||||
>
|
||||
<FAIcon
|
||||
v-if="attachment.type === 'video'"
|
||||
v-if="type === 'video'"
|
||||
class="play-icon"
|
||||
icon="play-circle"
|
||||
/>
|
||||
|
|
@ -80,24 +80,24 @@
|
|||
class="attachment-buttons"
|
||||
>
|
||||
<button
|
||||
v-if="attachment.type === 'flash' && flashLoaded"
|
||||
class="button-default attachment-button -transparent"
|
||||
v-if="type === 'flash' && flashLoaded"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.attachment_stop_flash')"
|
||||
@click.prevent="stopFlash"
|
||||
>
|
||||
<FAIcon icon="stop" />
|
||||
</button>
|
||||
<button
|
||||
v-if="attachment.description && size !== 'small' && !edit && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
>
|
||||
<FAIcon icon="align-right" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!useModal && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
v-if="!useModal && type !== 'unknown'"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.show_attachment_in_modal')"
|
||||
@click.prevent="openModalForce"
|
||||
>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="nsfw && hideNsfwLocal"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.hide_attachment')"
|
||||
@click.prevent="toggleHidden"
|
||||
>
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftUp"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.move_up')"
|
||||
@click.prevent="onShiftUp"
|
||||
>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftDn"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.move_down')"
|
||||
@click.prevent="onShiftDn"
|
||||
>
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.remove_attachment')"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
</div>
|
||||
|
||||
<a
|
||||
v-if="attachment.type === 'image' && (!hidden || preloadImage)"
|
||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||
class="image-container"
|
||||
:class="{'-hidden': hidden && preloadImage }"
|
||||
:href="attachment.url"
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
</a>
|
||||
|
||||
<a
|
||||
v-if="attachment.type === 'unknown' && !hidden"
|
||||
v-if="type === 'unknown' && !hidden"
|
||||
class="placeholder-container"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
|
||||
<component
|
||||
:is="videoTag"
|
||||
v-if="attachment.type === 'video' && !hidden"
|
||||
v-if="type === 'video' && !hidden"
|
||||
class="video-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
|
|
@ -193,13 +193,13 @@
|
|||
</component>
|
||||
|
||||
<span
|
||||
v-if="attachment.type === 'audio' && !hidden"
|
||||
v-if="type === 'audio' && !hidden"
|
||||
class="audio-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
<audio
|
||||
v-if="attachment.type === 'audio'"
|
||||
v-if="type === 'audio'"
|
||||
:src="attachment.url"
|
||||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
</span>
|
||||
|
||||
<div
|
||||
v-if="attachment.type === 'html' && attachment.oembed"
|
||||
v-if="type === 'html' && attachment.oembed"
|
||||
class="oembed-container"
|
||||
@click.prevent="linkClicked"
|
||||
>
|
||||
|
|
@ -229,7 +229,7 @@
|
|||
</div>
|
||||
|
||||
<span
|
||||
v-if="attachment.type === 'flash' && !hidden"
|
||||
v-if="type === 'flash' && !hidden"
|
||||
class="flash-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
|
|
@ -238,8 +238,8 @@
|
|||
ref="flash"
|
||||
class="flash"
|
||||
:src="attachment.large_thumb_url || attachment.url"
|
||||
@player-opened="setFlashLoaded(true)"
|
||||
@player-closed="setFlashLoaded(false)"
|
||||
@playerOpened="setFlashLoaded(true)"
|
||||
@playerClosed="setFlashLoaded(false)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,27 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { h, resolveComponent } from 'vue'
|
||||
|
||||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render() {
|
||||
render () {
|
||||
return h(resolveComponent(this.authForm))
|
||||
},
|
||||
computed: {
|
||||
authForm() {
|
||||
if (this.requiredTOTP) {
|
||||
return 'MFATOTPForm'
|
||||
}
|
||||
if (this.requiredRecovery) {
|
||||
return 'MFARecoveryForm'
|
||||
}
|
||||
authForm () {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery']),
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
},
|
||||
components: {
|
||||
MFARecoveryForm,
|
||||
MFATOTPForm,
|
||||
LoginForm,
|
||||
},
|
||||
LoginForm
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthForm
|
||||
|
|
|
|||
|
|
@ -2,55 +2,51 @@ const debounceMilliseconds = 500
|
|||
|
||||
export default {
|
||||
props: {
|
||||
query: {
|
||||
// function to query results and return a promise
|
||||
query: { // function to query results and return a promise
|
||||
type: Function,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
filter: {
|
||||
// function to filter results in real time
|
||||
type: Function,
|
||||
filter: { // function to filter results in real time
|
||||
type: Function
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Search...',
|
||||
},
|
||||
default: 'Search...'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
term: '',
|
||||
timeout: null,
|
||||
results: [],
|
||||
resultsVisible: false,
|
||||
resultsVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filtered() {
|
||||
filtered () {
|
||||
return this.filter ? this.filter(this.results) : this.results
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
term(val) {
|
||||
term (val) {
|
||||
this.fetchResults(val)
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchResults(term) {
|
||||
fetchResults (term) {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.results = []
|
||||
if (term) {
|
||||
this.query(term).then((results) => {
|
||||
this.results = results
|
||||
})
|
||||
this.query(term).then((results) => { this.results = results })
|
||||
}
|
||||
}, debounceMilliseconds)
|
||||
},
|
||||
onInputClick() {
|
||||
onInputClick () {
|
||||
this.resultsVisible = true
|
||||
},
|
||||
onClickOutside() {
|
||||
onClickOutside () {
|
||||
this.resultsVisible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const AvatarList = {
|
||||
props: ['users'],
|
||||
computed: {
|
||||
slicedUsers() {
|
||||
slicedUsers () {
|
||||
return this.users ? this.users.slice(0, 15) : []
|
||||
},
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserAvatar,
|
||||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
useInstanceStore().restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarList
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
export default {
|
||||
name: 'Badge',
|
||||
selector: '.badge',
|
||||
validInnerComponents: ['Text', 'Icon'],
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon'
|
||||
],
|
||||
variants: {
|
||||
notification: '.-notification',
|
||||
notification: '.-notification'
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--badgeNotification': 'color | --cRed',
|
||||
},
|
||||
'--badgeNotification': 'color | --cRed'
|
||||
}
|
||||
},
|
||||
{
|
||||
directives: {
|
||||
background: '--cGreen',
|
||||
},
|
||||
background: '--cGreen'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'notification',
|
||||
directives: {
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
],
|
||||
background: '--cRed'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,24 @@
|
|||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: ['user'],
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
UserLink,
|
||||
UserLink
|
||||
},
|
||||
methods: {
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
useInstanceStore().restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BasicUserCard
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
flex: 1 0;
|
||||
margin: 0;
|
||||
|
||||
--emoji-size: 1em;
|
||||
--emoji-size: 14px;
|
||||
|
||||
&-collapsed-content {
|
||||
margin-left: 0.7em;
|
||||
|
|
|
|||
|
|
@ -1,50 +1,40 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
||||
const BlockCard = {
|
||||
props: ['userId'],
|
||||
data () {
|
||||
return {
|
||||
progress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship() {
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
blocked() {
|
||||
blocked () {
|
||||
return this.relationship.blocking
|
||||
},
|
||||
blockExpiryAvailable() {
|
||||
return Object.hasOwn(this.user, 'block_expires_at')
|
||||
},
|
||||
blockExpiry() {
|
||||
return this.user.block_expires_at === false
|
||||
? this.$t('user_card.block_expires_forever')
|
||||
: this.$t('user_card.block_expires_at', [
|
||||
new Date(this.user.mute_expires_at).toLocaleString(),
|
||||
])
|
||||
},
|
||||
...mapState(useInstanceCapabilitiesStore, ['blockExpiration']),
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BasicUserCard,
|
||||
UserTimedFilterModal,
|
||||
BasicUserCard
|
||||
},
|
||||
methods: {
|
||||
unblockUser() {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
unblockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
},
|
||||
blockUser() {
|
||||
if (this.blockExpiration) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
}
|
||||
},
|
||||
},
|
||||
blockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BlockCard
|
||||
|
|
|
|||
|
|
@ -1,35 +1,33 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<div class="block-card-content-container">
|
||||
<span
|
||||
v-if="blocked && blockExpiryAvailable"
|
||||
class="alert neutral"
|
||||
>
|
||||
{{ blockExpiry }}
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
v-if="blocked"
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="unblockUser"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="blockUser"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.block') }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<UserTimedFilterModal
|
||||
ref="timedBlockDialog"
|
||||
:user="user"
|
||||
:is-mute="false"
|
||||
/>
|
||||
</teleport>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisH)
|
||||
|
||||
const BookmarkFolderCard = {
|
||||
props: ['folder', 'allBookmarks'],
|
||||
computed: {
|
||||
firstLetter() {
|
||||
return this.folder ? this.folder.name[0] : null
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolderCard
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="allBookmarks"
|
||||
class="bookmark-folder-card"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'bookmarks' }"
|
||||
class="bookmark-folder-name"
|
||||
>
|
||||
<span class="icon">
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 menu-icon"
|
||||
icon="bookmark"
|
||||
/>
|
||||
</span>{{ $t('nav.all_bookmarks') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="bookmark-folder-card"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'bookmark-folder', params: { id: folder.id } }"
|
||||
class="bookmark-folder-name"
|
||||
>
|
||||
<img
|
||||
v-if="folder.emoji_url"
|
||||
class="iconEmoji iconEmoji-image"
|
||||
:src="folder.emoji_url"
|
||||
:alt="folder.emoji"
|
||||
:title="folder.emoji"
|
||||
>
|
||||
<span
|
||||
v-else-if="folder.emoji"
|
||||
class="iconEmoji"
|
||||
>
|
||||
<span>
|
||||
{{ folder.emoji }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="firstLetter"
|
||||
class="icon iconLetter fa-scale-110"
|
||||
>{{ firstLetter }}</span>{{ folder.name }}
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'bookmark-folder-edit', params: { id: folder.id } }"
|
||||
class="button-folder-edit"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./bookmark_folder_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.bookmark-folder-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a.bookmark-folder-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
.icon,
|
||||
.iconLetter,
|
||||
.iconEmoji {
|
||||
display: inline-block;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.iconLetter {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.iconEmoji {
|
||||
text-align: center;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
|
||||
> span {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
img.iconEmoji {
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.bookmark-folder-name,
|
||||
.button-folder-edit {
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
color: var(--link);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import apiService from '../../services/api/api.service'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
const BookmarkFolderEdit = {
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
nameDraft: '',
|
||||
emoji: '',
|
||||
emojiUrl: null,
|
||||
emojiDraft: '',
|
||||
emojiUrlDraft: null,
|
||||
emojiPickerExpanded: false,
|
||||
reallyDelete: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker,
|
||||
},
|
||||
created() {
|
||||
if (!this.id) return
|
||||
const credentials = this.$store.state.users.currentUser.credentials
|
||||
apiService.fetchBookmarkFolders({ credentials }).then((folders) => {
|
||||
const folder = folders.find((folder) => folder.id === this.id)
|
||||
if (!folder) return
|
||||
|
||||
this.nameDraft = this.name = folder.name
|
||||
this.emojiDraft = this.emoji = folder.emoji
|
||||
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return this.$route.params.id
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectEmoji(event) {
|
||||
this.emojiDraft = event.insertion
|
||||
this.emojiUrlDraft = event.insertionUrl
|
||||
},
|
||||
showEmojiPicker() {
|
||||
if (!this.emojiPickerExpanded) {
|
||||
this.$refs.picker.showPicker()
|
||||
}
|
||||
},
|
||||
onShowPicker() {
|
||||
this.emojiPickerExpanded = true
|
||||
},
|
||||
onClosePicker() {
|
||||
this.emojiPickerExpanded = false
|
||||
},
|
||||
updateFolder() {
|
||||
useBookmarkFoldersStore()
|
||||
.updateBookmarkFolder({
|
||||
folderId: this.id,
|
||||
name: this.nameDraft,
|
||||
emoji: this.emojiDraft,
|
||||
})
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
})
|
||||
},
|
||||
createFolder() {
|
||||
useBookmarkFoldersStore()
|
||||
.createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
})
|
||||
.catch((e) => {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'bookmark_folders.error',
|
||||
messageArgs: [e.message],
|
||||
level: 'error',
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteFolder() {
|
||||
useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id })
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolderEdit
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
<template>
|
||||
<div class="panel-default panel BookmarkFolderEdit">
|
||||
<div
|
||||
ref="header"
|
||||
class="panel-heading folder-edit-heading"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled go-back-button"
|
||||
@click="$router.back"
|
||||
>
|
||||
<FAIcon
|
||||
size="lg"
|
||||
icon="chevron-left"
|
||||
/>
|
||||
</button>
|
||||
<h1 class="title">
|
||||
<i18n-t
|
||||
v-if="id"
|
||||
keypath="bookmark_folders.editing_folder"
|
||||
scope="global"
|
||||
>
|
||||
<template #folderName>
|
||||
{{ name }}
|
||||
</template>
|
||||
</i18n-t>
|
||||
<i18n-t
|
||||
v-else
|
||||
keypath="bookmark_folders.creating_folder"
|
||||
scope="global"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="input-wrap">
|
||||
<label for="folder-edit-title">{{ $t('bookmark_folders.emoji') }}</label>
|
||||
<button
|
||||
class="input input-emoji"
|
||||
:title="$t('bookmark_folder.emoji_pick')"
|
||||
@click="showEmojiPicker"
|
||||
>
|
||||
<img
|
||||
v-if="emojiUrlDraft"
|
||||
class="iconEmoji iconEmoji-image"
|
||||
:src="emojiUrlDraft"
|
||||
:alt="emojiDraft"
|
||||
:title="emojiDraft"
|
||||
>
|
||||
<span
|
||||
v-else-if="emojiDraft"
|
||||
class="iconEmoji"
|
||||
>
|
||||
<span>
|
||||
{{ emojiDraft }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<EmojiPicker
|
||||
ref="picker"
|
||||
class="emoji-picker-panel"
|
||||
@emoji="selectEmoji"
|
||||
@show="onShowPicker"
|
||||
@close="onClosePicker"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<label for="folder-edit-title">{{ $t('bookmark_folders.name') }}</label>
|
||||
<input
|
||||
id="folder-edit-title"
|
||||
ref="name"
|
||||
v-model="nameDraft"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="spacer" />
|
||||
<button
|
||||
v-if="!id"
|
||||
class="btn button-default footer-button"
|
||||
@click="createFolder"
|
||||
>
|
||||
{{ $t('bookmark_folders.create') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!reallyDelete"
|
||||
class="btn button-default footer-button"
|
||||
@click="reallyDelete = true"
|
||||
>
|
||||
{{ $t('bookmark_folders.delete') }}
|
||||
</button>
|
||||
<template v-else>
|
||||
{{ $t('bookmark_folders.really_delete') }}
|
||||
<button
|
||||
class="btn button-default footer-button"
|
||||
@click="deleteFolder"
|
||||
>
|
||||
{{ $t('general.yes') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default footer-button"
|
||||
@click="reallyDelete = false"
|
||||
>
|
||||
{{ $t('general.no') }}
|
||||
</button>
|
||||
</template>
|
||||
<div
|
||||
v-if="id && !reallyDelete"
|
||||
>
|
||||
<button
|
||||
class="btn button-default follow-button"
|
||||
@click="updateFolder"
|
||||
>
|
||||
{{ $t('bookmark_folders.update_folder') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./bookmark_folder_edit.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.BookmarkFolderEdit {
|
||||
--panel-body-padding: 0.5em;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.folder-edit-heading {
|
||||
grid-template-columns: auto minmax(50%, 1fr);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.emoji-picker-panel {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-emoji {
|
||||
height: 2.5em;
|
||||
width: 2.5em;
|
||||
padding: 0;
|
||||
|
||||
.iconEmoji {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
height: 2.5em;
|
||||
width: 2.5em;
|
||||
|
||||
> span {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
img.iconEmoji {
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.go-back-button {
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
height: 100%;
|
||||
align-self: start;
|
||||
width: var(--__panel-heading-height-inner);
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
grid-template-columns: minmax(10%, 1fr);
|
||||
|
||||
.footer-button {
|
||||
min-width: 9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
|
||||
const BookmarkFolders = {
|
||||
data() {
|
||||
return {
|
||||
isNew: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BookmarkFolderCard,
|
||||
},
|
||||
computed: {
|
||||
bookmarkFolders() {
|
||||
return useBookmarkFoldersStore().allFolders
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancelNewFolder() {
|
||||
this.isNew = false
|
||||
},
|
||||
newFolder() {
|
||||
this.isNew = true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolders
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<template>
|
||||
<div class="Bookmark-folders panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h1 class="title">
|
||||
{{ $t('nav.bookmark_folders') }}
|
||||
</h1>
|
||||
<router-link
|
||||
:to="{ name: 'bookmark-folder-new' }"
|
||||
class="button-default btn new-folder-button"
|
||||
>
|
||||
{{ $t("bookmark_folders.new") }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<BookmarkFolderCard
|
||||
:all-bookmarks="true"
|
||||
class="list-item"
|
||||
/>
|
||||
<BookmarkFolderCard
|
||||
v-for="folder in bookmarkFolders.slice().reverse()"
|
||||
:key="folder"
|
||||
:folder="folder"
|
||||
class="list-item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./bookmark_folders.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.Bookmark-folders {
|
||||
.new-folder-button {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
|
||||
export const BookmarkFoldersMenuContent = {
|
||||
props: ['showPin'],
|
||||
components: {
|
||||
NavigationEntry,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useBookmarkFoldersStore, {
|
||||
folders: getBookmarkFolderEntries,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFoldersMenuContent
|
||||