Compare commits

..

No commits in common. "shigusegubu-themes3" and "develop" have entirely different histories.

740 changed files with 33495 additions and 84648 deletions

View file

@ -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
View file

@ -0,0 +1,2 @@
build/*.js
config/*.js

27
.eslintrc.js Normal file
View 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
}
}

2
.gitattributes vendored
View file

@ -1 +1 @@
/build/commit_hash.js export-subst /build/webpack.prod.conf.js export-subst

6
.gitignore vendored
View file

@ -4,12 +4,8 @@ dist/
npm-debug.log npm-debug.log
test/unit/coverage test/unit/coverage
test/e2e/reports test/e2e/reports
test/e2e-playwright/test-results
test/e2e-playwright/playwright-report
selenium-debug.log selenium-debug.log
.idea/ .idea/
.gitlab-ci-local/
config/local.json config/local.json
src/assets/emoji.json static/emoji.json
logs/ logs/
__screenshots__/

View file

@ -1,7 +1,7 @@
# This file is a template, and might need editing before it works on your project. # 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: # Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/ # https://hub.docker.com/r/library/node/tags/
image: node:18 image: node:16
stages: stages:
- check-changelog - check-changelog
@ -34,23 +34,12 @@ check-changelog:
- apk add git - apk add git
- sh ./tools/check-changelog - sh ./tools/check-changelog
lint-eslint: lint:
stage: lint stage: lint
script: script:
- yarn - yarn
- yarn ci-eslint - npm run lint
- npm run stylelint
lint-biome:
stage: lint
script:
- yarn
- yarn ci-biome
lint-stylelint:
stage: lint
script:
- yarn
- yarn ci-stylelint
test: test:
stage: test stage: test
@ -61,144 +50,10 @@ test:
APT_CACHE_DIR: apt-cache APT_CACHE_DIR: apt-cache
script: script:
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update - mkdir -pv $APT_CACHE_DIR && apt-get -qq update
- apt install firefox-esr -y --no-install-recommends
- firefox --version
- yarn - yarn
- yarn playwright install firefox - yarn unit
- 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
build: build:
stage: build stage: build
@ -207,7 +62,7 @@ build:
- himem - himem
script: script:
- yarn - yarn
- yarn build - npm run build
artifacts: artifacts:
paths: paths:
- dist/ - dist/

View file

@ -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)

View file

@ -1 +1 @@
18.20.8 16.18.1

View file

@ -1,5 +1,6 @@
{ {
"extends": [ "extends": [
"stylelint-rscss/config",
"stylelint-config-standard", "stylelint-config-standard",
"stylelint-config-recommended-scss", "stylelint-config-recommended-scss",
"stylelint-config-html", "stylelint-config-html",
@ -7,13 +8,20 @@
], ],
"rules": { "rules": {
"declaration-no-important": true, "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, "selector-class-pattern": null,
"import-notation": null, "import-notation": null,
"custom-property-pattern": null, "custom-property-pattern": null,
"keyframes-name-pattern": null, "keyframes-name-pattern": null,
"scss/operator-no-newline-after": 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": [ "declaration-block-no-redundant-longhand-properties": [
true, true,
{ {

View file

@ -3,135 +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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 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 ## 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. 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.

View file

@ -1,145 +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/**"],
":BLANK_LINE:",
"@fortawesome/fontawesome-svg-core",
"@fortawesome/*"
]
}
}
}
}
}
}

43
build/build.js Normal file
View 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
View 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)
}
}

View file

@ -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)
}
}

View file

@ -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'
}
}
}

View file

@ -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
View 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
View 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)
})

View file

@ -1,69 +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) {
console.error(e)
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

View file

@ -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

View file

@ -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
}, {})
}

View file

@ -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
},
}
}

View file

@ -1,10 +1,12 @@
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with {
type: 'json',
}
import fs from 'fs'
Object.keys(emojis).map((k) => { module.exports = {
emojis[k].map((e) => { 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.unicode_version
delete e.emoji_version delete e.emoji_version
delete e.skin_tone_support_unicode_version delete e.skin_tone_support_unicode_version
@ -12,11 +14,14 @@ Object.keys(emojis).map((k) => {
}) })
const res = {} const res = {}
Object.keys(emojis).map((k) => { Object.keys(emojis)
.map(k => {
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
res[groupId] = emojis[k] res[groupId] = emojis[k]
}) })
console.info('Updating emojis...') console.info('Updating emojis...')
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res)) fs.writeFileSync('static/emoji.json', JSON.stringify(res))
console.info('Done.') console.info('Done.')
}
}

63
build/utils.js Normal file
View 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
View 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
View 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
View 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

View file

@ -1 +0,0 @@
fixed being unable to set actor type from profile page

View file

@ -0,0 +1 @@
Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name.

View file

@ -0,0 +1 @@
Support bookmark folders

View 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.

View file

@ -0,0 +1 @@
Support displaying time in absolute format

View file

@ -1 +0,0 @@
Add playwright E2E-tests with an optional docker-based backend

View file

@ -1 +0,0 @@
fix e2e

View file

@ -0,0 +1 @@
Fix whitespaces for multiple status mute reasons, display bot status reason

View file

@ -0,0 +1 @@
Inform users that Smithereen public polls are public

View file

@ -0,0 +1 @@
Simplify the OAuth client_name to 'PleromaFE'

View file

@ -0,0 +1 @@
Splash screen + loading indicator to make process of identifying initialization issues and load performance

6
config/dev.env.js Normal file
View 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
View 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
View file

@ -0,0 +1,3 @@
module.exports = {
NODE_ENV: '"production"'
}

6
config/test.env.js Normal file
View file

@ -0,0 +1,6 @@
var merge = require('webpack-merge')
var devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})

View file

@ -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"]

View file

@ -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"]

View file

@ -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"

View file

@ -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,
},
},
])

View file

@ -3,20 +3,115 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no"> <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="icon" type="image/png" href="/favicon.png">
<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 --> <!-- putting styles here to avoid having to wait for styles to load up -->
<link rel="stylesheet" id="splashscreen" href="/static/splash.css" /> <style id="splashscreen">
<link rel="stylesheet" id="custom-styles-holder" type="text/css" href="/static/empty.css" /> #splash {
--scale: 1;
width: 100vw;
height: 100vh;
display: grid;
grid-template-rows: auto;
grid-template-columns: auto;
align-content: center;
align-items: center;
justify-content: center;
justify-items: center;
flex-direction: column;
background: #0f161e;
font-family: sans-serif;
color: #b9b9ba;
position: absolute;
z-index: 9999;
font-size: calc(1vw + 1vh + 1vmin);
}
#splash-credit {
position: absolute;
font-size: 14px;
bottom: 16px;
right: 16px;
}
#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";
}
.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;
}
@media (prefers-reduced-motion) {
#throbber {
animation: none !important;
}
}
</style>
<style id="pleroma-eager-styles" type="text/css"></style>
<style id="pleroma-lazy-styles" type="text/css"></style>
<!--server-generated-meta--> <!--server-generated-meta-->
</head> </head>
<body> <body style="margin: 0; padding: 0">
<noscript>To use Pleroma, please enable JavaScript.</noscript> <noscript>To use Pleroma, please enable JavaScript.</noscript>
<div id="splash" class="initial-hidden"> <div id="splash">
<!-- we are hiding entire graphic so no point showing credit --> <!-- we are hiding entire graphic so no point showing credit -->
<div aria-hidden="true" id="splash-credit"> <div aria-hidden="true" id="splash-credit">
Art by pipivovott Art by pipivovott
@ -31,21 +126,18 @@
<div class="chunk" id="chunk-E"> <div class="chunk" id="chunk-E">
</div> </div>
</div> </div>
<img id="mascot" src="/static/pleromatan_apology_small.webp"> <img id="mascot" src="/static/pleromatan_apology.png">
</div> </div>
<div id="status" class="css-ok"> <div id="status" class="css-ok">
<!-- (。><) --> <!-- (。><) -->
<!-- it's a pseudographic, don't want screenreader read out nonsense --> <!-- it's a pseudographic, don't want screenreader read out nonsense -->
<span aria-hidden="true" class="initial-text">(。&gt;&lt;)</span> <span aria-hidden="true" class="initial-text">(。&gt;&lt;)</span>
</div> </div>
<code id="statusError"></code>
<pre id="statusStack"></pre>
</div> </div>
</div> </div>
<div id="app" class="hidden"></div> <div id="app" class="hidden"></div>
<div id="modal"></div> <div id="modal"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
<div id="popovers"></div> <div id="popovers" />
<script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

View file

@ -1,126 +1,137 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "2.10.0", "version": "2.7.1",
"description": "Pleroma frontend, the default frontend of Pleroma social network server", "description": "Pleroma frontend, the default frontend of Pleroma social network server",
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>", "author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
"private": false, "private": false,
"scripts": { "scripts": {
"dev": "node build/update-emoji.js && vite dev", "dev": "node build/dev-server.js",
"build": "node build/update-emoji.js && vite build", "build": "node build/build.js",
"unit": "node build/update-emoji.js && vitest --run", "unit": "karma start test/unit/karma.conf.js --single-run",
"unit-ci": "node build/update-emoji.js && vitest --run --browser.headless", "unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
"unit:watch": "node build/update-emoji.js && vitest", "e2e": "node test/e2e/runner.js",
"e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs", "test": "npm run unit && npm run e2e",
"e2e": "sh ./tools/e2e/run.sh", "stylelint": "npx stylelint '**/*.scss' '**/*.vue'",
"test": "yarn run unit && yarn run e2e", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"ci-biome": "yarn exec biome check", "lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
"ci-eslint": "yarn exec eslint",
"ci-stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
"lint": "yarn ci-biome; yarn ci-eslint; yarn ci-stylelint",
"lint-fix": "yarn exec eslint --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.4", "@babel/runtime": "7.21.5",
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "7.1.0", "@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-regular-svg-icons": "7.1.0", "@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "7.1.0", "@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/vue-fontawesome": "3.1.2", "@fortawesome/vue-fontawesome": "3.0.3",
"@kazvmoe-infra/pinch-zoom-element": "1.3.0", "@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.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/core": "2.0.3",
"@vuelidate/validators": "2.0.4", "@vuelidate/validators": "2.0.4",
"@web3-storage/parse-link-header": "^3.1.0",
"body-scroll-lock": "3.1.5", "body-scroll-lock": "3.1.5",
"chromatism": "3.0.0", "chromatism": "3.0.0",
"click-outside-vue3": "4.0.1", "click-outside-vue3": "4.0.1",
"cropperjs": "2.0.1", "cropperjs": "1.5.13",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"globals": "^16.0.0",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"localforage": "1.10.0", "localforage": "1.10.0",
"parse-link-header": "2.0.0", "parse-link-header": "2.0.0",
"phoenix": "1.8.1", "phoenix": "1.7.7",
"pinia": "^3.0.0", "punycode.js": "2.3.0",
"punycode.js": "2.3.1", "qrcode": "1.5.3",
"qrcode": "1.5.4",
"querystring-es3": "0.2.1", "querystring-es3": "0.2.1",
"url": "0.11.4", "url": "0.11.0",
"utf8": "3.0.0", "utf8": "3.0.0",
"uuid": "11.1.0", "vue": "3.2.45",
"vue": "3.5.22", "vue-i18n": "9.2.2",
"vue-i18n": "11", "vue-router": "4.1.6",
"vue-router": "4.6.4", "vue-template-compiler": "2.7.14",
"vue-virtual-scroller": "^2.0.0-beta.7", "vue-virtual-scroller": "^2.0.0-beta.7",
"vuex": "4.1.0" "vuex": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.5", "@babel/core": "7.21.8",
"@babel/eslint-parser": "7.28.5", "@babel/eslint-parser": "7.21.8",
"@babel/plugin-transform-runtime": "7.28.5", "@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.28.5", "@babel/preset-env": "7.21.5",
"@babel/register": "7.28.3", "@babel/register": "7.21.0",
"@biomejs/biome": "2.3.11", "@intlify/vue-i18n-loader": "5.0.1",
"@ungap/event-target": "0.2.4", "@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-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.5.0", "@vue/babel-plugin-jsx": "1.2.2",
"@vue/compiler-sfc": "3.5.22", "@vue/compiler-sfc": "3.2.45",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.2.8",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.19",
"babel-loader": "9.1.3",
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"chai": "5.3.3", "chai": "4.3.7",
"chalk": "5.6.2", "chalk": "1.1.3",
"chromedriver": "135.0.4", "chromedriver": "108.0.0",
"connect-history-api-fallback": "2.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", "custom-event-polyfill": "1.0.7",
"eslint": "9.39.2", "eslint": "8.33.0",
"eslint-config-standard": "17.1.0", "eslint-config-standard": "17.0.0",
"eslint-formatter-friendly": "7.0.0", "eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "17.23.1", "eslint-plugin-n": "15.6.1",
"eslint-plugin-promise": "7.2.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "10.6.2", "eslint-plugin-vue": "9.9.0",
"eslint-webpack-plugin": "3.2.0",
"eventsource-polyfill": "0.9.6", "eventsource-polyfill": "0.9.6",
"express": "5.1.0", "express": "4.18.2",
"function-bind": "1.1.2", "function-bind": "1.1.1",
"http-proxy-middleware": "3.0.5", "html-webpack-plugin": "5.5.1",
"iso-639-1": "3.1.5", "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", "lodash": "4.17.21",
"msw": "2.10.5", "mini-css-extract-plugin": "2.7.6",
"nightwatch": "3.12.2", "mocha": "10.2.0",
"playwright": "1.57.0", "nightwatch": "2.6.25",
"postcss": "8.5.6", "opn": "5.5.0",
"ora": "0.4.1",
"postcss": "8.4.23",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-loader": "7.0.2",
"postcss-scss": "^4.0.6", "postcss-scss": "^4.0.6",
"sass": "1.93.2", "sass": "1.60.0",
"selenium-server": "3.141.59", "sass-loader": "13.2.2",
"semver": "7.7.3", "selenium-server": "2.53.1",
"serve-static": "2.2.0", "semver": "7.3.8",
"shelljs": "0.10.0", "serviceworker-webpack5-plugin": "2.0.0",
"sinon": "20.0.0", "shelljs": "0.8.5",
"sinon-chai": "4.0.1", "sinon": "15.0.4",
"stylelint": "16.25.0", "sinon-chai": "3.7.0",
"stylelint": "14.16.1",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^16.0.0", "stylelint-config-recommended-scss": "^8.0.0",
"stylelint-config-recommended-scss": "^14.0.0", "stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-recommended-vue": "^1.6.0", "stylelint-config-standard": "29.0.0",
"stylelint-config-standard": "38.0.0", "stylelint-rscss": "0.4.0",
"vite": "^6.1.0", "stylelint-webpack-plugin": "^3.3.0",
"vite-plugin-eslint2": "^5.0.3", "vue-loader": "17.0.1",
"vite-plugin-stylelint": "^6.0.0", "vue-style-loader": "4.1.3",
"vitest": "^3.0.7", "webpack": "5.75.0",
"vue-eslint-parser": "10.2.0" "webpack-dev-middleware": "3.7.3",
"webpack-hot-middleware": "2.25.3",
"webpack-merge": "0.20.0"
}, },
"type": "module",
"engines": { "engines": {
"node": ">= 16.0.0" "node": ">= 16.0.0",
"npm": ">= 3.0.0"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View file

@ -1,5 +1,5 @@
import autoprefixer from 'autoprefixer' module.exports = {
plugins: [
export default { require('autoprefixer')
plugins: [autoprefixer], ]
} }

View file

@ -1 +0,0 @@
*.custom.*

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

@ -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;
}
}

View file

@ -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"
}

View file

@ -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;
}

View file

@ -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
}

View file

@ -1,4 +0,0 @@
{
"RedmondDX": "/static/styles/Redmond DX.iss",
"BreezyDX": "/static/styles/Breezy DX.iss"
}

View file

@ -1,4 +1,6 @@
{ {
"$schema": "https://docs.renovatebot.com/renovate-schema.json", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"] "extends": [
"config:base"
]
} }

View file

@ -1,36 +1,29 @@
import { throttle } from 'lodash'
import { defineAsyncComponent } from 'vue'
import { mapGetters } from 'vuex'
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 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 WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import { getOrCreateServiceWorker } from './services/sw/sw' import ShoutPanel from './components/shout_panel/shout_panel.vue'
import { windowHeight, windowWidth } from './services/window_utils/window_utils' import MediaModal from './components/media_modal/media_modal.vue'
import { useInterfaceStore } from './stores/interface' import SideDrawer from './components/side_drawer/side_drawer.vue'
import { useShoutStore } from './stores/shout' 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 { export default {
name: 'app', name: 'app',
components: { components: {
UserPanel, UserPanel,
NavPanel, NavPanel,
Notifications: defineAsyncComponent( Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
() => import('./components/notifications/notifications.vue'),
),
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
@ -40,126 +33,78 @@ export default {
MobilePostStatusButton, MobilePostStatusButton,
MobileNav, MobileNav,
DesktopNav, DesktopNav,
SettingsModal: defineAsyncComponent( SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
() => import('./components/settings_modal/settings_modal.vue'), UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
),
UpdateNotification: defineAsyncComponent(
() => import('./components/update_notification/update_notification.vue'),
),
UserReportingModal, UserReportingModal,
PostStatusModal, PostStatusModal,
EditStatusModal, EditStatusModal,
StatusHistoryModal, StatusHistoryModal,
GlobalNoticeList, GlobalNoticeList
}, },
data: () => ({ data: () => ({
mobileActivePanel: 'timeline', mobileActivePanel: 'timeline'
}), }),
watch: { watch: {
themeApplied() { themeApplied (value) {
this.removeSplash() this.removeSplash()
}, }
currentTheme() {
this.setThemeBodyClass()
},
layoutType() {
document.getElementById('modal').classList = ['-' + this.layoutType]
},
}, },
created () { created () {
// Load the locale from the storage // Load the locale from the storage
const val = this.$store.getters.mergedConfig.interfaceLanguage const val = this.$store.getters.mergedConfig.interfaceLanguage
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
document.getElementById('modal').classList = ['-' + this.layoutType] window.addEventListener('resize', this.updateMobileState)
// Create bound handlers
this.updateScrollState = throttle(this.scrollHandler, 200)
this.updateMobileState = throttle(this.resizeHandler, 200)
}, },
mounted () { mounted () {
window.addEventListener('resize', this.updateMobileState) if (this.$store.state.interface.themeApplied) {
this.scrollParent.addEventListener('scroll', this.updateScrollState)
if (useInterfaceStore().themeApplied) {
this.setThemeBodyClass()
this.removeSplash() this.removeSplash()
} }
getOrCreateServiceWorker()
}, },
unmounted () { unmounted () {
window.removeEventListener('resize', this.updateMobileState) window.removeEventListener('resize', this.updateMobileState)
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
}, },
computed: { computed: {
themeApplied () { themeApplied () {
return useInterfaceStore().themeApplied return this.$store.state.interface.themeApplied
},
currentTheme() {
if (useInterfaceStore().styleDataUsed) {
const styleMeta = useInterfaceStore().styleDataUsed.find(
(x) => x.component === '@meta',
)
if (styleMeta !== undefined) {
return styleMeta.directives.name.replaceAll(' ', '-').toLowerCase()
}
}
return 'stock'
},
layoutModalClass() {
return '-' + this.layoutType
}, },
classes () { classes () {
return [ return [
{ {
'-reverse': this.reverseLayout, '-reverse': this.reverseLayout,
'-no-sticky-headers': this.noSticky, '-no-sticky-headers': this.noSticky,
'-has-new-post-button': this.newPostButtonShown, '-has-new-post-button': this.newPostButtonShown
}, },
'-' + this.layoutType, '-' + this.layoutType
] ]
}, },
navClasses () { navClasses () {
const { navbarColumnStretch } = this.$store.getters.mergedConfig const { navbarColumnStretch } = this.$store.getters.mergedConfig
return [ return [
'-' + this.layoutType, '-' + this.layoutType,
...(navbarColumnStretch ? ['-column-stretch'] : []), ...(navbarColumnStretch ? ['-column-stretch'] : [])
] ]
}, },
currentUser() { currentUser () { return this.$store.state.users.currentUser },
return this.$store.state.users.currentUser userBackground () { return this.currentUser.background_image },
},
userBackground() {
return this.currentUser.background_image
},
instanceBackground () { instanceBackground () {
return this.mergedConfig.hideInstanceWallpaper return this.mergedConfig.hideInstanceWallpaper
? null ? null
: this.$store.state.instance.background : this.$store.state.instance.background
}, },
background() { background () { return this.userBackground || this.instanceBackground },
return this.userBackground || this.instanceBackground
},
bgStyle () { bgStyle () {
if (this.background) { if (this.background) {
return { return {
'--body-background-image': `url(${this.background})`, '--body-background-image': `url(${this.background})`
} }
} }
}, },
shout() { shout () { return this.$store.state.shout.joined },
return useShoutStore().joined suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
},
suggestionsEnabled() {
return this.$store.state.instance.suggestionsEnabled
},
showInstanceSpecificPanel () { showInstanceSpecificPanel () {
return ( return this.$store.state.instance.showInstanceSpecificPanel &&
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
)
}, },
isChats () { isChats () {
return this.$route.name === 'chat' || this.$route.name === 'chats' return this.$route.name === 'chat' || this.$route.name === 'chats'
@ -170,100 +115,43 @@ export default {
newPostButtonShown () { newPostButtonShown () {
if (this.isChats) return false if (this.isChats) return false
if (this.isListEdit) return false if (this.isListEdit) return false
return ( return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
this.$store.getters.mergedConfig.alwaysShowNewPostButton ||
this.layoutType === 'mobile'
)
},
showFeaturesPanel() {
return this.$store.state.instance.showFeaturesPanel
},
editingAvailable() {
return this.$store.state.instance.editingAvailable
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
shoutboxPosition () { shoutboxPosition () {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
}, },
hideShoutbox () { hideShoutbox () {
return this.$store.getters.mergedConfig.hideShoutbox return this.$store.getters.mergedConfig.hideShoutbox
}, },
layoutType() { layoutType () { return this.$store.state.interface.layoutType },
return useInterfaceStore().layoutType privateMode () { return this.$store.state.instance.private },
},
privateMode() {
return this.$store.state.instance.private
},
reverseLayout () { reverseLayout () {
const { thirdColumnMode, sidebarRight: reverseSetting } = const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
this.$store.getters.mergedConfig
if (this.layoutType !== 'wide') { if (this.layoutType !== 'wide') {
return reverseSetting return reverseSetting
} else { } else {
return thirdColumnMode === 'notifications' return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
? reverseSetting
: !reverseSetting
} }
}, },
noSticky() { noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
return this.$store.getters.mergedConfig.disableStickyHeaders showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
}, ...mapGetters(['mergedConfig'])
showScrollbars() {
return this.$store.getters.mergedConfig.showScrollbars
},
scrollParent() {
return window /* this.$refs.appContentRef */
},
...mapGetters(['mergedConfig']),
}, },
methods: { methods: {
resizeHandler() { updateMobileState () {
useInterfaceStore().setLayoutWidth(windowWidth()) this.$store.dispatch('setLayoutWidth', windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight()) this.$store.dispatch('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 () { removeSplash () {
document.querySelector('#status').textContent = this.$t( document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4))
'splash.fun_' + Math.ceil(Math.random() * 4),
)
const splashscreenRoot = document.querySelector('#splash') const splashscreenRoot = document.querySelector('#splash')
splashscreenRoot.addEventListener('transitionend', () => { splashscreenRoot.addEventListener('transitionend', () => {
splashscreenRoot.remove() splashscreenRoot.remove()
}) })
setTimeout(() => {
splashscreenRoot.remove() // forcibly remove it, should fix my plasma browser widget t. HJ
}, 600)
splashscreenRoot.classList.add('hidden') splashscreenRoot.classList.add('hidden')
document.querySelector('#app').classList.remove('hidden') document.querySelector('#app').classList.remove('hidden')
}, }
}, }
} }

View file

@ -1,9 +1,6 @@
// stylelint-disable rscss/class-format // stylelint-disable rscss/class-format
/* stylelint-disable no-descending-specificity */ /* stylelint-disable no-descending-specificity */
@use "panel"; @import "./panel";
@import '@fortawesome/fontawesome-svg-core/styles.css';
@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css';
:root { :root {
--status-margin: 0.75em; --status-margin: 0.75em;
@ -21,7 +18,7 @@
} }
html { html {
font-size: var(--textSize, 1rem); font-size: var(--textSize, 14px);
--navbar-height: var(--navbarSize, 3.5rem); --navbar-height: var(--navbarSize, 3.5rem);
--emoji-size: var(--emojiSize, 32px); --emoji-size: var(--emojiSize, 32px);
@ -33,12 +30,12 @@ body {
font-family: sans-serif; font-family: sans-serif;
font-family: var(--font); font-family: var(--font);
margin: 0; margin: 0;
padding: 0;
color: var(--text); color: var(--text);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overscroll-behavior-y: none; overscroll-behavior-y: none;
overflow: clip scroll; overflow-x: clip;
overflow-y: scroll;
&.hidden { &.hidden {
display: none; display: none;
@ -227,8 +224,9 @@ nav {
grid-template-rows: 1fr; grid-template-rows: 1fr;
box-sizing: border-box; box-sizing: border-box;
margin: 0 auto; margin: 0 auto;
place-content: flex-start center; align-content: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
min-height: 100vh; min-height: 100vh;
overflow-x: clip; overflow-x: clip;
@ -264,7 +262,8 @@ nav {
position: sticky; position: sticky;
top: var(--navbar-height); top: var(--navbar-height);
max-height: calc(100vh - 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); margin-left: calc(var(--___paddingIncrease) * -1);
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2); padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
@ -382,10 +381,6 @@ nav {
font-family: sans-serif; font-family: sans-serif;
font-family: var(--font); font-family: var(--font);
&.-transparent {
backdrop-filter: blur(0.125em) contrast(60%);
}
&::-moz-focus-inner { &::-moz-focus-inner {
border: none; border: none;
} }
@ -393,35 +388,39 @@ nav {
&:disabled { &:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
&:active {
transform: translate(1px, 1px);
}
} }
.menu-item { .menu-item,
line-height: var(--__line-height); .list-item {
display: block;
box-sizing: border-box;
border: none;
outline: none;
text-align: initial;
font-size: inherit;
font-family: inherit; font-family: inherit;
font-weight: 400; font-weight: 400;
font-size: 100%;
cursor: pointer; cursor: pointer;
color: inherit;
a, clear: both;
button:not(.button-default) { position: relative;
color: var(--text); white-space: nowrap;
font-size: 100%;
}
&.disabled {
cursor: not-allowed;
}
}
.list-item {
border-color: var(--border); border-color: var(--border);
border-style: solid; border-style: solid;
border-width: 0; border-width: 0;
border-top-width: 1px; 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, &.-active,
&:hover { &:hover {
@ -443,6 +442,20 @@ nav {
border-bottom-width: 1px; 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 { &:first-child {
border-top-right-radius: var(--roundness); border-top-right-radius: var(--roundness);
border-top-left-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 { .button-unstyled {
border: none; border: none;
outline: none; outline: none;
@ -513,12 +490,6 @@ nav {
} }
} }
label {
&.-disabled {
color: var(--textFaint);
}
}
input, input,
textarea { textarea {
border: none; border: none;
@ -535,10 +506,6 @@ textarea {
height: unset; height: unset;
} }
&::placeholder {
color: var(--textFaint)
}
--_padding: 0.5em; --_padding: 0.5em;
border: none; border: none;
@ -559,10 +526,6 @@ textarea {
&[disabled="disabled"], &[disabled="disabled"],
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
color: var(--textFaint);
/* stylelint-disable-next-line declaration-no-important */
background-color: transparent !important;
} }
&[type="range"] { &[type="range"] {
@ -588,8 +551,6 @@ textarea {
& + label::before { & + label::before {
opacity: 0.5; opacity: 0.5;
} }
background-color: var(--background);
} }
+ label::before { + label::before {
@ -689,8 +650,7 @@ option {
list-style: none; list-style: none;
display: grid; display: grid;
grid-auto-flow: row dense; grid-auto-flow: row dense;
grid-template-columns: repeat(auto-fit, minmax(20em, 1fr)); grid-template-columns: 1fr 1fr;
grid-gap: 0.5em;
li { li {
border: 1px solid var(--border); border: 1px solid var(--border);
@ -700,6 +660,11 @@ option {
} }
} }
.btn-block {
display: block;
width: 100%;
}
.btn-group { .btn-group {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
@ -711,6 +676,7 @@ option {
--_roundness-right: 0; --_roundness-right: 0;
position: relative; position: relative;
flex: 1 1 auto;
} }
> *:first-child, > *:first-child,
@ -757,15 +723,17 @@ option {
} }
&.-dot { &.-dot {
min-height: 0.6em; min-height: 8px;
max-height: 0.6em; max-height: 8px;
min-width: 0.6em; min-width: 8px;
max-width: 0.6em; max-width: 8px;
left: calc(50% + 0.5em);
top: calc(50% - 1em);
line-height: 0;
padding: 0; 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 { &.-counter {
@ -796,6 +764,12 @@ option {
color: var(--text); color: var(--text);
} }
.visibility-notice {
padding: 0.5em;
border: 1px solid var(--textFaint);
border-radius: var(--roundness);
}
.notice-dismissible { .notice-dismissible {
padding-right: 4rem; padding-right: 4rem;
position: relative; position: relative;
@ -840,7 +814,7 @@ option {
.login-hint { .login-hint {
text-align: center; text-align: center;
@media all and (width >= 801px) { @media all and (min-width: 801px) {
display: none; display: none;
} }
@ -862,7 +836,7 @@ option {
flex: 1; flex: 1;
} }
@media all and (width <= 800px) { @media all and (max-width: 800px) {
.mobile-hidden { .mobile-hidden {
display: none; display: none;
} }
@ -943,7 +917,12 @@ option {
#splash { #splash {
pointer-events: none; pointer-events: none;
// transition: opacity 0.5s; transition: opacity 2s;
opacity: 1;
&.hidden {
opacity: 0;
}
#status { #status {
&.css-ok { &.css-ok {
@ -967,7 +946,7 @@ option {
&.dead { &.dead {
animation-name: dead; animation-name: dead;
animation-duration: 0.5s; animation-duration: 2s;
animation-iteration-count: 1; animation-iteration-count: 1;
transform: rotateX(90deg) rotateY(0) rotateZ(-45deg); transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
} }
@ -1082,7 +1061,7 @@ option {
scale: 1.0063 0.9938; scale: 1.0063 0.9938;
translate: 0 -10%; translate: 0 -10%;
transform: rotateZ(var(--defaultZ)); transform: rotateZ(var(--defaultZ));
animation-timing-function: ease-in-out; animation-timing-function: ease-in-ou;
} }
90% { 90% {
@ -1101,8 +1080,3 @@ option {
} }
} }
} }
@property --shadow {
syntax: "*";
inherits: false;
}

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
v-show="themeApplied" v-show="$store.state.interface.themeApplied"
id="app-loaded" id="app-loaded"
:style="bgStyle" :style="bgStyle"
> >
@ -16,7 +16,6 @@
<Notifications v-if="currentUser" /> <Notifications v-if="currentUser" />
<div <div
id="content" id="content"
ref="appContentRef"
class="app-layout container" class="app-layout container"
:class="classes" :class="classes"
> >
@ -71,7 +70,7 @@
<PostStatusModal /> <PostStatusModal />
<EditStatusModal v-if="editingAvailable" /> <EditStatusModal v-if="editingAvailable" />
<StatusHistoryModal v-if="editingAvailable" /> <StatusHistoryModal v-if="editingAvailable" />
<SettingsModal :class="layoutModalClass" /> <SettingsModal />
<UpdateNotification /> <UpdateNotification />
<GlobalNoticeList /> <GlobalNoticeList />
</div> </div>

View file

@ -0,0 +1 @@
../../static/pleromatan_apology.png

View file

@ -0,0 +1 @@
../../static/pleromatan_apology_fox.png

View file

@ -1,39 +1,21 @@
/* global process */
import vClickOutside from 'click-outside-vue3'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import VueVirtualScroller from 'vue-virtual-scroller' import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { config } from '@fortawesome/fontawesome-svg-core' import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import {
FontAwesomeIcon,
FontAwesomeLayers,
} from '@fortawesome/vue-fontawesome'
config.autoAddCss = false
import VBodyScrollLock from 'src/directives/body_scroll_lock'
import {
instanceDefaultConfig,
staticOrApiConfigDefault,
} from 'src/modules/default_config_state.js'
import { useAnnouncementsStore } from 'src/stores/announcements'
import { useAuthFlowStore } from 'src/stores/auth_flow'
import { useI18nStore } from 'src/stores/i18n'
import { useInterfaceStore } from 'src/stores/interface'
import { useOAuthStore } from 'src/stores/oauth'
import App from '../App.vue' 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 { applyConfig } 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 routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock'
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 let staticInitialResults = null
@ -42,9 +24,7 @@ const parsedInitialResults = () => {
return null return null
} }
if (!staticInitialResults) { if (!staticInitialResults) {
staticInitialResults = JSON.parse( staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
document.getElementById('initial-results').textContent,
)
} }
return staticInitialResults return staticInitialResults
} }
@ -66,7 +46,7 @@ const preloadFetch = async (request) => {
return { return {
ok: true, ok: true,
json: () => requestData, json: () => requestData,
text: () => requestData, text: () => requestData
} }
} }
@ -78,57 +58,34 @@ const getInstanceConfig = async ({ store }) => {
const textlimit = data.max_toot_chars const textlimit = data.max_toot_chars
const vapidPublicKey = data.pleroma.vapid_public_key const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
name: 'pleromaExtensionsAvailable', store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
value: data.pleroma, store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma.metadata.birthday_required })
}) store.dispatch('setInstanceOption', { name: '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) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
name: 'vapidPublicKey',
value: vapidPublicKey,
})
} }
} else { } else {
throw res throw (res)
} }
} catch (error) { } catch (error) {
console.error('Could not load instance config, potentially fatal') console.error('Could not load instance config, potentially fatal')
console.error(error) 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 { try {
const res = await window.fetch('/api/pleroma/frontend_configurations') const res = await window.fetch('/api/pleroma/frontend_configurations')
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
return data.pleroma_fe return data.pleroma_fe
} else { } else {
throw res throw (res)
} }
} catch (error) { } catch (error) {
console.error( console.error('Could not load backend-provided frontend config, potentially fatal')
'Could not load backend-provided frontend config, potentially fatal',
)
console.error(error) console.error(error)
} }
} }
@ -139,7 +96,7 @@ const getStaticConfig = async () => {
if (res.ok) { if (res.ok) {
return res.json() return res.json()
} else { } else {
throw res throw (res)
} }
} catch (error) { } catch (error) {
console.warn('Failed to load static/config.json, continuing without it.') console.warn('Failed to load static/config.json, continuing without it.')
@ -162,15 +119,47 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
} }
const copyInstanceOption = (name) => { const copyInstanceOption = (name) => {
if (typeof config[name] !== 'undefined') {
store.dispatch('setInstanceOption', { name, value: config[name] }) store.dispatch('setInstanceOption', { name, value: config[name] })
} }
}
Object.keys(staticOrApiConfigDefault).forEach(copyInstanceOption) copyInstanceOption('theme')
Object.keys(instanceDefaultConfig).forEach(copyInstanceOption) 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 }) => { const getTOS = async ({ store }) => {
@ -180,10 +169,11 @@ const getTOS = async ({ store }) => {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { name: 'tos', value: html }) store.dispatch('setInstanceOption', { name: 'tos', value: html })
} else { } else {
throw res throw (res)
} }
} catch (e) { } catch (e) {
console.warn("Can't load TOS\n", e) console.warn("Can't load TOS")
console.warn(e)
} }
} }
@ -192,15 +182,13 @@ const getInstancePanel = async ({ store }) => {
const res = await preloadFetch('/instance/panel.html') const res = await preloadFetch('/instance/panel.html')
if (res.ok) { if (res.ok) {
const html = await res.text() const html = await res.text()
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
name: 'instanceSpecificPanelContent',
value: html,
})
} else { } else {
throw res throw (res)
} }
} catch (e) { } catch (e) {
console.warn("Can't load instance panel\n", e) console.warn("Can't load instance panel")
console.warn(e)
} }
} }
@ -209,8 +197,7 @@ const getStickers = async ({ store }) => {
const res = await window.fetch('/static/stickers.json') const res = await window.fetch('/static/stickers.json')
if (res.ok) { if (res.ok) {
const values = await res.json() const values = await res.json()
const stickers = ( const stickers = (await Promise.all(
await Promise.all(
Object.entries(values).map(async ([name, path]) => { Object.entries(values).map(async ([name, path]) => {
const resPack = await window.fetch(path + 'pack.json') const resPack = await window.fetch(path + 'pack.json')
let meta = {} let meta = {}
@ -220,209 +207,109 @@ const getStickers = async ({ store }) => {
return { return {
pack: name, pack: name,
path, path,
meta, meta
} }
}), })
) )).sort((a, b) => {
).sort((a, b) => {
return a.meta.title.localeCompare(b.meta.title) return a.meta.title.localeCompare(b.meta.title)
}) })
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers }) store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
} else { } else {
throw res throw (res)
} }
} catch (e) { } catch (e) {
console.warn("Can't load stickers\n", e) console.warn("Can't load stickers")
console.warn(e)
} }
} }
const getAppSecret = async ({ store }) => { const getAppSecret = async ({ store }) => {
const oauth = useOAuthStore() const { state, commit } = store
if (oauth.userToken) { const { oauth, instance } = state
store.commit( return getOrCreateApp({ ...oauth, instance: instance.server, commit })
'setBackendInteractor', .then((app) => getClientToken({ ...app, instance: instance.server }))
backendInteractorService(oauth.getToken), .then((token) => {
) commit('setAppToken', token.access_token)
} commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
})
} }
const resolveStaffAccounts = ({ store, accounts }) => { const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map((uri) => uri.split('/').pop()) const nicknames = accounts.map(uri => uri.split('/').pop())
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
name: 'staffAccounts',
value: nicknames,
})
} }
const getNodeInfo = async ({ store }) => { const getNodeInfo = async ({ store }) => {
try { try {
let res = await preloadFetch('/nodeinfo/2.1.json') const res = await preloadFetch('/nodeinfo/2.0.json')
if (!res.ok) res = await preloadFetch('/nodeinfo/2.0.json')
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
const metadata = data.metadata const metadata = data.metadata
const features = metadata.features const features = metadata.features
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
name: 'name', store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
value: metadata.nodeName, store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
}) store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
name: 'registrationOpen', store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
value: data.openRegistrations, store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
}) store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
name: 'mediaProxyAvailable', store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
value: features.includes('media_proxy'), store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
}) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
name: 'safeDM', store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
value: features.includes('safe_dm_mentions'), store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
})
store.dispatch('setInstanceOption', {
name: 'shoutAvailable',
value: features.includes('chat'),
})
store.dispatch('setInstanceOption', {
name: 'pleromaChatMessagesAvailable',
value: features.includes('pleroma_chat_messages'),
})
store.dispatch('setInstanceOption', {
name: 'pleromaCustomEmojiReactionsAvailable',
value:
features.includes('pleroma_custom_emoji_reactions') ||
features.includes('custom_emoji_reactions'),
})
store.dispatch('setInstanceOption', {
name: 'pleromaBookmarkFoldersAvailable',
value: features.includes('pleroma:bookmark_folders'),
})
store.dispatch('setInstanceOption', {
name: 'gopherAvailable',
value: features.includes('gopher'),
})
store.dispatch('setInstanceOption', {
name: 'pollsAvailable',
value: features.includes('polls'),
})
store.dispatch('setInstanceOption', {
name: 'editingAvailable',
value: features.includes('editing'),
})
store.dispatch('setInstanceOption', {
name: 'pollLimits',
value: metadata.pollLimits,
})
store.dispatch('setInstanceOption', {
name: 'mailerEnabled',
value: metadata.mailerEnabled,
})
store.dispatch('setInstanceOption', {
name: 'quotingAvailable',
value: features.includes('quote_posting'),
})
store.dispatch('setInstanceOption', {
name: 'groupActorAvailable',
value: features.includes('pleroma:group_actors'),
})
store.dispatch('setInstanceOption', {
name: 'blockExpiration',
value: features.includes('pleroma:block_expiration'),
})
store.dispatch('setInstanceOption', {
name: 'localBubbleInstances',
value: metadata.localBubbleInstances ?? [],
})
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
name: 'uploadlimit', store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
value: parseInt(uploadLimits.general), store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
}) store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
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,
})
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
name: 'restrictedNicknames', store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
value: metadata.restrictedNicknames,
})
store.dispatch('setInstanceOption', {
name: 'postFormats',
value: metadata.postFormats,
})
const suggestions = metadata.suggestions const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
name: 'suggestionsEnabled', store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
value: suggestions.enabled,
})
store.dispatch('setInstanceOption', {
name: 'suggestionsWeb',
value: suggestions.web,
})
const software = data.software const software = data.software
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
name: 'backendVersion', store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
value: software.version,
})
store.dispatch('setInstanceOption', {
name: 'backendRepository',
value: software.repository,
})
const priv = metadata.private const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv }) store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
name: 'frontendVersion',
value: frontendVersion,
})
const federation = metadata.federation const federation = metadata.federation
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'tagPolicyAvailable', name: 'tagPolicyAvailable',
value: value: typeof federation.mrf_policies === 'undefined'
typeof federation.mrf_policies === 'undefined'
? false ? false
: metadata.federation.mrf_policies.includes('TagPolicy'), : metadata.federation.mrf_policies.includes('TagPolicy')
}) })
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
name: 'federationPolicy',
value: federation,
})
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
name: 'federating', name: 'federating',
value: value: typeof federation.enabled === 'undefined'
typeof federation.enabled === 'undefined' ? true : federation.enabled, ? true
: federation.enabled
}) })
const accountActivationRequired = metadata.accountActivationRequired const accountActivationRequired = metadata.accountActivationRequired
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
name: 'accountActivationRequired',
value: accountActivationRequired,
})
const accounts = metadata.staffAccounts const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts }) resolveStaffAccounts({ store, accounts })
} else { } else {
throw res throw (res)
} }
} catch (e) { } catch (e) {
console.warn('Could not load nodeinfo') console.warn('Could not load nodeinfo')
@ -432,89 +319,23 @@ const getNodeInfo = async ({ store }) => {
const setConfig = async ({ store }) => { const setConfig = async ({ store }) => {
// apiConfig, staticConfig // apiConfig, staticConfig
const configInfos = await Promise.all([ const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
getBackendProvidedConfig({ store }),
getStaticConfig(),
])
const apiConfig = configInfos[0] const apiConfig = configInfos[0]
const staticConfig = configInfos[1] const staticConfig = configInfos[1]
getAppSecret({ store }) await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
await setSettings({ store, apiConfig, staticConfig })
} }
const checkOAuthToken = async ({ store }) => { const checkOAuthToken = async ({ store }) => {
const oauth = useOAuthStore() if (store.getters.getUserToken()) {
if (oauth.getUserToken) { return store.dispatch('loginUser', store.getters.getUserToken())
return store.dispatch('loginUser', oauth.getUserToken)
} }
return Promise.resolve() return Promise.resolve()
} }
const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { const afterStoreSetup = async ({ store, i18n }) => {
const app = createApp(App) store.dispatch('setLayoutWidth', windowWidth())
// Must have app use pinia before we do anything that touches the store store.dispatch('setLayoutHeight', windowHeight())
// 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())
FaviconService.initFaviconService() FaviconService.initFaviconService()
initServiceWorker(store) initServiceWorker(store)
@ -522,21 +343,15 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
window.addEventListener('focus', () => updateFocus()) window.addEventListener('focus', () => updateFocus())
const overrides = window.___pleromafe_dev_overrides || {} const overrides = window.___pleromafe_dev_overrides || {}
const server = const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
typeof overrides.target !== 'undefined'
? overrides.target
: window.location.origin
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
document.querySelector('#status').textContent = i18n.global.t('splash.settings')
await setConfig({ store }) await setConfig({ store })
document.querySelector('#status').textContent = i18n.global.t('splash.theme')
try { try {
await useInterfaceStore() await store.dispatch('setTheme').catch((e) => { console.error('Error setting theme', e) })
.applyTheme()
.catch((e) => {
console.error('Error setting theme', e)
})
} catch (e) { } catch (e) {
window.splashError(e)
return Promise.reject(e) return Promise.reject(e)
} }
@ -544,17 +359,17 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
// Now we can try getting the server settings and logging in // Now we can try getting the server settings and logging in
// Most of these are preloaded into the index.html so blocking is minimized // Most of these are preloaded into the index.html so blocking is minimized
document.querySelector('#status').textContent = i18n.global.t('splash.instance')
await Promise.all([ await Promise.all([
checkOAuthToken({ store }), checkOAuthToken({ store }),
getInstancePanel({ store }), getInstancePanel({ store }),
getNodeInfo({ store }), getNodeInfo({ store }),
getInstanceConfig({ store }), getInstanceConfig({ store })
]).catch((e) => Promise.reject(e)) ]).catch(e => Promise.reject(e))
// Start fetching things that don't need to block the UI // Start fetching things that don't need to block the UI
store.dispatch('fetchMutes') store.dispatch('fetchMutes')
store.dispatch('loadDrafts') store.dispatch('startFetchingAnnouncements')
useAnnouncementsStore().startFetchingAnnouncements()
getTOS({ store }) getTOS({ store })
getStickers({ store }) getStickers({ store })
@ -562,26 +377,19 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
history: createWebHistory(), history: createWebHistory(),
routes: routes(store), routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => { scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some((m) => m.meta.dontScroll)) { if (to.matched.some(m => m.meta.dontScroll)) {
return false return false
} }
return savedPosition || { left: 0, top: 0 } return savedPosition || { left: 0, top: 0 }
}, }
}) })
useI18nStore().setI18n(i18n) const app = createApp(App)
app.use(router) app.use(router)
app.use(store) app.use(store)
app.use(i18n) 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(vClickOutside)
app.use(VBodyScrollLock) app.use(VBodyScrollLock)
app.use(VueVirtualScroller) app.use(VueVirtualScroller)
@ -591,6 +399,7 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
// remove after vue 3.3 // remove after vue 3.3
app.config.unwrapInjectedRef = true app.config.unwrapInjectedRef = true
document.querySelector('#status').textContent = i18n.global.t('splash.almost')
app.mount('#app') app.mount('#app')
return app return app

View file

@ -1,36 +1,33 @@
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 PublicTimeline from 'components/public_timeline/public_timeline.vue'
import Registration from 'components/registration/registration.vue' import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue' import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
import Search from 'components/search/search.vue'
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
import TagTimeline from 'components/tag_timeline/tag_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 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 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 NavPanel from 'src/components/nav_panel/nav_panel.vue'
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue' import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue' import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
export default (store) => { export default (store) => {
const validateAuthenticatedRoute = (to, from, next) => { const validateAuthenticatedRoute = (to, from, next) => {
@ -45,177 +42,63 @@ export default (store) => {
{ {
name: 'root', name: 'root',
path: '/', path: '/',
redirect: () => { redirect: _to => {
return ( return (store.state.users.currentUser
(store.state.users.currentUser
? store.state.instance.redirectRootLogin ? store.state.instance.redirectRootLogin
: store.state.instance.redirectRootNoLogin) || '/main/all' : 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: '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: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { 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: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
{ {
name: 'remote-user-profile-acct', name: 'remote-user-profile-acct',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
component: RemoteUserResolver, component: RemoteUserResolver,
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute
}, },
{ {
name: 'remote-user-profile', name: 'remote-user-profile',
path: '/remote-users/:hostname/:username', path: '/remote-users/:hostname/:username',
component: RemoteUserResolver, component: RemoteUserResolver,
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: '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: 'registration', path: '/registration', component: Registration },
{ { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
name: 'password-reset', { name: 'registration-token', path: '/registration/:token', component: Registration },
path: '/password-reset', { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
component: PasswordReset, { name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
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: 'login', path: '/login', component: AuthForm },
{ { name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
name: 'shout-panel', { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
path: '/shout-panel', { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
component: ShoutPanel, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
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: 'about', path: '/about', component: About },
{ { name: 'announcements', path: '/announcements', component: AnnouncementsPage },
name: 'announcements',
path: '/announcements',
component: AnnouncementsPage,
},
{ name: 'drafts', path: '/drafts', component: Drafts },
{ name: 'user-profile', path: '/users/:name', component: UserProfile }, { name: 'user-profile', path: '/users/:name', component: UserProfile },
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile }, { name: 'legacy-user-profile', path: '/:name', component: UserProfile },
{ name: 'lists', path: '/lists', component: Lists }, { name: 'lists', path: '/lists', component: Lists },
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline }, { name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit }, { name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
{ name: 'lists-new', path: '/lists/new', 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: 'edit-navigation', { name: 'bookmark-folders', path: '/bookmark_folders', component: BookmarkFolders },
path: '/nav-edit', { name: 'bookmark-folder-new', path: '/bookmarks/new-folder', component: BookmarkFolderEdit },
component: NavPanel, { name: 'bookmark-folder', path: '/bookmarks/:id', component: BookmarkTimeline },
props: () => ({ forceExpand: true, forceEditMode: true }), { name: 'bookmark-folder-edit', path: '/bookmarks/:id/edit', component: BookmarkFolderEdit }
beforeEnter: validateAuthenticatedRoute,
},
{
name: 'bookmark-folders',
path: '/bookmark_folders',
component: BookmarkFolders,
},
{
name: 'bookmark-folder-new',
path: '/bookmarks/new-folder',
component: BookmarkFolderEdit,
},
{
name: 'bookmark-folder',
path: '/bookmarks/:id',
component: BookmarkTimeline,
},
{
name: 'bookmark-folder-edit',
path: '/bookmarks/:id/edit',
component: BookmarkFolderEdit,
},
] ]
if (store.state.instance.pleromaChatMessagesAvailable) { if (store.state.instance.pleromaChatMessagesAvailable) {
routes = routes.concat([ routes = routes.concat([
{ { name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
name: 'chat', { name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
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,
},
]) ])
} }

View file

@ -1,8 +1,8 @@
import FeaturesPanel from '../features_panel/features_panel.vue'
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue' import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue' import FeaturesPanel from '../features_panel/features_panel.vue'
import StaffPanel from '../staff_panel/staff_panel.vue'
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue' import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
import StaffPanel from '../staff_panel/staff_panel.vue'
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
const About = { const About = {
components: { components: {
@ -10,20 +10,16 @@ const About = {
FeaturesPanel, FeaturesPanel,
TermsOfServicePanel, TermsOfServicePanel,
StaffPanel, StaffPanel,
MRFTransparencyPanel, MRFTransparencyPanel
}, },
computed: { computed: {
showFeaturesPanel() { showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
return this.$store.state.instance.showFeaturesPanel
},
showInstanceSpecificPanel () { showInstanceSpecificPanel () {
return ( return this.$store.state.instance.showInstanceSpecificPanel &&
this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP && !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent this.$store.state.instance.instanceSpecificPanelContent
) }
}, }
},
} }
export default About export default About

View file

@ -1,42 +1,46 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
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 { useReportsStore } from 'src/stores/reports'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import Popover from '../popover/popover.vue'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue'
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' 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 = { const AccountActions = {
props: ['user', 'relationship'], props: [
'user', 'relationship'
],
data () { data () {
return { return {
showingConfirmBlock: false, showingConfirmBlock: false,
showingConfirmRemoveFollower: false, showingConfirmRemoveFollower: false
} }
}, },
components: { components: {
ProgressButton, ProgressButton,
Popover, Popover,
UserListMenu, UserListMenu,
ConfirmModal, ConfirmModal
UserTimedFilterModal,
}, },
methods: { methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showConfirmRemoveUserFromFollowers () { showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true this.showingConfirmRemoveFollower = true
}, },
hideConfirmRemoveUserFromFollowers () { hideConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = false this.showingConfirmRemoveFollower = false
}, },
hideConfirmBlock() {
this.showingConfirmBlock = false
},
showRepeats () { showRepeats () {
this.$store.dispatch('showReblogs', this.user.id) this.$store.dispatch('showReblogs', this.user.id)
}, },
@ -44,18 +48,14 @@ const AccountActions = {
this.$store.dispatch('hideReblogs', this.user.id) this.$store.dispatch('hideReblogs', this.user.id)
}, },
blockUser () { blockUser () {
if (this.$refs.timedBlockDialog) {
this.$refs.timedBlockDialog.optionallyPrompt()
} else {
if (!this.shouldConfirmBlock) { if (!this.shouldConfirmBlock) {
this.doBlockUser() this.doBlockUser()
} else { } else {
this.showingConfirmBlock = true this.showConfirmBlock()
}
} }
}, },
doBlockUser () { doBlockUser () {
this.$store.dispatch('blockUser', { id: this.user.id }) this.$store.dispatch('blockUser', this.user.id)
this.hideConfirmBlock() this.hideConfirmBlock()
}, },
unblockUser () { unblockUser () {
@ -73,17 +73,14 @@ const AccountActions = {
this.hideConfirmRemoveUserFromFollowers() this.hideConfirmRemoveUserFromFollowers()
}, },
reportUser () { reportUser () {
useReportsStore().openUserReportingModal({ userId: this.user.id }) this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
}, },
openChat () { openChat () {
this.$router.push({ this.$router.push({
name: 'chat', name: 'chat',
params: { params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
username: this.$store.state.users.currentUser.screen_name,
recipient_id: this.user.id,
},
}) })
}, }
}, },
computed: { computed: {
shouldConfirmBlock () { shouldConfirmBlock () {
@ -93,11 +90,9 @@ const AccountActions = {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
}, },
...mapState({ ...mapState({
blockExpirationSupported: (state) => state.instance.blockExpiration, pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
pleromaChatMessagesAvailable: (state) => })
state.instance.pleromaChatMessagesAvailable, }
}),
},
} }
export default AccountActions export default AccountActions

View file

@ -3,86 +3,67 @@
<Popover <Popover
trigger="click" trigger="click"
placement="bottom" placement="bottom"
:bound-to="{ x: 'container' }"
remove-padding remove-padding
> >
<template #content> <template #content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<template v-if="relationship.following"> <template v-if="relationship.following">
<div
v-if="relationship.showing_reblogs"
class="menu-item dropdown-item"
>
<button <button
class="main-button" v-if="relationship.showing_reblogs"
class="dropdown-item menu-item"
@click="hideRepeats" @click="hideRepeats"
> >
{{ $t('user_card.hide_repeats') }} {{ $t('user_card.hide_repeats') }}
</button> </button>
</div>
<div
v-if="!relationship.showing_reblogs"
class="menu-item dropdown-item"
>
<button <button
class="main-button" v-if="!relationship.showing_reblogs"
class="dropdown-item menu-item"
@click="showRepeats" @click="showRepeats"
> >
{{ $t('user_card.show_repeats') }} {{ $t('user_card.show_repeats') }}
</button> </button>
</div>
<div <div
role="separator" role="separator"
class="dropdown-divider" class="dropdown-divider"
/> />
</template> </template>
<UserListMenu :user="user" /> <UserListMenu :user="user" />
<div
v-if="relationship.followed_by"
class="menu-item dropdown-item"
>
<button <button
class="main-button" v-if="relationship.followed_by"
class="dropdown-item menu-item"
@click="removeUserFromFollowers" @click="removeUserFromFollowers"
> >
{{ $t('user_card.remove_follower') }} {{ $t('user_card.remove_follower') }}
</button> </button>
</div>
<div class="menu-item dropdown-item">
<button <button
v-if="relationship.blocking" v-if="relationship.blocking"
class="main-button" class="dropdown-item menu-item"
@click="unblockUser" @click="unblockUser"
> >
{{ $t('user_card.unblock') }} {{ $t('user_card.unblock') }}
</button> </button>
<button <button
v-else v-else
class="main-button" class="dropdown-item menu-item"
@click="blockUser" @click="blockUser"
> >
{{ $t('user_card.block') }} {{ $t('user_card.block') }}
</button> </button>
</div>
<div class="menu-item dropdown-item">
<button <button
class="main-button" class="dropdown-item menu-item"
@click="reportUser" @click="reportUser"
> >
{{ $t('user_card.report') }} {{ $t('user_card.report') }}
</button> </button>
</div>
<div
v-if="pleromaChatMessagesAvailable"
class="menu-item dropdown-item"
>
<button <button
class="main-button" v-if="pleromaChatMessagesAvailable"
class="dropdown-item menu-item"
@click="openChat" @click="openChat"
> >
{{ $t('user_card.message') }} {{ $t('user_card.message') }}
</button> </button>
</div> </div>
</div>
</template> </template>
<template #trigger> <template #trigger>
<button class="button-unstyled ellipsis-button"> <button class="button-unstyled ellipsis-button">
@ -95,8 +76,7 @@
</Popover> </Popover>
<teleport to="#modal"> <teleport to="#modal">
<confirm-modal <confirm-modal
v-if="showingConfirmBlock && !blockExpirationSupported" v-if="showingConfirmBlock"
ref="blockDialog"
:title="$t('user_card.block_confirm_title')" :title="$t('user_card.block_confirm_title')"
:confirm-text="$t('user_card.block_confirm_accept_button')" :confirm-text="$t('user_card.block_confirm_accept_button')"
:cancel-text="$t('user_card.block_confirm_cancel_button')" :cancel-text="$t('user_card.block_confirm_cancel_button')"
@ -106,7 +86,6 @@
<i18n-t <i18n-t
keypath="user_card.block_confirm" keypath="user_card.block_confirm"
tag="span" tag="span"
scope="global"
> >
<template #user> <template #user>
<span <span
@ -128,7 +107,6 @@
<i18n-t <i18n-t
keypath="user_card.remove_follower_confirm" keypath="user_card.remove_follower_confirm"
tag="span" tag="span"
scope="global"
> >
<template #user> <template #user>
<span <span
@ -137,12 +115,6 @@
</template> </template>
</i18n-t> </i18n-t>
</confirm-modal> </confirm-modal>
<UserTimedFilterModal
v-if="blockExpirationSupported"
ref="timedBlockDialog"
:is-mute="false"
:user="user"
/>
</teleport> </teleport>
</div> </div>
</template> </template>

View file

@ -1,51 +1,53 @@
export default { export default {
name: 'Alert', name: 'Alert',
selector: '.alert', selector: '.alert',
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'], validInnerComponents: [
'Text',
'Icon',
'Link',
'Border',
'ButtonUnstyled'
],
variants: { variants: {
normal: '.neutral', normal: '.neutral',
error: '.error', error: '.error',
warning: '.warning', warning: '.warning',
success: '.success', success: '.success'
},
editor: {
border: 1,
aspect: '3 / 1',
}, },
defaultRules: [ defaultRules: [
{ {
directives: { directives: {
background: '--text', background: '--text',
opacity: 0.5, opacity: 0.5,
blur: '9px', blur: '9px'
}, }
}, },
{ {
parent: { parent: {
component: 'Alert', component: 'Alert'
}, },
component: 'Border', component: 'Border',
directives: { directives: {
textColor: '--parent', textColor: '--parent'
}, }
}, },
{ {
variant: 'error', variant: 'error',
directives: { directives: {
background: '--cRed', background: '--cRed'
}, }
}, },
{ {
variant: 'warning', variant: 'warning',
directives: { directives: {
background: '--cOrange', background: '--cOrange'
}, }
}, },
{ {
variant: 'success', variant: 'success',
directives: { directives: {
background: '--cGreen', background: '--cGreen'
}, }
}, }
], ]
} }

View file

@ -1,14 +1,12 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { useAnnouncementsStore } from 'src/stores/announcements'
import localeService from '../../services/locale/locale.service.js'
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
import RichContent from '../rich_content/rich_content.jsx' import RichContent from '../rich_content/rich_content.jsx'
import localeService from '../../services/locale/locale.service.js'
const Announcement = { const Announcement = {
components: { components: {
AnnouncementEditor, AnnouncementEditor,
RichContent, RichContent
}, },
data () { data () {
return { return {
@ -17,25 +15,20 @@ const Announcement = {
content: '', content: '',
startsAt: undefined, startsAt: undefined,
endsAt: undefined, endsAt: undefined,
allDay: undefined, allDay: undefined
}, },
editError: '', editError: ''
} }
}, },
props: { props: {
announcement: Object, announcement: Object
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: (state) => state.users.currentUser, currentUser: state => state.users.currentUser
}), }),
canEditAnnouncement () { canEditAnnouncement () {
return ( return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
this.currentUser &&
this.currentUser.privileges.includes(
'announcements_manage_announcements',
)
)
}, },
content () { content () {
return this.announcement.content return this.announcement.content
@ -49,10 +42,7 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate( return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
time,
localeService.internalToBrowserLocale(this.$i18n.locale),
)
}, },
startsAt () { startsAt () {
const time = this.announcement.starts_at const time = this.announcement.starts_at
@ -60,10 +50,7 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate( return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
time,
localeService.internalToBrowserLocale(this.$i18n.locale),
)
}, },
endsAt () { endsAt () {
const time = this.announcement.ends_at const time = this.announcement.ends_at
@ -71,31 +58,24 @@ const Announcement = {
return return
} }
return this.formatTimeOrDate( return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
time,
localeService.internalToBrowserLocale(this.$i18n.locale),
)
}, },
inactive () { inactive () {
return this.announcement.inactive return this.announcement.inactive
}, }
}, },
methods: { methods: {
markAsRead () { markAsRead () {
if (!this.isRead) { if (!this.isRead) {
return useAnnouncementsStore().markAnnouncementAsRead( return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
this.announcement.id,
)
} }
}, },
deleteAnnouncement () { deleteAnnouncement () {
return useAnnouncementsStore().deleteAnnouncement(this.announcement.id) return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
}, },
formatTimeOrDate (time, locale) { formatTimeOrDate (time, locale) {
const d = new Date(time) const d = new Date(time)
return this.announcement.all_day return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
? d.toLocaleDateString(locale)
: d.toLocaleString(locale)
}, },
enterEditMode () { enterEditMode () {
this.editedAnnouncement.content = this.announcement.pleroma.raw_content this.editedAnnouncement.content = this.announcement.pleroma.raw_content
@ -105,15 +85,14 @@ const Announcement = {
this.editing = true this.editing = true
}, },
submitEdit () { submitEdit () {
useAnnouncementsStore() this.$store.dispatch('editAnnouncement', {
.editAnnouncement({
id: this.announcement.id, id: this.announcement.id,
...this.editedAnnouncement, ...this.editedAnnouncement
}) })
.then(() => { .then(() => {
this.editing = false this.editing = false
}) })
.catch((error) => { .catch(error => {
this.editError = error.error this.editError = error.error
}) })
}, },
@ -122,8 +101,8 @@ const Announcement = {
}, },
clearError () { clearError () {
this.editError = undefined this.editError = undefined
}, }
}, }
} }
export default Announcement export default Announcement

View file

@ -2,12 +2,12 @@ import Checkbox from '../checkbox/checkbox.vue'
const AnnouncementEditor = { const AnnouncementEditor = {
components: { components: {
Checkbox, Checkbox
}, },
props: { props: {
announcement: Object, announcement: Object,
disabled: Boolean, disabled: Boolean
}, }
} }
export default AnnouncementEditor export default AnnouncementEditor

View file

@ -34,9 +34,8 @@
id="announcement-all-day" id="announcement-all-day"
v-model="announcement.allDay" v-model="announcement.allDay"
:disabled="disabled" :disabled="disabled"
> />
{{ $t('announcements.all_day_prompt') }} <label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
</Checkbox>
</span> </span>
</div> </div>
</template> </template>
@ -56,7 +55,7 @@
.post-textarea { .post-textarea {
resize: vertical; resize: vertical;
height: 10em; height: 10em;
overflow: visible; overflow: none;
box-sizing: content-box; box-sizing: content-box;
} }
} }

View file

@ -1,13 +1,11 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { useAnnouncementsStore } from 'src/stores/announcements'
import Announcement from '../announcement/announcement.vue' import Announcement from '../announcement/announcement.vue'
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
const AnnouncementsPage = { const AnnouncementsPage = {
components: { components: {
Announcement, Announcement,
AnnouncementEditor, AnnouncementEditor
}, },
data () { data () {
return { return {
@ -15,42 +13,36 @@ const AnnouncementsPage = {
content: '', content: '',
startsAt: undefined, startsAt: undefined,
endsAt: undefined, endsAt: undefined,
allDay: false, allDay: false
}, },
posting: false, posting: false,
error: undefined, error: undefined
} }
}, },
mounted () { mounted () {
useAnnouncementsStore().fetchAnnouncements() this.$store.dispatch('fetchAnnouncements')
}, },
computed: { computed: {
...mapState({ ...mapState({
currentUser: (state) => state.users.currentUser, currentUser: state => state.users.currentUser
}), }),
announcements () { announcements () {
return useAnnouncementsStore().announcements return this.$store.state.announcements.announcements
}, },
canPostAnnouncement () { canPostAnnouncement () {
return ( return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
this.currentUser && }
this.currentUser.privileges.includes(
'announcements_manage_announcements',
)
)
},
}, },
methods: { methods: {
postAnnouncement () { postAnnouncement () {
this.posting = true this.posting = true
useAnnouncementsStore() this.$store.dispatch('postAnnouncement', this.newAnnouncement)
.postAnnouncement(this.newAnnouncement)
.then(() => { .then(() => {
this.newAnnouncement.content = '' this.newAnnouncement.content = ''
this.startsAt = undefined this.startsAt = undefined
this.endsAt = undefined this.endsAt = undefined
}) })
.catch((error) => { .catch(error => {
this.error = error.error this.error = error.error
}) })
.finally(() => { .finally(() => {
@ -59,8 +51,8 @@ const AnnouncementsPage = {
}, },
clearError () { clearError () {
this.error = undefined this.error = undefined
}, }
}, }
} }
export default AnnouncementsPage export default AnnouncementsPage

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="panel panel-default announcements-page"> <div class="panel panel-default announcements-page">
<div class="panel-heading"> <div class="panel-heading">
<h1 class="title"> <span>
{{ $t('announcements.page_header') }} {{ $t('announcements.page_header') }}
</h1> </span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<section <section

View file

@ -23,8 +23,8 @@ export default {
methods: { methods: {
retry () { retry () {
this.$emit('resetAsyncComponent') this.$emit('resetAsyncComponent')
}, }
}, }
} }
</script> </script>

View file

@ -1,25 +1,22 @@
import { mapGetters } from 'vuex' import StillImage from '../still-image/still-image.vue'
import Flash from '../flash/flash.vue'
import { useMediaViewerStore } from 'src/stores/media_viewer' import VideoAttachment from '../video_attachment/video_attachment.vue'
import nsfwImage from '../../assets/nsfw.png' import nsfwImage from '../../assets/nsfw.png'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
import Flash from '../flash/flash.vue' import { mapGetters } from 'vuex'
import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faAlignRight,
faFile, faFile,
faImage,
faMusic, faMusic,
faPencilAlt, faImage,
faPlayCircle,
faSearchPlus,
faStop,
faTimes,
faTrashAlt,
faVideo, faVideo,
faPlayCircle,
faTimes,
faStop,
faSearchPlus,
faTrashAlt,
faPencilAlt,
faAlignRight
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -33,7 +30,7 @@ library.add(
faSearchPlus, faSearchPlus,
faTrashAlt, faTrashAlt,
faPencilAlt, faPencilAlt,
faAlignRight, faAlignRight
) )
const Attachment = { const Attachment = {
@ -48,7 +45,7 @@ const Attachment = {
'remove', 'remove',
'shiftUp', 'shiftUp',
'shiftDn', 'shiftDn',
'edit', 'edit'
], ],
data () { data () {
return { return {
@ -57,19 +54,17 @@ const Attachment = {
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.getters.mergedConfig.preloadImage, preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false, loading: false,
img: img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
document.createElement('img'),
modalOpen: false, modalOpen: false,
showHidden: false, showHidden: false,
flashLoaded: false, flashLoaded: false,
showDescription: false, showDescription: false
} }
}, },
components: { components: {
Flash, Flash,
StillImage, StillImage,
VideoAttachment, VideoAttachment
}, },
computed: { computed: {
classNames () { classNames () {
@ -78,11 +73,11 @@ const Attachment = {
'-loading': this.loading, '-loading': this.loading,
'-nsfw-placeholder': this.hidden, '-nsfw-placeholder': this.hidden,
'-editable': this.edit !== undefined, '-editable': this.edit !== undefined,
'-compact': this.compact, '-compact': this.compact
}, },
'-type-' + this.type, '-type-' + this.type,
this.size && '-size-' + this.size, this.size && '-size-' + this.size,
`-${this.useContainFit ? 'contain' : 'cover'}-fit`, `-${this.useContainFit ? 'contain' : 'cover'}-fit`
] ]
}, },
usePlaceholder () { usePlaceholder () {
@ -113,7 +108,7 @@ const Attachment = {
return this.nsfw && this.hideNsfwLocal && !this.showHidden return this.nsfw && this.hideNsfwLocal && !this.showHidden
}, },
isEmpty () { isEmpty () {
return this.type === 'html' && !this.attachment.oembed return (this.type === 'html' && !this.attachment.oembed)
}, },
useModal () { useModal () {
let modalTypes = [] let modalTypes = []
@ -133,7 +128,7 @@ const Attachment = {
videoTag () { videoTag () {
return this.useModal ? 'button' : 'span' return this.useModal ? 'button' : 'span'
}, },
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig'])
}, },
watch: { watch: {
'attachment.description' (newVal) { 'attachment.description' (newVal) {
@ -141,7 +136,7 @@ const Attachment = {
}, },
localDescription (newVal) { localDescription (newVal) {
this.onEdit(newVal) this.onEdit(newVal)
}, }
}, },
methods: { methods: {
linkClicked ({ target }) { linkClicked ({ target }) {
@ -149,17 +144,17 @@ const Attachment = {
window.open(target.href, '_blank') window.open(target.href, '_blank')
} }
}, },
openModal() { openModal (event) {
if (this.useModal) { if (this.useModal) {
this.$emit('setMedia') this.$emit('setMedia')
useMediaViewerStore().setCurrentMedia(this.attachment) this.$store.dispatch('setCurrentMedia', this.attachment)
} else if (this.type === 'unknown') { } else if (this.type === 'unknown') {
window.open(this.attachment.url) window.open(this.attachment.url)
} }
}, },
openModalForce() { openModalForce (event) {
this.$emit('setMedia') this.$emit('setMedia')
useMediaViewerStore().setCurrentMedia(this.attachment) this.$store.dispatch('setCurrentMedia', this.attachment)
}, },
onEdit (event) { onEdit (event) {
this.edit && this.edit(this.attachment, event) this.edit && this.edit(this.attachment, event)
@ -184,8 +179,7 @@ const Attachment = {
}, },
toggleHidden (event) { toggleHidden (event) {
if ( if (
this.mergedConfig.useOneClickNsfw && (this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
!this.showHidden &&
(this.type !== 'video' || this.mergedConfig.playVideosInModal) (this.type !== 'video' || this.mergedConfig.playVideosInModal)
) { ) {
this.openModal(event) this.openModal(event)
@ -210,8 +204,8 @@ const Attachment = {
const width = image.naturalWidth const width = image.naturalWidth
const height = image.naturalHeight const height = image.naturalHeight
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height }) this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
}, }
}, }
} }
export default Attachment export default Attachment

View file

@ -107,9 +107,9 @@
.play-icon { .play-icon {
position: absolute; position: absolute;
font-size: 4.5em; font-size: 64px;
top: calc(50% - 2.25rem); top: calc(50% - 32px);
left: calc(50% - 2.25rem); left: calc(50% - 32px);
color: rgb(255 255 255 / 75%); color: rgb(255 255 255 / 75%);
text-shadow: 0 0 2px rgb(0 0 0 / 40%); text-shadow: 0 0 2px rgb(0 0 0 / 40%);
@ -177,8 +177,7 @@
.text { .text {
flex: 2; flex: 2;
margin: 8px; margin: 8px;
overflow-wrap: break-word; word-break: break-all;
text-wrap: pretty;
h1 { h1 {
font-size: 1rem; font-size: 1rem;

View 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
}
}
]
}

View file

@ -1,7 +1,7 @@
<template> <template>
<button <button
v-if="usePlaceholder" v-if="usePlaceholder"
class="Attachment -placeholder button-default" class="Attachment -placeholder button-unstyled"
:class="classNames" :class="classNames"
@click="openModal" @click="openModal"
> >
@ -23,7 +23,7 @@
> >
<button <button
v-if="remove" v-if="remove"
class="button-default attachment-button -transparent" class="button-unstyled attachment-button"
@click.prevent="onRemove" @click.prevent="onRemove"
> >
<FAIcon icon="trash-alt" /> <FAIcon icon="trash-alt" />
@ -81,7 +81,7 @@
> >
<button <button
v-if="type === 'flash' && flashLoaded" v-if="type === 'flash' && flashLoaded"
class="button-default attachment-button -transparent" class="button-unstyled attachment-button"
:title="$t('status.attachment_stop_flash')" :title="$t('status.attachment_stop_flash')"
@click.prevent="stopFlash" @click.prevent="stopFlash"
> >
@ -89,7 +89,7 @@
</button> </button>
<button <button
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'" v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
class="button-default attachment-button -transparent" class="button-unstyled attachment-button"
:title="$t('status.show_attachment_description')" :title="$t('status.show_attachment_description')"
@click.prevent="toggleDescription" @click.prevent="toggleDescription"
> >
@ -97,7 +97,7 @@
</button> </button>
<button <button
v-if="!useModal && type !== 'unknown'" v-if="!useModal && type !== 'unknown'"
class="button-default attachment-button -transparent" class="button-unstyled attachment-button"
:title="$t('status.show_attachment_in_modal')" :title="$t('status.show_attachment_in_modal')"
@click.prevent="openModalForce" @click.prevent="openModalForce"
> >
@ -105,7 +105,7 @@
</button> </button>
<button <button
v-if="nsfw && hideNsfwLocal" v-if="nsfw && hideNsfwLocal"
class="button-default attachment-button -transparent" class="button-unstyled attachment-button"
:title="$t('status.hide_attachment')" :title="$t('status.hide_attachment')"
@click.prevent="toggleHidden" @click.prevent="toggleHidden"
> >
@ -113,7 +113,7 @@
</button> </button>
<button <button
v-if="shiftUp" v-if="shiftUp"
class="button-default attachment-button -transparent" class="button-unstyled attachment-button"
:title="$t('status.move_up')" :title="$t('status.move_up')"
@click.prevent="onShiftUp" @click.prevent="onShiftUp"
> >
@ -121,7 +121,7 @@
</button> </button>
<button <button
v-if="shiftDn" v-if="shiftDn"
class="button-default attachment-button -transparent" class="button-unstyled attachment-button"
:title="$t('status.move_down')" :title="$t('status.move_down')"
@click.prevent="onShiftDn" @click.prevent="onShiftDn"
> >
@ -129,7 +129,7 @@
</button> </button>
<button <button
v-if="remove" v-if="remove"
class="button-default attachment-button -transparent" class="button-unstyled attachment-button"
:title="$t('status.remove_attachment')" :title="$t('status.remove_attachment')"
@click.prevent="onRemove" @click.prevent="onRemove"
> >
@ -238,8 +238,8 @@
ref="flash" ref="flash"
class="flash" class="flash"
:src="attachment.large_thumb_url || attachment.url" :src="attachment.large_thumb_url || attachment.url"
@player-opened="setFlashLoaded(true)" @playerOpened="setFlashLoaded(true)"
@player-closed="setFlashLoaded(false)" @playerClosed="setFlashLoaded(false)"
/> />
</span> </span>
</div> </div>

View file

@ -1,10 +1,8 @@
import { mapState } from 'pinia'
import { h, resolveComponent } from 'vue' import { h, resolveComponent } from 'vue'
import { useAuthFlowStore } from 'src/stores/auth_flow'
import LoginForm from '../login_form/login_form.vue' import LoginForm from '../login_form/login_form.vue'
import MFARecoveryForm from '../mfa_form/recovery_form.vue' import MFARecoveryForm from '../mfa_form/recovery_form.vue'
import MFATOTPForm from '../mfa_form/totp_form.vue' import MFATOTPForm from '../mfa_form/totp_form.vue'
import { mapGetters } from 'vuex'
const AuthForm = { const AuthForm = {
name: 'AuthForm', name: 'AuthForm',
@ -13,21 +11,17 @@ const AuthForm = {
}, },
computed: { computed: {
authForm () { authForm () {
if (this.requiredTOTP) { if (this.requiredTOTP) { return 'MFATOTPForm' }
return 'MFATOTPForm' if (this.requiredRecovery) { return 'MFARecoveryForm' }
}
if (this.requiredRecovery) {
return 'MFARecoveryForm'
}
return 'LoginForm' return 'LoginForm'
}, },
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery']), ...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
}, },
components: { components: {
MFARecoveryForm, MFARecoveryForm,
MFATOTPForm, MFATOTPForm,
LoginForm, LoginForm
}, }
} }
export default AuthForm export default AuthForm

View file

@ -2,37 +2,35 @@ const debounceMilliseconds = 500
export default { export default {
props: { props: {
query: { query: { // function to query results and return a promise
// function to query results and return a promise
type: Function, type: Function,
required: true, required: true
}, },
filter: { filter: { // function to filter results in real time
// function to filter results in real time type: Function
type: Function,
}, },
placeholder: { placeholder: {
type: String, type: String,
default: 'Search...', default: 'Search...'
}, }
}, },
data () { data () {
return { return {
term: '', term: '',
timeout: null, timeout: null,
results: [], results: [],
resultsVisible: false, resultsVisible: false
} }
}, },
computed: { computed: {
filtered () { filtered () {
return this.filter ? this.filter(this.results) : this.results return this.filter ? this.filter(this.results) : this.results
}, }
}, },
watch: { watch: {
term (val) { term (val) {
this.fetchResults(val) this.fetchResults(val)
}, }
}, },
methods: { methods: {
fetchResults (term) { fetchResults (term) {
@ -40,9 +38,7 @@ export default {
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
this.results = [] this.results = []
if (term) { if (term) {
this.query(term).then((results) => { this.query(term).then((results) => { this.results = results })
this.results = results
})
} }
}, debounceMilliseconds) }, debounceMilliseconds)
}, },
@ -51,6 +47,6 @@ export default {
}, },
onClickOutside () { onClickOutside () {
this.resultsVisible = false this.resultsVisible = false
}, }
}, }
} }

View file

@ -1,25 +1,21 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const AvatarList = { const AvatarList = {
props: ['users'], props: ['users'],
computed: { computed: {
slicedUsers () { slicedUsers () {
return this.users ? this.users.slice(0, 15) : [] return this.users ? this.users.slice(0, 15) : []
}, }
}, },
components: { components: {
UserAvatar, UserAvatar
}, },
methods: { methods: {
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink( return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
user.id, }
user.screen_name, }
this.$store.state.instance.restrictedNicknames,
)
},
},
} }
export default AvatarList export default AvatarList

View file

@ -1,27 +1,30 @@
export default { export default {
name: 'Badge', name: 'Badge',
selector: '.badge', selector: '.badge',
validInnerComponents: ['Text', 'Icon'], validInnerComponents: [
'Text',
'Icon'
],
variants: { variants: {
notification: '.-notification', notification: '.-notification'
}, },
defaultRules: [ defaultRules: [
{ {
component: 'Root', component: 'Root',
directives: { directives: {
'--badgeNotification': 'color | --cRed', '--badgeNotification': 'color | --cRed'
}, }
}, },
{ {
directives: { directives: {
background: '--cGreen', background: '--cGreen'
}, }
}, },
{ {
variant: 'notification', variant: 'notification',
directives: { directives: {
background: '--cRed', background: '--cRed'
}, }
}, }
], ]
} }

View file

@ -1,26 +1,24 @@
import RichContent from 'src/components/rich_content/rich_content.jsx' import UserPopover from '../user_popover/user_popover.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserLink from '../user_link/user_link.vue' import UserLink from '../user_link/user_link.vue'
import UserPopover from '../user_popover/user_popover.vue' import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const BasicUserCard = { const BasicUserCard = {
props: ['user'], props: [
'user'
],
components: { components: {
UserPopover, UserPopover,
UserAvatar, UserAvatar,
RichContent, RichContent,
UserLink, UserLink
}, },
methods: { methods: {
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink( return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
user.id, }
user.screen_name, }
this.$store.state.instance.restrictedNicknames,
)
},
},
} }
export default BasicUserCard export default BasicUserCard

View file

@ -48,7 +48,7 @@
flex: 1 0; flex: 1 0;
margin: 0; margin: 0;
--emoji-size: 1em; --emoji-size: 14px;
&-collapsed-content { &-collapsed-content {
margin-left: 0.7em; margin-left: 0.7em;

View file

@ -1,9 +1,12 @@
import { mapState } from 'vuex'
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const BlockCard = { const BlockCard = {
props: ['userId'], props: ['userId'],
data () {
return {
progress: false
}
},
computed: { computed: {
user () { user () {
return this.$store.getters.findUser(this.userId) return this.$store.getters.findUser(this.userId)
@ -13,36 +16,25 @@ const BlockCard = {
}, },
blocked () { blocked () {
return this.relationship.blocking 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({
blockExpirationSupported: (state) => state.instance.blockExpiration,
}),
}, },
components: { components: {
BasicUserCard, BasicUserCard
}, },
methods: { methods: {
unblockUser () { unblockUser () {
this.$store.dispatch('unblockUser', this.user.id) this.progress = true
this.$store.dispatch('unblockUser', this.user.id).then(() => {
this.progress = false
})
}, },
blockUser () { blockUser () {
if (this.blockExpirationSupported) { this.progress = true
this.$refs.timedBlockDialog.optionallyPrompt() this.$store.dispatch('blockUser', this.user.id).then(() => {
} else { this.progress = false
this.$store.dispatch('blockUser', { id: this.user.id }) })
}
} }
},
},
} }
export default BlockCard export default BlockCard

View file

@ -1,35 +1,33 @@
<template> <template>
<basic-user-card :user="user"> <basic-user-card :user="user">
<div class="block-card-content-container"> <div class="block-card-content-container">
<span
v-if="blocked && blockExpiryAvailable"
class="alert neutral"
>
{{ blockExpiry }}
</span>
{{ ' ' }}
<button <button
v-if="blocked" v-if="blocked"
class="btn button-default" class="btn button-default"
:disabled="progress"
@click="unblockUser" @click="unblockUser"
> >
<template v-if="progress">
{{ $t('user_card.unblock_progress') }}
</template>
<template v-else>
{{ $t('user_card.unblock') }} {{ $t('user_card.unblock') }}
</template>
</button> </button>
<button <button
v-else v-else
class="btn button-default" class="btn button-default"
:disabled="progress"
@click="blockUser" @click="blockUser"
> >
<template v-if="progress">
{{ $t('user_card.block_progress') }}
</template>
<template v-else>
{{ $t('user_card.block') }} {{ $t('user_card.block') }}
</template>
</button> </button>
</div> </div>
<teleport to="#modal">
<UserTimedFilterModal
ref="timedBlockDialog"
:user="user"
:is-mute="false"
/>
</teleport>
</basic-user-card> </basic-user-card>
</template> </template>

View file

@ -1,15 +1,22 @@
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons' import {
faEllipsisH
} from '@fortawesome/free-solid-svg-icons'
library.add(faEllipsisH) library.add(
faEllipsisH
)
const BookmarkFolderCard = { const BookmarkFolderCard = {
props: ['folder', 'allBookmarks'], props: [
'folder',
'allBookmarks'
],
computed: { computed: {
firstLetter () { firstLetter () {
return this.folder ? this.folder.name[0] : null return this.folder ? this.folder.name[0] : null
}, }
}, }
} }
export default BookmarkFolderCard export default BookmarkFolderCard

View file

@ -1,7 +1,5 @@
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
import { useInterfaceStore } from 'src/stores/interface'
import apiService from '../../services/api/api.service'
import EmojiPicker from '../emoji_picker/emoji_picker.vue' import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import apiService from '../../services/api/api.service'
const BookmarkFolderEdit = { const BookmarkFolderEdit = {
data () { data () {
@ -13,17 +11,18 @@ const BookmarkFolderEdit = {
emojiDraft: '', emojiDraft: '',
emojiUrlDraft: null, emojiUrlDraft: null,
emojiPickerExpanded: false, emojiPickerExpanded: false,
reallyDelete: false, reallyDelete: false
} }
}, },
components: { components: {
EmojiPicker, EmojiPicker
}, },
created () { created () {
if (!this.id) return if (!this.id) return
const credentials = this.$store.state.users.currentUser.credentials const credentials = this.$store.state.users.currentUser.credentials
apiService.fetchBookmarkFolders({ credentials }).then((folders) => { apiService.fetchBookmarkFolders({ credentials })
const folder = folders.find((folder) => folder.id === this.id) .then((folders) => {
const folder = folders.find(folder => folder.id === this.id)
if (!folder) return if (!folder) return
this.nameDraft = this.name = folder.name this.nameDraft = this.name = folder.name
@ -34,7 +33,7 @@ const BookmarkFolderEdit = {
computed: { computed: {
id () { id () {
return this.$route.params.id return this.$route.params.id
}, }
}, },
methods: { methods: {
selectEmoji (event) { selectEmoji (event) {
@ -53,35 +52,29 @@ const BookmarkFolderEdit = {
this.emojiPickerExpanded = false this.emojiPickerExpanded = false
}, },
updateFolder () { updateFolder () {
useBookmarkFoldersStore() this.$store.dispatch('setBookmarkFolder', { folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
.updateBookmarkFolder({
folderId: this.id,
name: this.nameDraft,
emoji: this.emojiDraft,
})
.then(() => { .then(() => {
this.$router.push({ name: 'bookmark-folders' }) this.$router.push({ name: 'bookmark-folders' })
}) })
}, },
createFolder () { createFolder () {
useBookmarkFoldersStore() this.$store.dispatch('createBookmarkFolder', { name: this.nameDraft, emoji: this.emojiDraft })
.createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
.then(() => { .then(() => {
this.$router.push({ name: 'bookmark-folders' }) this.$router.push({ name: 'bookmark-folders' })
}) })
.catch((e) => { .catch((e) => {
useInterfaceStore().pushGlobalNotice({ this.$store.dispatch('pushGlobalNotice', {
messageKey: 'bookmark_folders.error', messageKey: 'bookmark_folders.error',
messageArgs: [e.message], messageArgs: [e.message],
level: 'error', level: 'error'
}) })
}) })
}, },
deleteFolder () { deleteFolder () {
useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id }) this.$store.dispatch('deleteBookmarkFolder', { folderId: this.id })
this.$router.push({ name: 'bookmark-folders' }) this.$router.push({ name: 'bookmark-folders' })
}, }
}, }
} }
export default BookmarkFolderEdit export default BookmarkFolderEdit

View file

@ -13,11 +13,10 @@
icon="chevron-left" icon="chevron-left"
/> />
</button> </button>
<h1 class="title"> <div class="title">
<i18n-t <i18n-t
v-if="id" v-if="id"
keypath="bookmark_folders.editing_folder" keypath="bookmark_folders.editing_folder"
scope="global"
> >
<template #folderName> <template #folderName>
{{ name }} {{ name }}
@ -26,9 +25,8 @@
<i18n-t <i18n-t
v-else v-else
keypath="bookmark_folders.creating_folder" keypath="bookmark_folders.creating_folder"
scope="global"
/> />
</h1> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="input-wrap"> <div class="input-wrap">

View file

@ -1,19 +1,18 @@
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue' import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
const BookmarkFolders = { const BookmarkFolders = {
data () { data () {
return { return {
isNew: false, isNew: false
} }
}, },
components: { components: {
BookmarkFolderCard, BookmarkFolderCard
}, },
computed: { computed: {
bookmarkFolders () { bookmarkFolders () {
return useBookmarkFoldersStore().allFolders return this.$store.state.bookmarkFolders.allFolders
}, }
}, },
methods: { methods: {
cancelNewFolder () { cancelNewFolder () {
@ -21,8 +20,8 @@ const BookmarkFolders = {
}, },
newFolder () { newFolder () {
this.isNew = true this.isNew = true
}, }
}, }
} }
export default BookmarkFolders export default BookmarkFolders

View file

@ -1,9 +1,9 @@
<template> <template>
<div class="Bookmark-folders panel panel-default"> <div class="Bookmark-folders panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h1 class="title"> <div class="title">
{{ $t('nav.bookmark_folders') }} {{ $t('nav.bookmark_folders') }}
</h1> </div>
<router-link <router-link
:to="{ name: 'bookmark-folder-new' }" :to="{ name: 'bookmark-folder-new' }"
class="button-default btn new-folder-button" class="button-default btn new-folder-button"

View file

@ -1,19 +1,16 @@
import { mapState } from 'pinia' import { mapState } from 'vuex'
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
export const BookmarkFoldersMenuContent = { export const BookmarkFoldersMenuContent = {
props: ['showPin'],
components: { components: {
NavigationEntry, NavigationEntry
}, },
computed: { computed: {
...mapState(useBookmarkFoldersStore, { ...mapState({
folders: getBookmarkFolderEntries, folders: getBookmarkFolderEntries
}), })
}, }
} }
export default BookmarkFoldersMenuContent export default BookmarkFoldersMenuContent

View file

@ -7,12 +7,10 @@
label: 'nav.all_bookmarks', label: 'nav.all_bookmarks',
icon: 'bookmark' icon: 'bookmark'
}" }"
:show-pin="showPin"
/> />
<NavigationEntry <NavigationEntry
v-for="item in folders" v-for="item in folders"
:key="item.id" :key="item.id"
:show-pin="showPin"
:item="item" :item="item"
/> />
</ul> </ul>

View file

@ -3,13 +3,10 @@ import Timeline from '../timeline/timeline.vue'
const Bookmarks = { const Bookmarks = {
created () { created () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
timeline: 'bookmarks',
bookmarkFolderId: this.folderId || null,
})
}, },
components: { components: {
Timeline, Timeline
}, },
computed: { computed: {
folderId () { folderId () {
@ -17,22 +14,19 @@ const Bookmarks = {
}, },
timeline () { timeline () {
return this.$store.state.statuses.timelines.bookmarks return this.$store.state.statuses.timelines.bookmarks
}, }
}, },
watch: { watch: {
folderId () { folderId () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks') this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
timeline: 'bookmarks', }
bookmarkFolderId: this.folderId || null,
})
},
}, },
unmounted () { unmounted () {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks') this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
}, }
} }
export default Bookmarks export default Bookmarks

View file

@ -5,9 +5,9 @@ export default {
defaultRules: [ defaultRules: [
{ {
directives: { directives: {
textColor: '$mod(--parent 10)', textColor: '$mod(--parent, 10)',
textAuto: 'no-auto', textAuto: 'no-auto'
}, }
}, }
], ]
} }

View file

@ -1,20 +0,0 @@
import Timeline from '../timeline/timeline.vue'
const BubbleTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.bubble
},
},
created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'bubble')
},
}
export default BubbleTimeline

Some files were not shown because too many files have changed in this diff Show more