Compare commits
No commits in common. "shigusegubu-themes3" and "develop" have entirely different histories.
shigusegub
...
develop
729 changed files with 33608 additions and 84035 deletions
2
.eslintignore
Normal file
2
.eslintignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
build/*.js
|
||||
config/*.js
|
||||
27
.eslintrc.js
Normal file
27
.eslintrc.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow paren-less arrow functions
|
||||
'arrow-parens': 0,
|
||||
// allow async-await
|
||||
'generator-star-spacing': 0,
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'vue/require-prop-types': 0,
|
||||
'vue/multi-word-component-names': 0
|
||||
}
|
||||
}
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -1 +1 @@
|
|||
/build/commit_hash.js export-subst
|
||||
/build/webpack.prod.conf.js export-subst
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -7,6 +7,5 @@ test/e2e/reports
|
|||
selenium-debug.log
|
||||
.idea/
|
||||
config/local.json
|
||||
src/assets/emoji.json
|
||||
static/emoji.json
|
||||
logs/
|
||||
__screenshots__/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official framework image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/node/tags/
|
||||
image: node:18
|
||||
image: node:16
|
||||
|
||||
stages:
|
||||
- check-changelog
|
||||
|
|
@ -34,23 +34,12 @@ check-changelog:
|
|||
- apk add git
|
||||
- sh ./tools/check-changelog
|
||||
|
||||
lint-eslint:
|
||||
lint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-eslint
|
||||
|
||||
lint-biome:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-biome
|
||||
|
||||
lint-stylelint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-stylelint
|
||||
- npm run lint
|
||||
- npm run stylelint
|
||||
|
||||
test:
|
||||
stage: test
|
||||
|
|
@ -61,15 +50,10 @@ test:
|
|||
APT_CACHE_DIR: apt-cache
|
||||
script:
|
||||
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
||||
- apt install firefox-esr -y --no-install-recommends
|
||||
- firefox --version
|
||||
- yarn
|
||||
- yarn playwright install firefox
|
||||
- yarn playwright install-deps
|
||||
- yarn unit-ci
|
||||
artifacts:
|
||||
# When the test fails, upload screenshots for better context on why it fails
|
||||
paths:
|
||||
- test/**/__screenshots__
|
||||
when: on_failure
|
||||
- yarn unit
|
||||
|
||||
build:
|
||||
stage: build
|
||||
|
|
@ -78,7 +62,7 @@ build:
|
|||
- himem
|
||||
script:
|
||||
- yarn
|
||||
- yarn build
|
||||
- npm run build
|
||||
artifacts:
|
||||
paths:
|
||||
- dist/
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
### Release checklist
|
||||
* [ ] Bump version in `package.json`
|
||||
* [ ] Compile a changelog with the `tools/collect-changelog` script
|
||||
* [ ] Create an MR with an announcement to pleroma.social
|
||||
#### post-merge
|
||||
* [ ] Tag the release on the merge commit
|
||||
* [ ] Make the tag into a Gitlab Release™
|
||||
* [ ] Merge `master` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs)
|
||||
|
|
@ -1 +1 @@
|
|||
18.20.8
|
||||
16.18.1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-rscss/config",
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-recommended-scss",
|
||||
"stylelint-config-html",
|
||||
|
|
@ -7,13 +8,20 @@
|
|||
],
|
||||
"rules": {
|
||||
"declaration-no-important": true,
|
||||
"rscss/no-descendant-combinator": false,
|
||||
"rscss/class-format": [
|
||||
false,
|
||||
{
|
||||
"component": "pascal-case",
|
||||
"variant": "^-[a-z]\\w+",
|
||||
"element": "^[a-z]\\w+"
|
||||
}
|
||||
],
|
||||
"selector-class-pattern": null,
|
||||
"import-notation": null,
|
||||
"custom-property-pattern": null,
|
||||
"keyframes-name-pattern": null,
|
||||
"scss/operator-no-newline-after": null,
|
||||
"declaration-property-value-no-unknown": true,
|
||||
"scss/declaration-property-value-no-unknown": true,
|
||||
"declaration-block-no-redundant-longhand-properties": [
|
||||
true,
|
||||
{
|
||||
|
|
|
|||
129
CHANGELOG.md
129
CHANGELOG.md
|
|
@ -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/).
|
||||
|
||||
## 2.10.0
|
||||
### Changed
|
||||
- Temporary changes modal now shows actual countdown instead of fixed timeout
|
||||
- Disabled elements are more disabled now
|
||||
- Rearranged and split settings to make more sense and be less of a wall of text
|
||||
- On mobile settings now take up full width and presented in navigation style
|
||||
improved styles for settings
|
||||
|
||||
### Added
|
||||
- Most of the remaining AdminFE tabs were added into Admin Dashboard
|
||||
- It's now possible to customize PWA Manfiest from PleromaFE
|
||||
- Make every configuration option default-overridable by instance admins
|
||||
|
||||
### Fixed
|
||||
- Fixed settings not appearing if user never touched "show advanced" toggle
|
||||
- Fix display of the broken/deleted/banned users
|
||||
- Fixed incorrect emoji display in post interaction lists
|
||||
- Fixed list title not being saved when editing
|
||||
- Fixed poll notifications not being expandable
|
||||
|
||||
|
||||
## 2.9.3
|
||||
### Fixed
|
||||
- Being unable to update profile
|
||||
|
||||
## 2.9.2
|
||||
### Changed
|
||||
- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible
|
||||
- User card/profile got an overhaul
|
||||
- Profile editing overhaul
|
||||
- Visually combined subject and content fields in post form
|
||||
- Moved post form's emoji button into input field
|
||||
- Minor visual changes and fixes
|
||||
- Clicking on fav/rt/emoji notifications' contents expands/collapses it
|
||||
- Reduced time taken processing theme by half
|
||||
- Splash screen only appears if loading takes more than 2 seconds
|
||||
|
||||
### Added
|
||||
- Mutes received an update, adding support for regex, muting based on username and expiration time.
|
||||
- Mutes are now synchronized across sessions
|
||||
- Support for expiring mutes and blocks (if available)
|
||||
- Clicking on emoji shows bigger version of it alongside with its shortcode
|
||||
- Admins also are able to copy it into a local pack
|
||||
- Added support for Akkoma and IceShrimp.NET backends
|
||||
- Compatibility with stricter CSP (Akkoma backend)
|
||||
- Added a way to upload new packs from a URL or ZIP file via the Admin Dashboard
|
||||
- Unify show/hide content buttons
|
||||
- Add support for detachable scrollTop button
|
||||
- Option to left-align user bio
|
||||
- Cache assets and emojis with service worker
|
||||
- Indicate currently active V3 theme as a body element class
|
||||
- Add arithmetic blend ISS function
|
||||
|
||||
### Fixed
|
||||
- Display counter for status action buttons when they are in the menu
|
||||
- Fix bookmark button alignment in the extra actions menu
|
||||
- Instance favicons are no longer stretched
|
||||
- A lot more scalable UI fixes
|
||||
- Emoji picker now should work fine when emoji size is increased
|
||||
|
||||
## 2.8.0
|
||||
### Changed
|
||||
- BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image
|
||||
- BREAKING: static/emoji.json is replaced with a properly hashed path under static/js in the production build, meaning server admins cannot provide their own set of unicode emojis by overriding this file (custom (image-based) emojis not affected)
|
||||
- Speed up initial boot.
|
||||
- Updated our build system to support browsers:
|
||||
Safari >= 15
|
||||
Firefox >= 115
|
||||
Android > 4
|
||||
no Opera Mini support
|
||||
no IE support
|
||||
no "dead" (unmaintained) browsers support
|
||||
|
||||
This does not guarantee that browsers will or will not work.
|
||||
|
||||
- Use /api/v1/accounts/:id/follow for account subscriptions instead of the deprecated routes
|
||||
- Modal layout for mobile has new layout to make it easy to use
|
||||
- Better display of mute reason on posts
|
||||
- Simplify the OAuth client_name to 'PleromaFE'
|
||||
- Partially migrated from vuex to pinia
|
||||
- Authenticate and subscribe to streaming after connection
|
||||
- Tabs now have indentation for better visibility of which tab is currently active
|
||||
- Upgraded Vue to version 3.5
|
||||
|
||||
### Added
|
||||
- Support bookmark folders
|
||||
- Some new default color schemes
|
||||
- Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree
|
||||
- Post actions customization
|
||||
- Support displaying time in absolute format
|
||||
- Add draft management system
|
||||
- Compress most kinds of images on upload.
|
||||
- Added option to always convert images to JPEG format instead of using WebP when compressing images.
|
||||
- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
|
||||
- Inform users that Smithereen public polls are public
|
||||
- Splash screen + loading indicator to make process of identifying initialization issues and load performance
|
||||
- UI for making v3 themes and palettes, support for bundling v3 themes
|
||||
- Make UserLink wrappable
|
||||
|
||||
### Fixed
|
||||
- Fixed occasional overflows in emoji picker and made header scrollable
|
||||
- Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name.
|
||||
- Checkbox vertical alignment
|
||||
- Check for canvas extract permission when initializing favicon service
|
||||
- Fix some of the color manipulation functions
|
||||
- Fix draft saving when auto-save is off
|
||||
- Switch from class hack to normalButton attribute for emoji count popover
|
||||
- Fix emoji inconsistencies in notifications,
|
||||
- Fix some emoji not scaling with interface
|
||||
- Make sure hover style is also applied to :focus-visible
|
||||
- Improved ToS and registration
|
||||
- Fix small markup inconsistencies
|
||||
- Fixed modals buttons overflow
|
||||
- Fix whitespaces for multiple status mute reasons, display bot status reason
|
||||
- Create an OAuth app only when needed
|
||||
- Fix CSS compatibility issues in style_setter.js for older browsers like Palemoon
|
||||
- Proper sticky header for conversations on user page
|
||||
- Add text label for more actions button in post status form
|
||||
- Reply-or-quote buttons now take less space
|
||||
- Allow repeats of own posts with private scopes
|
||||
- Bookmarks visible again on mobile
|
||||
- Remove focusability on hidden popover in subject input
|
||||
- Show only month and day instead of weird "day, hour" format.
|
||||
|
||||
### Removed
|
||||
- BREAKING: drop support for browsers that do not support `<script type="module">`
|
||||
- BREAKING: css source map does not work in production (see https://github.com/vitejs/vite/issues/2830 )
|
||||
- Remove emoji annotations code for unused languages from final build
|
||||
|
||||
## 2.7.1
|
||||
Bugfix release. Added small optimizations to emoji picker that should make it a bit more responsive, however it needs rather large change to make it more performant which might come in a major release.
|
||||
|
||||
|
|
|
|||
129
biome.json
129
biome.json
|
|
@ -1,129 +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": "on" } }
|
||||
}
|
||||
}
|
||||
43
build/build.js
Normal file
43
build/build.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// https://github.com/shelljs/shelljs
|
||||
require('./check-versions')()
|
||||
require('shelljs/global')
|
||||
env.NODE_ENV = 'production'
|
||||
|
||||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var ora = require('ora')
|
||||
var webpack = require('webpack')
|
||||
var webpackConfig = require('./webpack.prod.conf')
|
||||
|
||||
console.log(
|
||||
' Tip:\n' +
|
||||
' Built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
)
|
||||
|
||||
var spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
var updateEmoji = require('./update-emoji').updateEmoji
|
||||
updateEmoji()
|
||||
|
||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||
rm('-rf', assetsPath)
|
||||
mkdir('-p', assetsPath)
|
||||
cp('-R', 'static/*', assetsPath)
|
||||
|
||||
webpack(webpackConfig, function (err, stats) {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n')
|
||||
if (stats.hasErrors()) {
|
||||
console.error('See above for errors.')
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
45
build/check-versions.js
Normal file
45
build/check-versions.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
var semver = require('semver')
|
||||
var chalk = require('chalk')
|
||||
var packageConfig = require('../package.json')
|
||||
var exec = function (cmd) {
|
||||
return require('child_process')
|
||||
.execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
var versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node
|
||||
},
|
||||
{
|
||||
name: 'npm',
|
||||
currentVersion: exec('npm --version'),
|
||||
versionRequirement: packageConfig.engines.npm
|
||||
}
|
||||
]
|
||||
|
||||
module.exports = function () {
|
||||
var warnings = []
|
||||
for (var i = 0; i < versionRequirements.length; i++) {
|
||||
var mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log()
|
||||
for (var i = 0; i < warnings.length; i++) {
|
||||
var warning = warnings[i]
|
||||
console.log(' ' + warning)
|
||||
}
|
||||
console.log()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import chalk from 'chalk'
|
||||
import semver from 'semver'
|
||||
|
||||
import packageConfig from '../package.json' with { type: 'json' }
|
||||
|
||||
var versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node,
|
||||
},
|
||||
]
|
||||
|
||||
export default function () {
|
||||
const warnings = []
|
||||
for (let i = 0; i < versionRequirements.length; i++) {
|
||||
const mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(
|
||||
mod.name +
|
||||
': ' +
|
||||
chalk.red(mod.currentVersion) +
|
||||
' should be ' +
|
||||
chalk.green(mod.versionRequirement),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
'\nTo use this template, you must update following to modules:\n',
|
||||
),
|
||||
)
|
||||
for (let i = 0; i < warnings.length; i++) {
|
||||
const warning = warnings[i]
|
||||
console.warn(' ' + warning)
|
||||
}
|
||||
console.warn()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import childProcess from 'child_process'
|
||||
|
||||
export const getCommitHash = () => {
|
||||
const subst = '$Format:%h$'
|
||||
if (!subst.match(/Format:/)) {
|
||||
return subst
|
||||
} else {
|
||||
try {
|
||||
return childProcess
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString()
|
||||
.trim()
|
||||
} catch (e) {
|
||||
console.error('Failed run git:', e)
|
||||
return 'UNKNOWN'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { cp } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import serveStatic from 'serve-static'
|
||||
|
||||
const getPrefix = (s) => {
|
||||
const padEnd = s.endsWith('/') ? s : s + '/'
|
||||
return padEnd.startsWith('/') ? padEnd : '/' + padEnd
|
||||
}
|
||||
|
||||
const copyPlugin = ({ inUrl, inFs }) => {
|
||||
const prefix = getPrefix(inUrl)
|
||||
const subdir = prefix.slice(1)
|
||||
let copyTarget
|
||||
const handler = serveStatic(inFs)
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'copy-plugin-serve',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
server.middlewares.use(prefix, handler)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'copy-plugin-build',
|
||||
apply: 'build',
|
||||
configResolved(config) {
|
||||
copyTarget = resolve(config.root, config.build.outDir, subdir)
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler() {
|
||||
console.info(`Copying '${inFs}' to ${copyTarget}...`)
|
||||
await cp(inFs, copyTarget, { recursive: true })
|
||||
console.info('Done.')
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export default copyPlugin
|
||||
9
build/dev-client.js
Normal file
9
build/dev-client.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* eslint-disable */
|
||||
require('eventsource-polyfill')
|
||||
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
|
||||
|
||||
hotClient.subscribe(function (event) {
|
||||
if (event.action === 'reload') {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
81
build/dev-server.js
Normal file
81
build/dev-server.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
require('./check-versions')()
|
||||
var config = require('../config')
|
||||
if (!process.env.NODE_ENV) process.env.NODE_ENV = config.dev.env
|
||||
var path = require('path')
|
||||
var express = require('express')
|
||||
var webpack = require('webpack')
|
||||
var opn = require('opn')
|
||||
var proxyMiddleware = require('http-proxy-middleware')
|
||||
var webpackConfig = process.env.NODE_ENV === 'testing'
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
|
||||
var updateEmoji = require('./update-emoji').updateEmoji
|
||||
updateEmoji()
|
||||
|
||||
// default port where dev server listens for incoming traffic
|
||||
var port = process.env.PORT || config.dev.port
|
||||
// Define HTTP proxies to your custom API backend
|
||||
// https://github.com/chimurai/http-proxy-middleware
|
||||
var proxyTable = config.dev.proxyTable
|
||||
|
||||
var app = express()
|
||||
var compiler = webpack(webpackConfig)
|
||||
|
||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
writeToDisk: true,
|
||||
stats: {
|
||||
colors: true,
|
||||
chunks: false
|
||||
}
|
||||
})
|
||||
|
||||
var hotMiddleware = require('webpack-hot-middleware')(compiler)
|
||||
|
||||
// FIXME: The statement below gives error about hooks being required in webpack 5.
|
||||
// force page reload when html-webpack-plugin template changes
|
||||
// compiler.plugin('compilation', function (compilation) {
|
||||
// compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||
// // FIXME: This supposed to reload whole page when index.html is changed,
|
||||
// // however now it reloads entire page on every breath, i suppose the order
|
||||
// // of plugins changed or something. It's a minor thing and douesn't hurt
|
||||
// // disabling it, constant reloads hurt much more
|
||||
|
||||
// // hotMiddleware.publish({ action: 'reload' })
|
||||
// // cb()
|
||||
// })
|
||||
// })
|
||||
|
||||
// proxy api requests
|
||||
Object.keys(proxyTable).forEach(function (context) {
|
||||
var options = proxyTable[context]
|
||||
if (typeof options === 'string') {
|
||||
options = { target: options }
|
||||
}
|
||||
app.use(proxyMiddleware.createProxyMiddleware(context, options))
|
||||
})
|
||||
|
||||
// handle fallback for HTML5 history API
|
||||
app.use(require('connect-history-api-fallback')())
|
||||
|
||||
// serve webpack bundle output
|
||||
app.use(devMiddleware)
|
||||
|
||||
// enable hot-reload and state-preserving
|
||||
// compilation error display
|
||||
app.use(hotMiddleware)
|
||||
|
||||
// serve pure static assets
|
||||
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
app.use(staticPath, express.static('./static'))
|
||||
|
||||
module.exports = app.listen(port, function (err) {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
return
|
||||
}
|
||||
var uri = 'http://localhost:' + port
|
||||
console.log('Listening at ' + uri + '\n')
|
||||
// opn(uri)
|
||||
})
|
||||
|
|
@ -1,68 +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
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const target = 'node_modules/msw/lib/mockServiceWorker.js'
|
||||
|
||||
const mswPlugin = () => {
|
||||
let projectRoot
|
||||
return {
|
||||
name: 'msw-plugin',
|
||||
apply: 'serve',
|
||||
configResolved(conf) {
|
||||
projectRoot = conf.root
|
||||
},
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
if (req.path === '/mockServiceWorker.js') {
|
||||
const file = await readFile(resolve(projectRoot, target))
|
||||
res.set('Content-Type', 'text/javascript')
|
||||
res.send(file)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default mswPlugin
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { langCodeToJsonName, languages } from '../src/i18n/languages.js'
|
||||
|
||||
const i18nDir = resolve(
|
||||
dirname(dirname(fileURLToPath(import.meta.url))),
|
||||
'src/i18n',
|
||||
)
|
||||
|
||||
export const i18nFiles = languages.reduce((acc, lang) => {
|
||||
const name = langCodeToJsonName(lang)
|
||||
const file = resolve(i18nDir, name + '.json')
|
||||
acc[lang] = file
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
export const generateServiceWorkerMessages = async () => {
|
||||
const msgArray = await Promise.all(
|
||||
Object.entries(i18nFiles).map(async ([lang, file]) => {
|
||||
const fileContent = await readFile(file, 'utf-8')
|
||||
const msg = {
|
||||
notifications: JSON.parse(fileContent).notifications || {},
|
||||
}
|
||||
return [lang, msg]
|
||||
}),
|
||||
)
|
||||
return msgArray.reduce((acc, [lang, msg]) => {
|
||||
acc[lang] = msg
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import * as esbuild from 'esbuild'
|
||||
import { build } from 'vite'
|
||||
import {
|
||||
generateServiceWorkerMessages,
|
||||
i18nFiles,
|
||||
} from './service_worker_messages.js'
|
||||
|
||||
const getSWMessagesAsText = async () => {
|
||||
const messages = await generateServiceWorkerMessages()
|
||||
return `export default ${JSON.stringify(messages, undefined, 2)}`
|
||||
}
|
||||
const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
|
||||
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
||||
const swEnvNameResolved = '\0' + swEnvName
|
||||
const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };`
|
||||
const getProdSwEnv = ({ assets }) =>
|
||||
`self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||
|
||||
export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => {
|
||||
const swFullSrc = resolve(projectRoot, swSrc)
|
||||
const esbuildAlias = {}
|
||||
Object.entries(alias).forEach(([source, dest]) => {
|
||||
esbuildAlias[source] = dest.startsWith('/') ? projectRoot + dest : dest
|
||||
})
|
||||
|
||||
return {
|
||||
name: 'dev-sw-plugin',
|
||||
apply: 'serve',
|
||||
configResolved() {
|
||||
/* no-op */
|
||||
},
|
||||
resolveId(id) {
|
||||
const name = id.startsWith('/') ? id.slice(1) : id
|
||||
if (name === swDest) {
|
||||
return swFullSrc
|
||||
} else if (name === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
async load(id) {
|
||||
if (id === swFullSrc) {
|
||||
return readFile(swFullSrc, 'utf-8')
|
||||
} else if (id === swEnvNameResolved) {
|
||||
return getDevSwEnv()
|
||||
}
|
||||
return null
|
||||
},
|
||||
/**
|
||||
* vite does not bundle the service worker
|
||||
* during dev, and firefox does not support ESM as service worker
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
|
||||
*/
|
||||
async transform(code, id) {
|
||||
if (id === swFullSrc && transformSW) {
|
||||
const res = await esbuild.build({
|
||||
entryPoints: [swSrc],
|
||||
bundle: true,
|
||||
write: false,
|
||||
outfile: 'sw-pleroma.js',
|
||||
alias: esbuildAlias,
|
||||
plugins: [
|
||||
{
|
||||
name: 'vite-like-root-resolve',
|
||||
setup(b) {
|
||||
b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({
|
||||
path: resolve(projectRoot, args.path.slice(1)),
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sw-messages',
|
||||
setup(b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swMessagesName + '$') },
|
||||
(args) => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-messages',
|
||||
}),
|
||||
)
|
||||
b.onLoad(
|
||||
{ filter: /.*/, namespace: 'sw-messages' },
|
||||
async () => ({
|
||||
contents: await getSWMessagesAsText(),
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sw-env',
|
||||
setup(b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swEnvName + '$') },
|
||||
(args) => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-env',
|
||||
}),
|
||||
)
|
||||
b.onLoad({ filter: /.*/, namespace: 'sw-env' }, () => ({
|
||||
contents: getDevSwEnv(),
|
||||
}))
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
const text = res.outputFiles[0].text
|
||||
return text
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Idea taken from
|
||||
// https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/plugins/build.ts
|
||||
// rollup does not support compiling to iife if we want to code-split;
|
||||
// however, we must compile the service worker to iife because of browser support.
|
||||
// Run another vite build just for the service worker targeting iife at
|
||||
// the end of the build.
|
||||
export const buildSwPlugin = ({ swSrc, swDest }) => {
|
||||
let config
|
||||
return {
|
||||
name: 'build-sw-plugin',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
configResolved(resolvedConfig) {
|
||||
config = {
|
||||
define: resolvedConfig.define,
|
||||
resolve: resolvedConfig.resolve,
|
||||
plugins: [swMessagesPlugin()],
|
||||
publicDir: false,
|
||||
build: {
|
||||
...resolvedConfig.build,
|
||||
lib: {
|
||||
entry: swSrc,
|
||||
formats: ['iife'],
|
||||
name: 'sw_pleroma',
|
||||
},
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: swDest,
|
||||
},
|
||||
},
|
||||
},
|
||||
configFile: false,
|
||||
}
|
||||
},
|
||||
generateBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler(_, bundle) {
|
||||
const assets = Object.keys(bundle)
|
||||
.filter((name) => !/\.map$/.test(name))
|
||||
.map((name) => '/' + name)
|
||||
config.plugins.push({
|
||||
name: 'build-sw-env-plugin',
|
||||
resolveId(id) {
|
||||
if (id === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
load(id) {
|
||||
if (id === swEnvNameResolved) {
|
||||
return getProdSwEnv({ assets })
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler() {
|
||||
console.info('Building service worker for production')
|
||||
await build(config)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const swMessagesName = 'virtual:pleroma-fe/service_worker_messages'
|
||||
const swMessagesNameResolved = '\0' + swMessagesName
|
||||
|
||||
export const swMessagesPlugin = () => {
|
||||
return {
|
||||
name: 'sw-messages-plugin',
|
||||
resolveId(id) {
|
||||
if (id === swMessagesName) {
|
||||
Object.values(i18nFiles).forEach((f) => {
|
||||
this.addWatchFile(f)
|
||||
})
|
||||
return swMessagesNameResolved
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
if (id === swMessagesNameResolved) {
|
||||
return await getSWMessagesAsText()
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,27 @@
|
|||
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with {
|
||||
type: 'json',
|
||||
|
||||
module.exports = {
|
||||
updateEmoji () {
|
||||
const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group')
|
||||
const fs = require('fs')
|
||||
|
||||
Object.keys(emojis)
|
||||
.map(k => {
|
||||
emojis[k].map(e => {
|
||||
delete e.unicode_version
|
||||
delete e.emoji_version
|
||||
delete e.skin_tone_support_unicode_version
|
||||
})
|
||||
})
|
||||
|
||||
const res = {}
|
||||
Object.keys(emojis)
|
||||
.map(k => {
|
||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||
res[groupId] = emojis[k]
|
||||
})
|
||||
|
||||
console.info('Updating emojis...')
|
||||
fs.writeFileSync('static/emoji.json', JSON.stringify(res))
|
||||
console.info('Done.')
|
||||
}
|
||||
}
|
||||
import fs from 'fs'
|
||||
|
||||
Object.keys(emojis).map((k) => {
|
||||
emojis[k].map((e) => {
|
||||
delete e.unicode_version
|
||||
delete e.emoji_version
|
||||
delete e.skin_tone_support_unicode_version
|
||||
})
|
||||
})
|
||||
|
||||
const res = {}
|
||||
Object.keys(emojis).map((k) => {
|
||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||
res[groupId] = emojis[k]
|
||||
})
|
||||
|
||||
console.info('Updating emojis...')
|
||||
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res))
|
||||
console.info('Done.')
|
||||
|
|
|
|||
63
build/utils.js
Normal file
63
build/utils.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var sass = require('sass')
|
||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
}
|
||||
|
||||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
function generateLoaders (loaders) {
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return [MiniCssExtractPlugin.loader].concat(loaders)
|
||||
} else {
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
|
||||
return [
|
||||
{
|
||||
test: /\.(post)?css$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader']),
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']),
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
use: generateLoaders([
|
||||
'css-loader',
|
||||
'postcss-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
indentedSyntax: true
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'sass-loader'])
|
||||
},
|
||||
{
|
||||
test: /\.styl(us)?$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Generate loaders for standalone style files (outside of .vue)
|
||||
exports.styleLoaders = function (options) {
|
||||
return exports.cssLoaders(options)
|
||||
}
|
||||
129
build/webpack.base.conf.js
Normal file
129
build/webpack.base.conf.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin')
|
||||
var CopyPlugin = require('copy-webpack-plugin');
|
||||
var { VueLoaderPlugin } = require('vue-loader')
|
||||
var ESLintPlugin = require('eslint-webpack-plugin');
|
||||
var StylelintPlugin = require('stylelint-webpack-plugin');
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
// various preprocessor loaders added to vue-loader at the end of this file
|
||||
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
|
||||
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
|
||||
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
|
||||
|
||||
var now = Date.now()
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].js'
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.jsx', '.vue'],
|
||||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
alias: {
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components'),
|
||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||
},
|
||||
fallback: {
|
||||
'querystring': require.resolve('querystring-es3'),
|
||||
'url': require.resolve('url/')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
noParse: /node_modules\/localforage\/dist\/localforage.js/,
|
||||
rules: [
|
||||
{
|
||||
enforce: 'post',
|
||||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||
type: 'javascript/auto',
|
||||
loader: '@intlify/vue-i18n-loader',
|
||||
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||
path.resolve(__dirname, '../src/i18n')
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
isCustomElement(tag) {
|
||||
if (tag === 'pinch-zoom') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
include: projectRoot,
|
||||
exclude: /node_modules\/(?!tributejs)/,
|
||||
use: 'babel-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
type: 'asset',
|
||||
generator: {
|
||||
filename: utils.assetsPath('img/[name].[hash:7][ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
type: 'asset',
|
||||
generator: {
|
||||
filename: utils.assetsPath('fonts/[name].[hash:7][ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new ServiceWorkerWebpackPlugin({
|
||||
entry: path.join(__dirname, '..', 'src/sw.js'),
|
||||
filename: 'sw-pleroma.js'
|
||||
}),
|
||||
new ESLintPlugin({
|
||||
extensions: ['js', 'vue'],
|
||||
formatter: require('eslint-formatter-friendly')
|
||||
}),
|
||||
new StylelintPlugin({}),
|
||||
new VueLoaderPlugin(),
|
||||
// This copies Ruffle's WASM to a directory so that JS side can access it
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "node_modules/@ruffle-rs/ruffle/**/*",
|
||||
to: "static/ruffle/[name][ext]"
|
||||
},
|
||||
],
|
||||
options: {
|
||||
concurrency: 100,
|
||||
},
|
||||
})
|
||||
]
|
||||
}
|
||||
37
build/webpack.dev.conf.js
Normal file
37
build/webpack.dev.conf.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
var config = require('../config')
|
||||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
var utils = require('./utils')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
|
||||
// add hot-reload related code to entry chunks
|
||||
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
|
||||
})
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
},
|
||||
mode: 'development',
|
||||
// eval-source-map is faster for development
|
||||
devtool: 'eval-source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
'COMMIT_HASH': JSON.stringify('DEV'),
|
||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
inject: true
|
||||
})
|
||||
]
|
||||
})
|
||||
104
build/webpack.prod.conf.js
Normal file
104
build/webpack.prod.conf.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
var path = require('path')
|
||||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var merge = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var env = process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
: config.build.env
|
||||
|
||||
let commitHash = (() => {
|
||||
const subst = "$Format:%h$";
|
||||
if(!subst.match(/Format:/)) {
|
||||
return subst;
|
||||
} else {
|
||||
return require('child_process')
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString();
|
||||
}
|
||||
})();
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
mode: 'production',
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? 'source-map' : false,
|
||||
optimization: {
|
||||
minimize: true,
|
||||
splitChunks: {
|
||||
chunks: 'all'
|
||||
},
|
||||
minimizer: [
|
||||
`...`,
|
||||
new CssMinimizerPlugin()
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: utils.assetsPath('js/[name].[chunkhash].js'),
|
||||
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env,
|
||||
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||
'DEV_OVERRIDES': JSON.stringify(undefined),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
}),
|
||||
// extract css into its own file
|
||||
new MiniCssExtractPlugin({
|
||||
filename: utils.assetsPath('css/[name].[contenthash].css')
|
||||
}),
|
||||
// generate dist index.html with correct asset hash for caching.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: process.env.NODE_ENV === 'testing'
|
||||
? 'index.html'
|
||||
: config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeAttributeQuotes: true,
|
||||
ignoreCustomComments: [/server-generated-meta/]
|
||||
// more options:
|
||||
// https://github.com/kangax/html-minifier#options-quick-reference
|
||||
}
|
||||
}),
|
||||
// split vendor js into its own file
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
// new webpack.optimize.SplitChunksPlugin({
|
||||
// name: ['app', 'vendor']
|
||||
// }),
|
||||
]
|
||||
})
|
||||
|
||||
if (config.build.productionGzip) {
|
||||
var CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
new CompressionWebpackPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
||||
1
changelog.d/better-shadow-control.fix
Normal file
1
changelog.d/better-shadow-control.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Updated shadow editor, hopefully fixed long-standing bugs, added ability to specify shadow's name.
|
||||
1
changelog.d/bookmark-folders.add
Normal file
1
changelog.d/bookmark-folders.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Support bookmark folders
|
||||
9
changelog.d/browsers-support.change
Normal file
9
changelog.d/browsers-support.change
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
Updated our build system to support browsers:
|
||||
Safari >= 15
|
||||
Firefox >= 115
|
||||
Android > 4
|
||||
no Opera Mini support
|
||||
no IE support
|
||||
no "dead" (unmaintained) browsers support
|
||||
|
||||
This does not guarantee that browsers will or will not work.
|
||||
1
changelog.d/date-absolute.add
Normal file
1
changelog.d/date-absolute.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Support displaying time in absolute format
|
||||
1
changelog.d/multiple-status-mute-reasons.fix
Normal file
1
changelog.d/multiple-status-mute-reasons.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix whitespaces for multiple status mute reasons, display bot status reason
|
||||
1
changelog.d/non-anonymous-polls.add
Normal file
1
changelog.d/non-anonymous-polls.add
Normal file
|
|
@ -0,0 +1 @@
|
|||
Inform users that Smithereen public polls are public
|
||||
1
changelog.d/oauth-app-name.change
Normal file
1
changelog.d/oauth-app-name.change
Normal file
|
|
@ -0,0 +1 @@
|
|||
Simplify the OAuth client_name to 'PleromaFE'
|
||||
1
changelog.d/splashscreen.add
Normal file
1
changelog.d/splashscreen.add
Normal 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
6
config/dev.env.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
var merge = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
})
|
||||
73
config/index.js
Normal file
73
config/index.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// see http://vuejs-templates.github.io/webpack for documentation.
|
||||
const path = require('path')
|
||||
let settings = {}
|
||||
try {
|
||||
settings = require('./local.json')
|
||||
if (settings.target && settings.target.endsWith('/')) {
|
||||
// replacing trailing slash since it can conflict with some apis
|
||||
// and that's how actual BE reports its url
|
||||
settings.target = settings.target.replace(/\/$/, '')
|
||||
}
|
||||
console.log('Using local dev server settings (/config/local.json):')
|
||||
console.log(JSON.stringify(settings, null, 2))
|
||||
} catch (e) {
|
||||
console.log('Local dev server settings not found (/config/local.json)')
|
||||
}
|
||||
|
||||
const target = settings.target || 'http://localhost:4000/'
|
||||
|
||||
module.exports = {
|
||||
build: {
|
||||
env: require('./prod.env'),
|
||||
index: path.resolve(__dirname, '../dist/index.html'),
|
||||
assetsRoot: path.resolve(__dirname, '../dist'),
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
productionSourceMap: true,
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
// Surge or Netlify already gzip all static assets for you.
|
||||
// Before setting to `true`, make sure to:
|
||||
// npm install --save-dev compression-webpack-plugin
|
||||
productionGzip: false,
|
||||
productionGzipExtensions: ['js', 'css']
|
||||
},
|
||||
dev: {
|
||||
env: require('./dev.env'),
|
||||
port: 8080,
|
||||
settings,
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
'/api': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/nodeinfo': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/socket': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost',
|
||||
ws: true,
|
||||
headers: {
|
||||
'Origin': target
|
||||
}
|
||||
},
|
||||
'/oauth/revoke': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
}
|
||||
},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
// (https://github.com/webpack/css-loader#sourcemaps)
|
||||
// In our experience, they generally work as expected,
|
||||
// just be aware of this issue when enabling this option.
|
||||
cssSourceMap: false
|
||||
}
|
||||
}
|
||||
3
config/prod.env.js
Normal file
3
config/prod.env.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
6
config/test.env.js
Normal file
6
config/test.env.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
var merge = require('webpack-merge')
|
||||
var devEnv = require('./dev.env')
|
||||
|
||||
module.exports = merge(devEnv, {
|
||||
NODE_ENV: '"testing"'
|
||||
})
|
||||
|
|
@ -1,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,
|
||||
},
|
||||
},
|
||||
])
|
||||
122
index.html
122
index.html
|
|
@ -3,20 +3,115 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<link rel="preload" href="/static/config.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/api/pleroma/frontend_configurations" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/nodeinfo/2.0.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/nodeinfo/2.1.json" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/api/v1/instance" as="fetch" crossorigin />
|
||||
<link rel="preload" href="/static/pleromatan_apology_fox_small.webp" as="image" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<!-- putting styles here to avoid having to wait for styles to load up -->
|
||||
<link rel="stylesheet" id="splashscreen" href="/static/splash.css" />
|
||||
<link rel="stylesheet" id="custom-styles-holder" type="text/css" href="/static/empty.css" />
|
||||
<style id="splashscreen">
|
||||
#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-->
|
||||
</head>
|
||||
<body>
|
||||
<body style="margin: 0; padding: 0">
|
||||
<noscript>To use Pleroma, please enable JavaScript.</noscript>
|
||||
<div id="splash" class="initial-hidden">
|
||||
<div id="splash">
|
||||
<!-- we are hiding entire graphic so no point showing credit -->
|
||||
<div aria-hidden="true" id="splash-credit">
|
||||
Art by pipivovott
|
||||
|
|
@ -31,21 +126,18 @@
|
|||
<div class="chunk" id="chunk-E">
|
||||
</div>
|
||||
</div>
|
||||
<img id="mascot" src="/static/pleromatan_apology_small.webp">
|
||||
<img id="mascot" src="/static/pleromatan_apology.png">
|
||||
</div>
|
||||
<div id="status" class="css-ok">
|
||||
<!-- (。>﹏<) -->
|
||||
<!-- it's a pseudographic, don't want screenreader read out nonsense -->
|
||||
<span aria-hidden="true" class="initial-text">(。>﹏<)</span>
|
||||
</div>
|
||||
<code id="statusError"></code>
|
||||
<pre id="statusStack"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app" class="hidden"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<div id="popovers"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<div id="popovers" />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
176
package.json
176
package.json
|
|
@ -1,125 +1,137 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "2.10.0",
|
||||
"version": "2.7.1",
|
||||
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "node build/update-emoji.js && vite dev",
|
||||
"build": "node build/update-emoji.js && vite build",
|
||||
"unit": "node build/update-emoji.js && vitest --run",
|
||||
"unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
|
||||
"unit:watch": "node build/update-emoji.js && vitest",
|
||||
"dev": "node build/dev-server.js",
|
||||
"build": "node build/build.js",
|
||||
"unit": "karma start test/unit/karma.conf.js --single-run",
|
||||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "yarn run unit && yarn run e2e",
|
||||
"ci-biome": "yarn exec biome check",
|
||||
"ci-eslint": "yarn exec eslint",
|
||||
"ci-stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
|
||||
"lint": "yarn ci-biome; yarn ci-eslint; yarn ci-stylelint",
|
||||
"lint-fix": "yarn exec eslint --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write"
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"stylelint": "npx stylelint '**/*.scss' '**/*.vue'",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.4",
|
||||
"@babel/runtime": "7.21.5",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.1.0",
|
||||
"@fortawesome/vue-fontawesome": "3.1.2",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2024.8.21",
|
||||
"@vuelidate/core": "2.0.3",
|
||||
"@vuelidate/validators": "2.0.4",
|
||||
"@web3-storage/parse-link-header": "^3.1.0",
|
||||
"body-scroll-lock": "3.1.5",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "2.0.1",
|
||||
"cropperjs": "1.5.13",
|
||||
"escape-html": "1.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.8.1",
|
||||
"pinia": "^3.0.0",
|
||||
"punycode.js": "2.3.1",
|
||||
"qrcode": "1.5.4",
|
||||
"phoenix": "1.7.7",
|
||||
"punycode.js": "2.3.0",
|
||||
"qrcode": "1.5.3",
|
||||
"querystring-es3": "0.2.1",
|
||||
"url": "0.11.4",
|
||||
"url": "0.11.0",
|
||||
"utf8": "3.0.0",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.5.22",
|
||||
"vue-i18n": "11",
|
||||
"vue-router": "4.6.4",
|
||||
"vue": "3.2.45",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.6",
|
||||
"vue-template-compiler": "2.7.14",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.7",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.5",
|
||||
"@babel/eslint-parser": "7.28.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@babel/register": "7.28.3",
|
||||
"@biomejs/biome": "2.3.11",
|
||||
"@babel/core": "7.21.8",
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@babel/plugin-transform-runtime": "7.21.4",
|
||||
"@babel/preset-env": "7.21.5",
|
||||
"@babel/register": "7.21.0",
|
||||
"@intlify/vue-i18n-loader": "5.0.1",
|
||||
"@ungap/event-target": "0.2.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vitest/browser": "^3.0.7",
|
||||
"@vitest/ui": "^3.0.7",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||
"@vue/babel-plugin-jsx": "1.5.0",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.21",
|
||||
"@vue/babel-plugin-jsx": "1.2.2",
|
||||
"@vue/compiler-sfc": "3.2.45",
|
||||
"@vue/test-utils": "2.2.8",
|
||||
"autoprefixer": "10.4.19",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "5.3.3",
|
||||
"chalk": "5.6.2",
|
||||
"chromedriver": "135.0.4",
|
||||
"chai": "4.3.7",
|
||||
"chalk": "1.1.3",
|
||||
"chromedriver": "108.0.0",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
"css-loader": "6.10.0",
|
||||
"css-minimizer-webpack-plugin": "4.2.2",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-standard": "17.0.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-n": "17.23.1",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-n": "15.6.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"eslint-webpack-plugin": "3.2.0",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "5.1.0",
|
||||
"function-bind": "1.1.2",
|
||||
"http-proxy-middleware": "3.0.5",
|
||||
"iso-639-1": "3.1.5",
|
||||
"express": "4.18.2",
|
||||
"function-bind": "1.1.1",
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"iso-639-1": "2.1.15",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "6.4.4",
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-firefox-launcher": "2.1.3",
|
||||
"karma-mocha": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-sinon-chai": "2.0.2",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"msw": "2.10.5",
|
||||
"nightwatch": "3.12.2",
|
||||
"playwright": "1.57.0",
|
||||
"postcss": "8.5.6",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"mocha": "10.2.0",
|
||||
"nightwatch": "2.6.25",
|
||||
"opn": "5.5.0",
|
||||
"ora": "0.4.1",
|
||||
"postcss": "8.4.23",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "7.0.2",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"sass": "1.93.2",
|
||||
"selenium-server": "3.141.59",
|
||||
"semver": "7.7.3",
|
||||
"serve-static": "2.2.0",
|
||||
"shelljs": "0.10.0",
|
||||
"sinon": "20.0.0",
|
||||
"sinon-chai": "4.0.1",
|
||||
"stylelint": "16.25.0",
|
||||
"sass": "1.60.0",
|
||||
"sass-loader": "13.2.2",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "7.3.8",
|
||||
"serviceworker-webpack5-plugin": "2.0.0",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "15.0.4",
|
||||
"sinon-chai": "3.7.0",
|
||||
"stylelint": "14.16.1",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.6.0",
|
||||
"stylelint-config-standard": "38.0.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-eslint2": "^5.0.3",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
"vitest": "^3.0.7",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
"stylelint-config-recommended-scss": "^8.0.0",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "29.0.0",
|
||||
"stylelint-rscss": "0.4.0",
|
||||
"stylelint-webpack-plugin": "^3.3.0",
|
||||
"vue-loader": "17.0.1",
|
||||
"vue-style-loader": "4.1.3",
|
||||
"webpack": "5.75.0",
|
||||
"webpack-dev-middleware": "3.7.3",
|
||||
"webpack-hot-middleware": "2.25.3",
|
||||
"webpack-merge": "0.20.0"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
"node": ">= 16.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import autoprefixer from 'autoprefixer'
|
||||
|
||||
export default {
|
||||
plugins: [autoprefixer],
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
}
|
||||
|
|
|
|||
1
public/static/.gitignore
vendored
1
public/static/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
*.custom.*
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
{
|
||||
"pleroma-dark": [
|
||||
"Pleroma Dark",
|
||||
"#121a24",
|
||||
"#182230",
|
||||
"#b9b9ba",
|
||||
"#d8a070",
|
||||
"#d31014",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#ffa500"
|
||||
],
|
||||
"pleroma-light": [
|
||||
"Pleroma Light",
|
||||
"#f2f4f6",
|
||||
"#dbe0e8",
|
||||
"#304055",
|
||||
"#f86f0f",
|
||||
"#d31014",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#ffa500"
|
||||
],
|
||||
"classic-dark": {
|
||||
"name": "Classic Dark",
|
||||
"bg": "#161c20",
|
||||
"fg": "#282e32",
|
||||
"text": "#b9b9b9",
|
||||
"link": "#baaa9c",
|
||||
"cRed": "#d31014",
|
||||
"cGreen": "#0fa00f",
|
||||
"cBlue": "#0095ff",
|
||||
"cOrange": "#ffa500"
|
||||
},
|
||||
"bird": [
|
||||
"Bird",
|
||||
"#f8fafd",
|
||||
"#e6ecf0",
|
||||
"#14171a",
|
||||
"#0084b8",
|
||||
"#e0245e",
|
||||
"#17bf63",
|
||||
"#1b95e0",
|
||||
"#fab81e"
|
||||
],
|
||||
"pleroma-amoled": [
|
||||
"Pleroma Dark AMOLED",
|
||||
"#000000",
|
||||
"#111111",
|
||||
"#b0b0b1",
|
||||
"#d8a070",
|
||||
"#aa0000",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#d59500"
|
||||
],
|
||||
"tomorrow-night": {
|
||||
"name": "Tomorrow Night",
|
||||
"bg": "#1d1f21",
|
||||
"fg": "#373b41",
|
||||
"link": "#81a2be",
|
||||
"text": "#c5c8c6",
|
||||
"cRed": "#cc6666",
|
||||
"cBlue": "#8abeb7",
|
||||
"cGreen": "#b5bd68",
|
||||
"cOrange": "#de935f"
|
||||
},
|
||||
"dracula": {
|
||||
"name": "Dracula",
|
||||
"bg": "#282A36",
|
||||
"fg": "#44475A",
|
||||
"link": "#BC92F9",
|
||||
"text": "#f8f8f2",
|
||||
"cRed": "#FF5555",
|
||||
"cBlue": "#8BE9FD",
|
||||
"cGreen": "#50FA7B",
|
||||
"cOrange": "#FFB86C"
|
||||
},
|
||||
"ir-black": [
|
||||
"Ir Black",
|
||||
"#000000",
|
||||
"#242422",
|
||||
"#b5b3aa",
|
||||
"#ff6c60",
|
||||
"#FF6C60",
|
||||
"#A8FF60",
|
||||
"#96CBFE",
|
||||
"#FFFFB6"
|
||||
],
|
||||
"monokai": [
|
||||
"Monokai",
|
||||
"#272822",
|
||||
"#383830",
|
||||
"#f8f8f2",
|
||||
"#f92672",
|
||||
"#F92672",
|
||||
"#a6e22e",
|
||||
"#66d9ef",
|
||||
"#f4bf75"
|
||||
],
|
||||
"purple-stream": {
|
||||
"name": "Purple stream",
|
||||
"bg": "#17171A",
|
||||
"fg": "#450F92",
|
||||
"link": "#8769B4",
|
||||
"text": "#C0C0C5",
|
||||
"cRed": "#EB0300",
|
||||
"cBlue": "#4656FF",
|
||||
"cGreen": "#B0E020",
|
||||
"cOrange": "#FF9046"
|
||||
},
|
||||
"feud": {
|
||||
"name": "Feud",
|
||||
"bg": "#323337",
|
||||
"fg": "#1D1E21",
|
||||
"link": "#18A0E3",
|
||||
"accent": "#6671E2",
|
||||
"text": "#DBDDE0",
|
||||
"cRed": "#E05053",
|
||||
"cBlue": "#6671E2",
|
||||
"cGreen": "#3A8D5D",
|
||||
"cOrange": "#DCAA45"
|
||||
},
|
||||
"constabulary": {
|
||||
"name": "Constabulary",
|
||||
"bg": "#FFFFFF",
|
||||
"fg": "#3B5897",
|
||||
"link": "#28487C",
|
||||
"text": "#333333",
|
||||
"cRed": "#FA3C4C",
|
||||
"cBlue": "#0083FF",
|
||||
"cGreen": "#44BDC6",
|
||||
"cOrange": "#FFC200"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
|
|
@ -1,133 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#splash {
|
||||
--scale: 1;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto;
|
||||
align-content: center;
|
||||
place-items: center;
|
||||
flex-direction: column;
|
||||
background: #0f161e;
|
||||
font-family: sans-serif;
|
||||
color: #b9b9ba;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
opacity: 1;
|
||||
transition: opacity 500ms ease-out 2s;
|
||||
}
|
||||
|
||||
#splash.hidden,
|
||||
#splash.initial-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#splash-credit {
|
||||
position: absolute;
|
||||
font-size: 1em;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#mascot-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
perspective: 60em;
|
||||
perspective-origin: 0 -15em;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
#mascot {
|
||||
width: calc(10em * var(--scale));
|
||||
height: calc(10em * var(--scale));
|
||||
object-fit: contain;
|
||||
object-position: bottom;
|
||||
transform: translateZ(-2em);
|
||||
}
|
||||
|
||||
#throbber {
|
||||
display: grid;
|
||||
width: calc(5em * 0.5 * var(--scale));
|
||||
height: calc(8em * 0.5 * var(--scale));
|
||||
margin-left: 4.1em;
|
||||
z-index: 2;
|
||||
grid-template-rows: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-areas:
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . . ."
|
||||
"P P . . ."
|
||||
"P P . E E"
|
||||
"P P . E E";
|
||||
|
||||
--logoChunkSize: calc(2em * 0.5 * var(--scale));
|
||||
}
|
||||
|
||||
.chunk {
|
||||
background-color: #e2b188;
|
||||
box-shadow: 0.01em 0.01em 0.1em 0 #e2b188;
|
||||
}
|
||||
|
||||
#chunk-P {
|
||||
grid-area: P;
|
||||
border-top-left-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-L {
|
||||
grid-area: L;
|
||||
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-E {
|
||||
grid-area: E;
|
||||
border-bottom-right-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
line-height: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#statusError {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
line-height: 2;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#statusStack {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
font-size: calc((1vw + 1vh + 1vmin) / 2.5);
|
||||
width: calc(100vw - 5em);
|
||||
padding: 1em;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
#throbber {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"pleroma-dark": "/static/themes/pleroma-dark.json",
|
||||
"pleroma-light": "/static/themes/pleroma-light.json",
|
||||
"redmond-xx": "/static/themes/redmond-xx.json",
|
||||
"redmond-xx-se": "/static/themes/redmond-xx-se.json",
|
||||
"redmond-xxi": "/static/themes/redmond-xxi.json",
|
||||
"breezy-dark": "/static/themes/breezy-dark.json",
|
||||
"breezy-light": "/static/themes/breezy-light.json",
|
||||
"mammal": "/static/themes/mammal.json",
|
||||
"paper": "/static/themes/paper.json"
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
@meta {
|
||||
name: Breezy DX;
|
||||
author: HJ;
|
||||
license: WTFPL;
|
||||
website: ebin.club;
|
||||
}
|
||||
|
||||
@palette.Dark {
|
||||
bg: #292C32;
|
||||
fg: #292C32;
|
||||
text: #ffffff;
|
||||
link: #1CA4F3;
|
||||
accent: #1CA4F3;
|
||||
cRed: #f41a51;
|
||||
cBlue: #1CA4F3;
|
||||
cGreen: #1af46e;
|
||||
cOrange: #f4af1a;
|
||||
}
|
||||
|
||||
@palette.Light {
|
||||
bg: #EFF0F2;
|
||||
fg: #EFF0F2;
|
||||
text: #1B1F22;
|
||||
underlay: #5d6086;
|
||||
accent: #1CA4F3;
|
||||
cBlue: #1CA4F3;
|
||||
cRed: #f41a51;
|
||||
cGreen: #0b6a30;
|
||||
cOrange: #f4af1a;
|
||||
border: #d8e6f9;
|
||||
link: #1CA4F3;
|
||||
}
|
||||
|
||||
@palette.Panda {
|
||||
bg: #EFF0F2;
|
||||
fg: #292C32;
|
||||
text: #1B1F22;
|
||||
link: #1CA4F3;
|
||||
accent: #1CA4F3;
|
||||
cRed: #f41a51;
|
||||
cBlue: #1CA4F3;
|
||||
cGreen: #0b6a30;
|
||||
cOrange: #f4af1a;
|
||||
}
|
||||
|
||||
Root {
|
||||
--badgeNotification: color | --cRed;
|
||||
--buttonDefaultHoverGlow: shadow | inset 0 0 0 1 --accent / 1;
|
||||
--buttonDefaultFocusGlow: shadow | inset 0 0 0 1 --accent / 1;
|
||||
--buttonDefaultShadow: shadow | inset 0 0 0 1 --text / 0.35, 0 5 5 -5 #000000 / 0.35;
|
||||
--buttonDefaultBevel: shadow | inset 0 14 14 -14 #FFFFFF / 0.1;
|
||||
--buttonPressedBevel: shadow | inset 0 -20 20 -20 #000000 / 0.05;
|
||||
--defaultInputBevel: shadow | inset 0 0 0 1 --text / 0.35;
|
||||
--defaultInputHoverGlow: shadow | 0 0 0 1 --accent / 1;
|
||||
--defaultInputFocusGlow: shadow | 0 0 0 1 --link / 1;
|
||||
}
|
||||
|
||||
Button {
|
||||
background: --parent;
|
||||
}
|
||||
|
||||
Button:disabled {
|
||||
shadow: --buttonDefaultBevel, --buttonDefaultShadow
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
background: --inheritedBackground;
|
||||
shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow
|
||||
}
|
||||
|
||||
Button:toggled {
|
||||
background: $blend(--inheritedBackground 0.3 --accent)
|
||||
}
|
||||
|
||||
Button:pressed {
|
||||
background: $blend(--inheritedBackground 0.8 --accent)
|
||||
}
|
||||
|
||||
Button:pressed:toggled {
|
||||
background: $blend(--inheritedBackground 0.2 --accent)
|
||||
}
|
||||
|
||||
Button:toggled:hover {
|
||||
background: $blend(--inheritedBackground 0.3 --accent)
|
||||
}
|
||||
|
||||
Input {
|
||||
shadow: --defaultInputBevel;
|
||||
background: $mod(--bg -10);
|
||||
}
|
||||
|
||||
PanelHeader {
|
||||
shadow: inset 0 30 30 -30 #ffffff / 0.25
|
||||
}
|
||||
|
||||
Tab:hover {
|
||||
shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow
|
||||
}
|
||||
|
||||
Tab {
|
||||
background: --bg;
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
@meta {
|
||||
name: Redmond DX;
|
||||
author: HJ;
|
||||
license: WTFPL;
|
||||
website: ebin.club;
|
||||
}
|
||||
|
||||
@palette.Modern {
|
||||
bg: #D3CFC7;
|
||||
fg: #092369;
|
||||
text: #000000;
|
||||
link: #0000FF;
|
||||
accent: #A5C9F0;
|
||||
cRed: #FF3000;
|
||||
cBlue: #009EFF;
|
||||
cGreen: #309E00;
|
||||
cOrange: #FFCE00;
|
||||
}
|
||||
|
||||
@palette.Classic {
|
||||
bg: #BFBFBF;
|
||||
fg: #000180;
|
||||
text: #000000;
|
||||
link: #0000FF;
|
||||
accent: #A5C9F0;
|
||||
cRed: #FF0000;
|
||||
cBlue: #2E2ECE;
|
||||
cGreen: #007E00;
|
||||
cOrange: #CE8F5F;
|
||||
}
|
||||
|
||||
@palette.Vapor {
|
||||
bg: #F0ADCD;
|
||||
fg: #bca4ee;
|
||||
text: #602040;
|
||||
link: #064745;
|
||||
accent: #9DF7C8;
|
||||
cRed: #86004a;
|
||||
cBlue: #0e5663;
|
||||
cGreen: #0a8b51;
|
||||
cOrange: #787424;
|
||||
}
|
||||
|
||||
Root {
|
||||
--gradientColor: color | --accent;
|
||||
--inputColor: color | #FFFFFF;
|
||||
--bevelLight: color | $brightness(--bg 50);
|
||||
--bevelDark: color | $brightness(--bg -20);
|
||||
--bevelExtraDark: color | #404040;
|
||||
--buttonDefaultBevel: shadow | $borderSide(--bevelExtraDark bottom-right 1 1), $borderSide(--bevelLight top-left 1 1), $borderSide(--bevelDark bottom-right 1 2);
|
||||
--buttonPressedFocusedBevel: shadow | inset 0 0 0 1 #000000 / 1 #Outer , inset 0 0 0 2 --bevelExtraDark / 1 #inner;
|
||||
--buttonPressedBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2);
|
||||
--defaultInputBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2), $borderSide(--bg bottom-right 1 2);
|
||||
}
|
||||
|
||||
Button:toggled {
|
||||
background: --bg;
|
||||
shadow: --buttonPressedBevel
|
||||
}
|
||||
|
||||
Button:focused {
|
||||
shadow: --buttonDefaultBevel, 0 0 0 1 #000000 / 1
|
||||
}
|
||||
|
||||
Button:pressed {
|
||||
shadow: --buttonPressedBevel
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
shadow: --buttonDefaultBevel;
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Button {
|
||||
shadow: --buttonDefaultBevel;
|
||||
background: --bg;
|
||||
roundness: 0
|
||||
}
|
||||
|
||||
Button:pressed:hover {
|
||||
shadow: --buttonPressedBevel
|
||||
}
|
||||
|
||||
Button:hover:pressed:focused {
|
||||
shadow: --buttonPressedFocusedBevel
|
||||
}
|
||||
|
||||
Button:pressed:focused {
|
||||
shadow: --buttonPressedFocusedBevel
|
||||
}
|
||||
|
||||
Button:toggled:pressed {
|
||||
shadow: --buttonPressedFocusedBevel
|
||||
}
|
||||
|
||||
Input {
|
||||
background: $boost(--bg 20);
|
||||
shadow: --defaultInputBevel;
|
||||
roundness: 0
|
||||
}
|
||||
|
||||
Input:focused {
|
||||
shadow: inset 0 0 0 1 #000000 / 1, --defaultInputBevel
|
||||
}
|
||||
|
||||
Input:focused:hover {
|
||||
shadow: --defaultInputBevel
|
||||
}
|
||||
|
||||
Input:focused:hover:disabled {
|
||||
shadow: --defaultInputBevel
|
||||
}
|
||||
|
||||
Input:hover {
|
||||
shadow: --defaultInputBevel
|
||||
}
|
||||
|
||||
Input:disabled {
|
||||
shadow: --defaultInputBevel
|
||||
}
|
||||
|
||||
Panel {
|
||||
shadow: --buttonDefaultBevel;
|
||||
roundness: 0
|
||||
}
|
||||
|
||||
PanelHeader {
|
||||
shadow: inset -1100 0 1000 -1000 --gradientColor / 1 #Gradient ;
|
||||
background: --fg
|
||||
}
|
||||
|
||||
PanelHeader ButtonUnstyled Icon {
|
||||
textColor: --text;
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
|
||||
PanelHeader Button Icon {
|
||||
textColor: --text;
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
|
||||
PanelHeader Button Text {
|
||||
textColor: --text;
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
|
||||
Tab:hover {
|
||||
background: --bg;
|
||||
shadow: --buttonDefaultBevel
|
||||
}
|
||||
|
||||
Tab:active {
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Tab:active:hover {
|
||||
background: --bg;
|
||||
shadow: --defaultButtonBevel
|
||||
}
|
||||
|
||||
Tab:active:hover:disabled {
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Tab:hover:disabled {
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Tab:disabled {
|
||||
background: --bg
|
||||
}
|
||||
|
||||
Tab {
|
||||
background: --bg;
|
||||
shadow: --buttonDefaultBevel
|
||||
}
|
||||
|
||||
Tab:hover:active {
|
||||
shadow: --buttonDefaultBevel
|
||||
}
|
||||
|
||||
TopBar Link {
|
||||
textColor: #ffffff
|
||||
}
|
||||
|
||||
MenuItem:hover {
|
||||
background: --fg
|
||||
}
|
||||
|
||||
MenuItem:active {
|
||||
background: --fg
|
||||
}
|
||||
|
||||
MenuItem:active:hover {
|
||||
background: --fg
|
||||
}
|
||||
|
||||
Popover {
|
||||
shadow: --buttonDefaultBevel, 5 5 0 0 #000000 / 0.2;
|
||||
roundness: 0
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"RedmondDX": "/static/styles/Redmond DX.iss",
|
||||
"BreezyDX": "/static/styles/Breezy DX.iss"
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"]
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
249
src/App.js
249
src/App.js
|
|
@ -1,35 +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 UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import { getOrCreateServiceWorker } from './services/sw/sw'
|
||||
import { windowHeight, windowWidth } from './services/window_utils/window_utils'
|
||||
import { useInterfaceStore } from './stores/interface'
|
||||
import { useShoutStore } from './stores/shout'
|
||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(
|
||||
() => import('./components/notifications/notifications.vue'),
|
||||
),
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
|
@ -39,230 +33,125 @@ export default {
|
|||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal: defineAsyncComponent(
|
||||
() => import('./components/settings_modal/settings_modal.vue'),
|
||||
),
|
||||
UpdateNotification: defineAsyncComponent(
|
||||
() => import('./components/update_notification/update_notification.vue'),
|
||||
),
|
||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
StatusHistoryModal,
|
||||
GlobalNoticeList,
|
||||
GlobalNoticeList
|
||||
},
|
||||
data: () => ({
|
||||
mobileActivePanel: 'timeline',
|
||||
mobileActivePanel: 'timeline'
|
||||
}),
|
||||
watch: {
|
||||
themeApplied() {
|
||||
themeApplied (value) {
|
||||
this.removeSplash()
|
||||
},
|
||||
currentTheme() {
|
||||
this.setThemeBodyClass()
|
||||
},
|
||||
layoutType() {
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
created () {
|
||||
// Load the locale from the storage
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
|
||||
// Create bound handlers
|
||||
this.updateScrollState = throttle(this.scrollHandler, 200)
|
||||
this.updateMobileState = throttle(this.resizeHandler, 200)
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.addEventListener('scroll', this.updateScrollState)
|
||||
|
||||
if (useInterfaceStore().themeApplied) {
|
||||
this.setThemeBodyClass()
|
||||
},
|
||||
mounted () {
|
||||
if (this.$store.state.interface.themeApplied) {
|
||||
this.removeSplash()
|
||||
}
|
||||
getOrCreateServiceWorker()
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
|
||||
},
|
||||
computed: {
|
||||
themeApplied() {
|
||||
return useInterfaceStore().themeApplied
|
||||
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 [
|
||||
{
|
||||
'-reverse': this.reverseLayout,
|
||||
'-no-sticky-headers': this.noSticky,
|
||||
'-has-new-post-button': this.newPostButtonShown,
|
||||
'-has-new-post-button': this.newPostButtonShown
|
||||
},
|
||||
'-' + this.layoutType,
|
||||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
navClasses() {
|
||||
navClasses () {
|
||||
const { navbarColumnStretch } = this.$store.getters.mergedConfig
|
||||
return [
|
||||
'-' + this.layoutType,
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : []),
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : [])
|
||||
]
|
||||
},
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
userBackground() {
|
||||
return this.currentUser.background_image
|
||||
},
|
||||
instanceBackground() {
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
return this.mergedConfig.hideInstanceWallpaper
|
||||
? null
|
||||
: this.$store.state.instance.background
|
||||
},
|
||||
background() {
|
||||
return this.userBackground || this.instanceBackground
|
||||
},
|
||||
bgStyle() {
|
||||
background () { return this.userBackground || this.instanceBackground },
|
||||
bgStyle () {
|
||||
if (this.background) {
|
||||
return {
|
||||
'--body-background-image': `url(${this.background})`,
|
||||
'--body-background-image': `url(${this.background})`
|
||||
}
|
||||
}
|
||||
},
|
||||
shout() {
|
||||
return useShoutStore().joined
|
||||
},
|
||||
suggestionsEnabled() {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
shout () { return this.$store.state.shout.joined },
|
||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
isChats() {
|
||||
isChats () {
|
||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||
},
|
||||
isListEdit() {
|
||||
isListEdit () {
|
||||
return this.$route.name === 'lists-edit'
|
||||
},
|
||||
newPostButtonShown() {
|
||||
newPostButtonShown () {
|
||||
if (this.isChats) return false
|
||||
if (this.isListEdit) return false
|
||||
return (
|
||||
this.$store.getters.mergedConfig.alwaysShowNewPostButton ||
|
||||
this.layoutType === 'mobile'
|
||||
)
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
},
|
||||
showFeaturesPanel() {
|
||||
return this.$store.state.instance.showFeaturesPanel
|
||||
},
|
||||
editingAvailable() {
|
||||
return this.$store.state.instance.editingAvailable
|
||||
},
|
||||
shoutboxPosition() {
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
shoutboxPosition () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
||||
},
|
||||
hideShoutbox() {
|
||||
hideShoutbox () {
|
||||
return this.$store.getters.mergedConfig.hideShoutbox
|
||||
},
|
||||
layoutType() {
|
||||
return useInterfaceStore().layoutType
|
||||
},
|
||||
privateMode() {
|
||||
return this.$store.state.instance.private
|
||||
},
|
||||
reverseLayout() {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } =
|
||||
this.$store.getters.mergedConfig
|
||||
layoutType () { return this.$store.state.interface.layoutType },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
reverseLayout () {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
||||
if (this.layoutType !== 'wide') {
|
||||
return reverseSetting
|
||||
} else {
|
||||
return thirdColumnMode === 'notifications'
|
||||
? reverseSetting
|
||||
: !reverseSetting
|
||||
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
||||
}
|
||||
},
|
||||
noSticky() {
|
||||
return this.$store.getters.mergedConfig.disableStickyHeaders
|
||||
},
|
||||
showScrollbars() {
|
||||
return this.$store.getters.mergedConfig.showScrollbars
|
||||
},
|
||||
scrollParent() {
|
||||
return window /* this.$refs.appContentRef */
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
resizeHandler() {
|
||||
useInterfaceStore().setLayoutWidth(windowWidth())
|
||||
useInterfaceStore().setLayoutHeight(windowHeight())
|
||||
updateMobileState () {
|
||||
this.$store.dispatch('setLayoutWidth', windowWidth())
|
||||
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() {
|
||||
document.querySelector('#status').textContent = this.$t(
|
||||
'splash.fun_' + Math.ceil(Math.random() * 4),
|
||||
)
|
||||
removeSplash () {
|
||||
document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4))
|
||||
const splashscreenRoot = document.querySelector('#splash')
|
||||
splashscreenRoot.addEventListener('transitionend', () => {
|
||||
splashscreenRoot.remove()
|
||||
})
|
||||
setTimeout(() => {
|
||||
splashscreenRoot.remove() // forcibly remove it, should fix my plasma browser widget t. HJ
|
||||
}, 600)
|
||||
splashscreenRoot.classList.add('hidden')
|
||||
document.querySelector('#app').classList.remove('hidden')
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
184
src/App.scss
184
src/App.scss
|
|
@ -1,9 +1,6 @@
|
|||
// stylelint-disable rscss/class-format
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
@use "panel";
|
||||
|
||||
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
@import "./panel";
|
||||
|
||||
:root {
|
||||
--status-margin: 0.75em;
|
||||
|
|
@ -21,7 +18,7 @@
|
|||
}
|
||||
|
||||
html {
|
||||
font-size: var(--textSize, 1rem);
|
||||
font-size: var(--textSize, 14px);
|
||||
|
||||
--navbar-height: var(--navbarSize, 3.5rem);
|
||||
--emoji-size: var(--emojiSize, 32px);
|
||||
|
|
@ -33,12 +30,12 @@ body {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overscroll-behavior-y: none;
|
||||
overflow: clip scroll;
|
||||
overflow-x: clip;
|
||||
overflow-y: scroll;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
|
|
@ -227,8 +224,9 @@ nav {
|
|||
grid-template-rows: 1fr;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
place-content: flex-start center;
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
|
||||
|
|
@ -264,7 +262,8 @@ nav {
|
|||
position: sticky;
|
||||
top: var(--navbar-height);
|
||||
max-height: calc(100vh - var(--navbar-height));
|
||||
overflow: hidden auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-left: calc(var(--___paddingIncrease) * -1);
|
||||
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
|
||||
|
|
@ -382,10 +381,6 @@ nav {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
|
||||
&.-transparent {
|
||||
backdrop-filter: blur(0.125em) contrast(60%);
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -393,35 +388,39 @@ nav {
|
|||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate(1px, 1px);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
line-height: var(--__line-height);
|
||||
.menu-item,
|
||||
.list-item {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: initial;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
font-size: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
a,
|
||||
button:not(.button-default) {
|
||||
color: var(--text);
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item {
|
||||
color: inherit;
|
||||
clear: both;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
border-color: var(--border);
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
border-top-width: 1px;
|
||||
width: 100%;
|
||||
line-height: var(--__line-height);
|
||||
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
||||
background: transparent;
|
||||
|
||||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__vertical-gap: 0.5em;
|
||||
|
||||
&.-non-interactive {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
&.-active,
|
||||
&:hover {
|
||||
|
|
@ -443,6 +442,20 @@ nav {
|
|||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
a,
|
||||
button:not(.button-default) {
|
||||
text-align: initial;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: inline;
|
||||
font-size: 100%;
|
||||
font-family: inherit;
|
||||
line-height: unset;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: var(--roundness);
|
||||
border-top-left-radius: var(--roundness);
|
||||
|
|
@ -456,42 +469,6 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
.menu-item,
|
||||
.list-item {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: initial;
|
||||
color: inherit;
|
||||
clear: both;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
||||
background: transparent;
|
||||
|
||||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__vertical-gap: 0.5em;
|
||||
|
||||
&.-non-interactive {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
a,
|
||||
button:not(.button-default) {
|
||||
text-align: initial;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: inline;
|
||||
font-family: inherit;
|
||||
line-height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.button-unstyled {
|
||||
border: none;
|
||||
outline: none;
|
||||
|
|
@ -513,12 +490,6 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
label {
|
||||
&.-disabled {
|
||||
color: var(--textFaint);
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border: none;
|
||||
|
|
@ -535,10 +506,6 @@ textarea {
|
|||
height: unset;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--textFaint)
|
||||
}
|
||||
|
||||
--_padding: 0.5em;
|
||||
|
||||
border: none;
|
||||
|
|
@ -559,10 +526,6 @@ textarea {
|
|||
&[disabled="disabled"],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--textFaint);
|
||||
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&[type="range"] {
|
||||
|
|
@ -588,8 +551,6 @@ textarea {
|
|||
& + label::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
+ label::before {
|
||||
|
|
@ -689,8 +650,7 @@ option {
|
|||
list-style: none;
|
||||
display: grid;
|
||||
grid-auto-flow: row dense;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20em, 1fr));
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
li {
|
||||
border: 1px solid var(--border);
|
||||
|
|
@ -700,6 +660,11 @@ option {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
|
@ -711,6 +676,7 @@ option {
|
|||
--_roundness-right: 0;
|
||||
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
> *:first-child,
|
||||
|
|
@ -757,15 +723,17 @@ option {
|
|||
}
|
||||
|
||||
&.-dot {
|
||||
min-height: 0.6em;
|
||||
max-height: 0.6em;
|
||||
min-width: 0.6em;
|
||||
max-width: 0.6em;
|
||||
left: calc(50% + 0.5em);
|
||||
top: calc(50% - 1em);
|
||||
line-height: 0;
|
||||
min-height: 8px;
|
||||
max-height: 8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 0;
|
||||
font-size: 0;
|
||||
left: calc(50% - 4px);
|
||||
top: calc(50% - 4px);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
&.-counter {
|
||||
|
|
@ -796,6 +764,12 @@ option {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.visibility-notice {
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--textFaint);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
.notice-dismissible {
|
||||
padding-right: 4rem;
|
||||
position: relative;
|
||||
|
|
@ -840,7 +814,7 @@ option {
|
|||
.login-hint {
|
||||
text-align: center;
|
||||
|
||||
@media all and (width >= 801px) {
|
||||
@media all and (min-width: 801px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -862,7 +836,7 @@ option {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
@media all and (width <= 800px) {
|
||||
@media all and (max-width: 800px) {
|
||||
.mobile-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -943,7 +917,12 @@ option {
|
|||
|
||||
#splash {
|
||||
pointer-events: none;
|
||||
// transition: opacity 0.5s;
|
||||
transition: opacity 2s;
|
||||
opacity: 1;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#status {
|
||||
&.css-ok {
|
||||
|
|
@ -967,7 +946,7 @@ option {
|
|||
|
||||
&.dead {
|
||||
animation-name: dead;
|
||||
animation-duration: 0.5s;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: 1;
|
||||
transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
|
||||
}
|
||||
|
|
@ -1082,7 +1061,7 @@ option {
|
|||
scale: 1.0063 0.9938;
|
||||
translate: 0 -10%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-timing-function: ease-in-ou;
|
||||
}
|
||||
|
||||
90% {
|
||||
|
|
@ -1101,8 +1080,3 @@ option {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@property --shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="themeApplied"
|
||||
v-show="$store.state.interface.themeApplied"
|
||||
id="app-loaded"
|
||||
:style="bgStyle"
|
||||
>
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
<Notifications v-if="currentUser" />
|
||||
<div
|
||||
id="content"
|
||||
ref="appContentRef"
|
||||
class="app-layout container"
|
||||
:class="classes"
|
||||
>
|
||||
|
|
@ -71,7 +70,7 @@
|
|||
<PostStatusModal />
|
||||
<EditStatusModal v-if="editingAvailable" />
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal :class="layoutModalClass" />
|
||||
<SettingsModal />
|
||||
<UpdateNotification />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
|
|
|
|||
1
src/assets/pleromatan_apology.png
Symbolic link
1
src/assets/pleromatan_apology.png
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../static/pleromatan_apology.png
|
||||
1
src/assets/pleromatan_apology_fox.png
Symbolic link
1
src/assets/pleromatan_apology_fox.png
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../static/pleromatan_apology_fox.png
|
||||
|
|
@ -1,39 +1,21 @@
|
|||
/* global process */
|
||||
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeLayers,
|
||||
} from '@fortawesome/vue-fontawesome'
|
||||
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 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 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
|
||||
|
||||
|
|
@ -42,9 +24,7 @@ const parsedInitialResults = () => {
|
|||
return null
|
||||
}
|
||||
if (!staticInitialResults) {
|
||||
staticInitialResults = JSON.parse(
|
||||
document.getElementById('initial-results').textContent,
|
||||
)
|
||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||
}
|
||||
return staticInitialResults
|
||||
}
|
||||
|
|
@ -66,7 +46,7 @@ const preloadFetch = async (request) => {
|
|||
return {
|
||||
ok: true,
|
||||
json: () => requestData,
|
||||
text: () => requestData,
|
||||
text: () => requestData
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,57 +58,34 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const textlimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pleromaExtensionsAvailable',
|
||||
value: data.pleroma,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'textlimit',
|
||||
value: textlimit,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'accountApprovalRequired',
|
||||
value: data.approval_required,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'birthdayRequired',
|
||||
value: !!data.pleroma?.metadata.birthday_required,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'birthdayMinAge',
|
||||
value: data.pleroma?.metadata.birthday_min_age || 0,
|
||||
})
|
||||
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) {
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'vapidPublicKey',
|
||||
value: vapidPublicKey,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
}
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load instance config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
// We should check for scrobbles support here but it requires userId
|
||||
// so instead we check for it where it's fetched (statuses.js)
|
||||
}
|
||||
|
||||
const getBackendProvidedConfig = async () => {
|
||||
const getBackendProvidedConfig = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/api/pleroma/frontend_configurations')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
return data.pleroma_fe
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Could not load backend-provided frontend config, potentially fatal',
|
||||
)
|
||||
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -139,7 +96,7 @@ const getStaticConfig = async () => {
|
|||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
|
|
@ -162,15 +119,47 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
}
|
||||
|
||||
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)
|
||||
Object.keys(instanceDefaultConfig).forEach(copyInstanceOption)
|
||||
copyInstanceOption('theme')
|
||||
copyInstanceOption('nsfwCensorImage')
|
||||
copyInstanceOption('background')
|
||||
copyInstanceOption('hidePostStats')
|
||||
copyInstanceOption('hideBotIndication')
|
||||
copyInstanceOption('hideUserStats')
|
||||
copyInstanceOption('hideFilteredStatuses')
|
||||
copyInstanceOption('logo')
|
||||
|
||||
useAuthFlowStore().setInitialStrategy(config.loginMethod)
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
value: typeof config.logoMask === 'undefined'
|
||||
? true
|
||||
: config.logoMask
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMargin',
|
||||
value: typeof config.logoMargin === 'undefined'
|
||||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
copyInstanceOption('showInstanceSpecificPanel')
|
||||
copyInstanceOption('minimalScopesMode')
|
||||
copyInstanceOption('hideMutedPosts')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('sidebarRight')
|
||||
}
|
||||
|
||||
const getTOS = async ({ store }) => {
|
||||
|
|
@ -180,10 +169,11 @@ const getTOS = async ({ store }) => {
|
|||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load TOS\n", e)
|
||||
console.warn("Can't load TOS")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,15 +182,13 @@ const getInstancePanel = async ({ store }) => {
|
|||
const res = await preloadFetch('/instance/panel.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'instanceSpecificPanelContent',
|
||||
value: html,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load instance panel\n", e)
|
||||
console.warn("Can't load instance panel")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -209,220 +197,119 @@ const getStickers = async ({ store }) => {
|
|||
const res = await window.fetch('/static/stickers.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const stickers = (
|
||||
await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta,
|
||||
}
|
||||
}),
|
||||
)
|
||||
).sort((a, b) => {
|
||||
const stickers = (await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
return a.meta.title.localeCompare(b.meta.title)
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load stickers\n", e)
|
||||
console.warn("Can't load stickers")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
const getAppSecret = async ({ store }) => {
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.userToken) {
|
||||
store.commit(
|
||||
'setBackendInteractor',
|
||||
backendInteractorService(oauth.getToken),
|
||||
)
|
||||
}
|
||||
const { state, commit } = store
|
||||
const { oauth, instance } = state
|
||||
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
|
||||
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
||||
.then((token) => {
|
||||
commit('setAppToken', token.access_token)
|
||||
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
})
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'staffAccounts',
|
||||
value: nicknames,
|
||||
})
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
}
|
||||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
try {
|
||||
let res = await preloadFetch('/nodeinfo/2.1.json')
|
||||
if (!res.ok) res = await preloadFetch('/nodeinfo/2.0.json')
|
||||
const res = await preloadFetch('/nodeinfo/2.0.json')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
const features = metadata.features
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'name',
|
||||
value: metadata.nodeName,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'registrationOpen',
|
||||
value: data.openRegistrations,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'mediaProxyAvailable',
|
||||
value: features.includes('media_proxy'),
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'safeDM',
|
||||
value: features.includes('safe_dm_mentions'),
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'shoutAvailable',
|
||||
value: features.includes('chat'),
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pleromaChatMessagesAvailable',
|
||||
value: features.includes('pleroma_chat_messages'),
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pleromaCustomEmojiReactionsAvailable',
|
||||
value:
|
||||
features.includes('pleroma_custom_emoji_reactions') ||
|
||||
features.includes('custom_emoji_reactions'),
|
||||
})
|
||||
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 ?? [],
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
|
||||
store.dispatch('setInstanceOption', { name: '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') })
|
||||
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'uploadlimit',
|
||||
value: parseInt(uploadLimits.general),
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'avatarlimit',
|
||||
value: parseInt(uploadLimits.avatar),
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'backgroundlimit',
|
||||
value: parseInt(uploadLimits.background),
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'bannerlimit',
|
||||
value: parseInt(uploadLimits.banner),
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'fieldsLimits',
|
||||
value: metadata.fieldsLimits,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
||||
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'restrictedNicknames',
|
||||
value: metadata.restrictedNicknames,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'postFormats',
|
||||
value: metadata.postFormats,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||
|
||||
const suggestions = metadata.suggestions
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'suggestionsEnabled',
|
||||
value: suggestions.enabled,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'suggestionsWeb',
|
||||
value: suggestions.web,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
||||
|
||||
const software = data.software
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'backendVersion',
|
||||
value: software.version,
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'backendRepository',
|
||||
value: software.repository,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
|
||||
|
||||
const priv = metadata.private
|
||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'frontendVersion',
|
||||
value: frontendVersion,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
|
||||
const federation = metadata.federation
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'tagPolicyAvailable',
|
||||
value:
|
||||
typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy'),
|
||||
value: typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federationPolicy',
|
||||
value: federation,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
value:
|
||||
typeof federation.enabled === 'undefined' ? true : federation.enabled,
|
||||
value: typeof federation.enabled === 'undefined'
|
||||
? true
|
||||
: federation.enabled
|
||||
})
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'accountActivationRequired',
|
||||
value: accountActivationRequired,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
throw res
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load nodeinfo')
|
||||
|
|
@ -432,89 +319,23 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const setConfig = async ({ store }) => {
|
||||
// apiConfig, staticConfig
|
||||
const configInfos = await Promise.all([
|
||||
getBackendProvidedConfig({ store }),
|
||||
getStaticConfig(),
|
||||
])
|
||||
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
getAppSecret({ store })
|
||||
await setSettings({ store, apiConfig, staticConfig })
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.getUserToken) {
|
||||
return store.dispatch('loginUser', oauth.getUserToken)
|
||||
if (store.getters.getUserToken()) {
|
||||
return store.dispatch('loginUser', store.getters.getUserToken())
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
||||
const app = createApp(App)
|
||||
// Must have app use pinia before we do anything that touches the store
|
||||
// https://pinia.vuejs.org/core-concepts/plugins.html#Introduction
|
||||
// "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied."
|
||||
app.use(pinia)
|
||||
|
||||
const waitForAllStoresToLoad = async () => {
|
||||
// the stores that do not persist technically do not need to be awaited here,
|
||||
// but that involves either hard-coding the stores in some place (prone to errors)
|
||||
// or writing another vite plugin to analyze which stores needs persisting (++load time)
|
||||
const allStores = import.meta.glob('../stores/*.js', { eager: true })
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// do some checks to avoid common errors
|
||||
if (!Object.keys(allStores).length) {
|
||||
throw new Error(
|
||||
'No stores are available. Check the code in src/boot/after_store.js',
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Object.entries(allStores).map(async ([name, mod]) => {
|
||||
const isStoreName = (name) => name.startsWith('use')
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (Object.keys(mod).filter(isStoreName).length !== 1) {
|
||||
throw new Error(
|
||||
'Each store file must export exactly one store as a named export. Check your code in src/stores/',
|
||||
)
|
||||
}
|
||||
}
|
||||
const storeFuncName = Object.keys(mod).find(isStoreName)
|
||||
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
||||
const p = mod[storeFuncName]().$persistLoaded
|
||||
if (!(p instanceof Promise)) {
|
||||
throw new Error(
|
||||
`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`,
|
||||
)
|
||||
}
|
||||
await p
|
||||
} else {
|
||||
throw new Error(
|
||||
`Store module ${name} does not export a 'use...' function`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await waitForAllStoresToLoad()
|
||||
} catch (e) {
|
||||
console.error('Cannot load stores:', e)
|
||||
storageError = e
|
||||
}
|
||||
|
||||
if (storageError) {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'errors.storage_unavailable',
|
||||
level: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
useInterfaceStore().setLayoutWidth(windowWidth())
|
||||
useInterfaceStore().setLayoutHeight(windowHeight())
|
||||
const afterStoreSetup = async ({ store, i18n }) => {
|
||||
store.dispatch('setLayoutWidth', windowWidth())
|
||||
store.dispatch('setLayoutHeight', windowHeight())
|
||||
|
||||
FaviconService.initFaviconService()
|
||||
initServiceWorker(store)
|
||||
|
|
@ -522,21 +343,15 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
window.addEventListener('focus', () => updateFocus())
|
||||
|
||||
const overrides = window.___pleromafe_dev_overrides || {}
|
||||
const server =
|
||||
typeof overrides.target !== 'undefined'
|
||||
? overrides.target
|
||||
: window.location.origin
|
||||
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
|
||||
document.querySelector('#status').textContent = i18n.global.t('splash.settings')
|
||||
await setConfig({ store })
|
||||
document.querySelector('#status').textContent = i18n.global.t('splash.theme')
|
||||
try {
|
||||
await useInterfaceStore()
|
||||
.applyTheme()
|
||||
.catch((e) => {
|
||||
console.error('Error setting theme', e)
|
||||
})
|
||||
await store.dispatch('setTheme').catch((e) => { console.error('Error setting theme', e) })
|
||||
} catch (e) {
|
||||
window.splashError(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
|
||||
// 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([
|
||||
checkOAuthToken({ store }),
|
||||
getInstancePanel({ store }),
|
||||
getNodeInfo({ store }),
|
||||
getInstanceConfig({ store }),
|
||||
]).catch((e) => Promise.reject(e))
|
||||
getInstanceConfig({ store })
|
||||
]).catch(e => Promise.reject(e))
|
||||
|
||||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
store.dispatch('loadDrafts')
|
||||
useAnnouncementsStore().startFetchingAnnouncements()
|
||||
store.dispatch('startFetchingAnnouncements')
|
||||
getTOS({ store })
|
||||
getStickers({ store })
|
||||
|
||||
|
|
@ -562,26 +377,19 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
history: createWebHistory(),
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some((m) => m.meta.dontScroll)) {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
return false
|
||||
}
|
||||
return savedPosition || { left: 0, top: 0 }
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
useI18nStore().setI18n(i18n)
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(i18n)
|
||||
|
||||
// Little thing to get out of invalid theme state
|
||||
window.resetThemes = () => {
|
||||
useInterfaceStore().resetThemeV3()
|
||||
useInterfaceStore().resetThemeV3Palette()
|
||||
useInterfaceStore().resetThemeV2()
|
||||
}
|
||||
|
||||
app.use(vClickOutside)
|
||||
app.use(VBodyScrollLock)
|
||||
app.use(VueVirtualScroller)
|
||||
|
|
@ -591,6 +399,7 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
|
||||
// remove after vue 3.3
|
||||
app.config.unwrapInjectedRef = true
|
||||
document.querySelector('#status').textContent = i18n.global.t('splash.almost')
|
||||
|
||||
app.mount('#app')
|
||||
return app
|
||||
|
|
|
|||
|
|
@ -1,35 +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 Registration from 'components/registration/registration.vue'
|
||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||
import Interactions from 'components/interactions/interactions.vue'
|
||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import ChatList from 'components/chat_list/chat_list.vue'
|
||||
import Chat from 'components/chat/chat.vue'
|
||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import Registration from 'components/registration/registration.vue'
|
||||
import PasswordReset from 'components/password_reset/password_reset.vue'
|
||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import AuthForm from 'components/auth_form/auth_form.js'
|
||||
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
import About from 'components/about/about.vue'
|
||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import Lists from 'components/lists/lists.vue'
|
||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
|
||||
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
|
||||
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
|
|
@ -44,177 +42,63 @@ export default (store) => {
|
|||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect: () => {
|
||||
return (
|
||||
(store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'public-external-timeline',
|
||||
path: '/main/all',
|
||||
component: PublicAndExternalTimeline,
|
||||
},
|
||||
{
|
||||
name: 'public-timeline',
|
||||
path: '/main/public',
|
||||
component: PublicTimeline,
|
||||
},
|
||||
{
|
||||
name: 'friends',
|
||||
path: '/main/friends',
|
||||
component: FriendsTimeline,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
redirect: _to => {
|
||||
return (store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
}
|
||||
},
|
||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'bubble', path: '/bubble', component: BubbleTimeline },
|
||||
{
|
||||
name: 'conversation',
|
||||
path: '/notice/:id',
|
||||
component: ConversationPage,
|
||||
meta: { dontScroll: true },
|
||||
},
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
|
||||
{
|
||||
name: 'remote-user-profile-acct',
|
||||
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'remote-user-profile',
|
||||
path: '/remote-users/:hostname/:username',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'external-user-profile',
|
||||
path: '/users/$:id',
|
||||
component: UserProfile,
|
||||
},
|
||||
{
|
||||
name: 'interactions',
|
||||
path: '/users/:username/interactions',
|
||||
component: Interactions,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'dms',
|
||||
path: '/users/:username/dms',
|
||||
component: DMs,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{
|
||||
name: 'password-reset',
|
||||
path: '/password-reset',
|
||||
component: PasswordReset,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'registration-token',
|
||||
path: '/registration/:token',
|
||||
component: Registration,
|
||||
},
|
||||
{
|
||||
name: 'friend-requests',
|
||||
path: '/friend-requests',
|
||||
component: FollowRequests,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
path: '/:username/notifications',
|
||||
component: Notifications,
|
||||
props: () => ({ disableTeleport: true }),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{
|
||||
name: 'shout-panel',
|
||||
path: '/shout-panel',
|
||||
component: ShoutPanel,
|
||||
props: () => ({ floating: false }),
|
||||
},
|
||||
{
|
||||
name: 'oauth-callback',
|
||||
path: '/oauth-callback',
|
||||
component: OAuthCallback,
|
||||
props: (route) => ({ code: route.query.code }),
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: '/search',
|
||||
component: Search,
|
||||
props: (route) => ({ query: route.query.query }),
|
||||
},
|
||||
{
|
||||
name: 'who-to-follow',
|
||||
path: '/who-to-follow',
|
||||
component: WhoToFollow,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{
|
||||
name: 'announcements',
|
||||
path: '/announcements',
|
||||
component: AnnouncementsPage,
|
||||
},
|
||||
{ name: 'drafts', path: '/drafts', component: Drafts },
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
||||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||
{ name: 'lists', path: '/lists', component: Lists },
|
||||
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
|
||||
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
|
||||
{ name: 'lists-new', path: '/lists/new', component: ListsEdit },
|
||||
{
|
||||
name: 'edit-navigation',
|
||||
path: '/nav-edit',
|
||||
component: NavPanel,
|
||||
props: () => ({ forceExpand: true, forceEditMode: true }),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folders',
|
||||
path: '/bookmark_folders',
|
||||
component: BookmarkFolders,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder-new',
|
||||
path: '/bookmarks/new-folder',
|
||||
component: BookmarkFolderEdit,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder',
|
||||
path: '/bookmarks/:id',
|
||||
component: BookmarkTimeline,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder-edit',
|
||||
path: '/bookmarks/:id/edit',
|
||||
component: BookmarkFolderEdit,
|
||||
},
|
||||
{ name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{ 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) {
|
||||
routes = routes.concat([
|
||||
{
|
||||
name: 'chat',
|
||||
path: '/users/:username/chats/:recipient_id',
|
||||
component: Chat,
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'chats',
|
||||
path: '/users/:username/chats',
|
||||
component: ChatList,
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
|
||||
const About = {
|
||||
components: {
|
||||
|
|
@ -10,20 +10,16 @@ const About = {
|
|||
FeaturesPanel,
|
||||
TermsOfServicePanel,
|
||||
StaffPanel,
|
||||
MRFTransparencyPanel,
|
||||
MRFTransparencyPanel
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel() {
|
||||
return this.$store.state.instance.showFeaturesPanel
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default About
|
||||
|
|
|
|||
|
|
@ -1,101 +1,98 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||
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 { mapState } from 'vuex'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import Popover from '../popover/popover.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 {
|
||||
faEllipsisV
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisV)
|
||||
library.add(
|
||||
faEllipsisV
|
||||
)
|
||||
|
||||
const AccountActions = {
|
||||
props: ['user', 'relationship'],
|
||||
data() {
|
||||
props: [
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingConfirmBlock: false,
|
||||
showingConfirmRemoveFollower: false,
|
||||
showingConfirmRemoveFollower: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover,
|
||||
UserListMenu,
|
||||
ConfirmModal,
|
||||
UserTimedFilterModal,
|
||||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
showConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
hideConfirmBlock() {
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats() {
|
||||
showConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers () {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
showRepeats () {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
hideRepeats() {
|
||||
hideRepeats () {
|
||||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser() {
|
||||
if (this.$refs.timedBlockDialog) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
blockUser () {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showingConfirmBlock = true
|
||||
}
|
||||
this.showConfirmBlock()
|
||||
}
|
||||
},
|
||||
doBlockUser() {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
doBlockUser () {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser() {
|
||||
unblockUser () {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers() {
|
||||
removeUserFromFollowers () {
|
||||
if (!this.shouldConfirmRemoveUserFromFollowers) {
|
||||
this.doRemoveUserFromFollowers()
|
||||
} else {
|
||||
this.showConfirmRemoveUserFromFollowers()
|
||||
}
|
||||
},
|
||||
doRemoveUserFromFollowers() {
|
||||
doRemoveUserFromFollowers () {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
this.hideConfirmRemoveUserFromFollowers()
|
||||
},
|
||||
reportUser() {
|
||||
useReportsStore().openUserReportingModal({ userId: this.user.id })
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
},
|
||||
openChat() {
|
||||
openChat () {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: {
|
||||
username: this.$store.state.users.currentUser.screen_name,
|
||||
recipient_id: this.user.id,
|
||||
},
|
||||
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock() {
|
||||
shouldConfirmBlock () {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
shouldConfirmRemoveUserFromFollowers() {
|
||||
shouldConfirmRemoveUserFromFollowers () {
|
||||
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
|
||||
},
|
||||
...mapState({
|
||||
blockExpirationSupported: (state) => state.instance.blockExpiration,
|
||||
pleromaChatMessagesAvailable: (state) =>
|
||||
state.instance.pleromaChatMessagesAvailable,
|
||||
}),
|
||||
},
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountActions
|
||||
|
|
|
|||
|
|
@ -3,85 +3,66 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="relationship.following">
|
||||
<div
|
||||
<button
|
||||
v-if="relationship.showing_reblogs"
|
||||
class="menu-item dropdown-item"
|
||||
class="dropdown-item menu-item"
|
||||
@click="hideRepeats"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click="hideRepeats"
|
||||
>
|
||||
{{ $t('user_card.hide_repeats') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
{{ $t('user_card.hide_repeats') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!relationship.showing_reblogs"
|
||||
class="menu-item dropdown-item"
|
||||
class="dropdown-item menu-item"
|
||||
@click="showRepeats"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click="showRepeats"
|
||||
>
|
||||
{{ $t('user_card.show_repeats') }}
|
||||
</button>
|
||||
</div>
|
||||
{{ $t('user_card.show_repeats') }}
|
||||
</button>
|
||||
<div
|
||||
role="separator"
|
||||
class="dropdown-divider"
|
||||
/>
|
||||
</template>
|
||||
<UserListMenu :user="user" />
|
||||
<div
|
||||
<button
|
||||
v-if="relationship.followed_by"
|
||||
class="menu-item dropdown-item"
|
||||
class="dropdown-item menu-item"
|
||||
@click="removeUserFromFollowers"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click="removeUserFromFollowers"
|
||||
>
|
||||
{{ $t('user_card.remove_follower') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="menu-item dropdown-item">
|
||||
<button
|
||||
v-if="relationship.blocking"
|
||||
class="main-button"
|
||||
@click="unblockUser"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="main-button"
|
||||
@click="blockUser"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="menu-item dropdown-item">
|
||||
<button
|
||||
class="main-button"
|
||||
@click="reportUser"
|
||||
>
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
{{ $t('user_card.remove_follower') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="relationship.blocking"
|
||||
class="dropdown-item menu-item"
|
||||
@click="unblockUser"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="dropdown-item menu-item"
|
||||
@click="blockUser"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item menu-item"
|
||||
@click="reportUser"
|
||||
>
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="pleromaChatMessagesAvailable"
|
||||
class="menu-item dropdown-item"
|
||||
class="dropdown-item menu-item"
|
||||
@click="openChat"
|
||||
>
|
||||
<button
|
||||
class="main-button"
|
||||
@click="openChat"
|
||||
>
|
||||
{{ $t('user_card.message') }}
|
||||
</button>
|
||||
</div>
|
||||
{{ $t('user_card.message') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
|
|
@ -95,8 +76,7 @@
|
|||
</Popover>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
v-if="showingConfirmBlock && !blockExpirationSupported"
|
||||
ref="blockDialog"
|
||||
v-if="showingConfirmBlock"
|
||||
:title="$t('user_card.block_confirm_title')"
|
||||
:confirm-text="$t('user_card.block_confirm_accept_button')"
|
||||
:cancel-text="$t('user_card.block_confirm_cancel_button')"
|
||||
|
|
@ -106,7 +86,6 @@
|
|||
<i18n-t
|
||||
keypath="user_card.block_confirm"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #user>
|
||||
<span
|
||||
|
|
@ -128,7 +107,6 @@
|
|||
<i18n-t
|
||||
keypath="user_card.remove_follower_confirm"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #user>
|
||||
<span
|
||||
|
|
@ -137,12 +115,6 @@
|
|||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
<UserTimedFilterModal
|
||||
v-if="blockExpirationSupported"
|
||||
ref="timedBlockDialog"
|
||||
:is-mute="false"
|
||||
:user="user"
|
||||
/>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,51 +1,53 @@
|
|||
export default {
|
||||
name: 'Alert',
|
||||
selector: '.alert',
|
||||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Link',
|
||||
'Border',
|
||||
'ButtonUnstyled'
|
||||
],
|
||||
variants: {
|
||||
normal: '.neutral',
|
||||
error: '.error',
|
||||
warning: '.warning',
|
||||
success: '.success',
|
||||
},
|
||||
editor: {
|
||||
border: 1,
|
||||
aspect: '3 / 1',
|
||||
success: '.success'
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--text',
|
||||
opacity: 0.5,
|
||||
blur: '9px',
|
||||
},
|
||||
blur: '9px'
|
||||
}
|
||||
},
|
||||
{
|
||||
parent: {
|
||||
component: 'Alert',
|
||||
component: 'Alert'
|
||||
},
|
||||
component: 'Border',
|
||||
directives: {
|
||||
textColor: '--parent',
|
||||
},
|
||||
textColor: '--parent'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'error',
|
||||
directives: {
|
||||
background: '--cRed',
|
||||
},
|
||||
background: '--cRed'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'warning',
|
||||
directives: {
|
||||
background: '--cOrange',
|
||||
},
|
||||
background: '--cOrange'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'success',
|
||||
directives: {
|
||||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
],
|
||||
background: '--cGreen'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,128 +1,108 @@
|
|||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { mapState } from 'vuex'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import RichContent from '../rich_content/rich_content.jsx'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent,
|
||||
RichContent
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: undefined,
|
||||
allDay: undefined
|
||||
},
|
||||
editError: '',
|
||||
editError: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
announcement: Object
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
canEditAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
)
|
||||
canEditAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
},
|
||||
content() {
|
||||
content () {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead() {
|
||||
isRead () {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt() {
|
||||
publishedAt () {
|
||||
const time = this.announcement.published_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
startsAt() {
|
||||
startsAt () {
|
||||
const time = this.announcement.starts_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
endsAt() {
|
||||
endsAt () {
|
||||
const time = this.announcement.ends_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
inactive() {
|
||||
inactive () {
|
||||
return this.announcement.inactive
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsRead() {
|
||||
markAsRead () {
|
||||
if (!this.isRead) {
|
||||
return useAnnouncementsStore().markAnnouncementAsRead(
|
||||
this.announcement.id,
|
||||
)
|
||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement() {
|
||||
return useAnnouncementsStore().deleteAnnouncement(this.announcement.id)
|
||||
deleteAnnouncement () {
|
||||
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate(time, locale) {
|
||||
formatTimeOrDate (time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement.all_day
|
||||
? d.toLocaleDateString(locale)
|
||||
: d.toLocaleString(locale)
|
||||
return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode() {
|
||||
enterEditMode () {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma.raw_content
|
||||
this.editedAnnouncement.startsAt = this.announcement.starts_at
|
||||
this.editedAnnouncement.endsAt = this.announcement.ends_at
|
||||
this.editedAnnouncement.allDay = this.announcement.all_day
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit() {
|
||||
useAnnouncementsStore()
|
||||
.editAnnouncement({
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement,
|
||||
})
|
||||
submitEdit () {
|
||||
this.$store.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit() {
|
||||
cancelEdit () {
|
||||
this.editing = false
|
||||
},
|
||||
clearError() {
|
||||
clearError () {
|
||||
this.editError = undefined
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Announcement
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import Checkbox from '../checkbox/checkbox.vue'
|
|||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
Checkbox,
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
disabled: Boolean,
|
||||
},
|
||||
disabled: Boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementEditor
|
||||
|
|
|
|||
|
|
@ -34,9 +34,8 @@
|
|||
id="announcement-all-day"
|
||||
v-model="announcement.allDay"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ $t('announcements.all_day_prompt') }}
|
||||
</Checkbox>
|
||||
/>
|
||||
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -56,7 +55,7 @@
|
|||
.post-textarea {
|
||||
resize: vertical;
|
||||
height: 10em;
|
||||
overflow: visible;
|
||||
overflow: none;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { mapState } from 'vuex'
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
|
|
@ -6,60 +5,54 @@ import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
|||
const AnnouncementsPage = {
|
||||
components: {
|
||||
Announcement,
|
||||
AnnouncementEditor,
|
||||
AnnouncementEditor
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: false,
|
||||
allDay: false
|
||||
},
|
||||
posting: false,
|
||||
error: undefined,
|
||||
error: undefined
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
useAnnouncementsStore().fetchAnnouncements()
|
||||
mounted () {
|
||||
this.$store.dispatch('fetchAnnouncements')
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
announcements() {
|
||||
return useAnnouncementsStore().announcements
|
||||
},
|
||||
canPostAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
)
|
||||
announcements () {
|
||||
return this.$store.state.announcements.announcements
|
||||
},
|
||||
canPostAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement() {
|
||||
postAnnouncement () {
|
||||
this.posting = true
|
||||
useAnnouncementsStore()
|
||||
.postAnnouncement(this.newAnnouncement)
|
||||
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError() {
|
||||
clearError () {
|
||||
this.error = undefined
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementsPage
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="panel panel-default announcements-page">
|
||||
<div class="panel-heading">
|
||||
<h1 class="title">
|
||||
<span>
|
||||
{{ $t('announcements.page_header') }}
|
||||
</h1>
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
export default {
|
||||
emits: ['resetAsyncComponent'],
|
||||
methods: {
|
||||
retry() {
|
||||
retry () {
|
||||
this.$emit('resetAsyncComponent')
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAlignRight,
|
||||
faFile,
|
||||
faImage,
|
||||
faMusic,
|
||||
faPencilAlt,
|
||||
faPlayCircle,
|
||||
faSearchPlus,
|
||||
faStop,
|
||||
faTimes,
|
||||
faTrashAlt,
|
||||
faVideo,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
import { mapGetters } from 'vuex'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faFile,
|
||||
faMusic,
|
||||
faImage,
|
||||
faVideo,
|
||||
faPlayCircle,
|
||||
faTimes,
|
||||
faStop,
|
||||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
|
|
@ -31,7 +30,7 @@ library.add(
|
|||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight,
|
||||
faAlignRight
|
||||
)
|
||||
|
||||
const Attachment = {
|
||||
|
|
@ -46,74 +45,72 @@ const Attachment = {
|
|||
'remove',
|
||||
'shiftUp',
|
||||
'shiftDn',
|
||||
'edit',
|
||||
'edit'
|
||||
],
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img:
|
||||
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
|
||||
document.createElement('img'),
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
showDescription: false,
|
||||
showDescription: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment,
|
||||
VideoAttachment
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
classNames () {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
'-nsfw-placeholder': this.hidden,
|
||||
'-editable': this.edit !== undefined,
|
||||
'-compact': this.compact,
|
||||
'-compact': this.compact
|
||||
},
|
||||
'-type-' + this.type,
|
||||
this.size && '-size-' + this.size,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
]
|
||||
},
|
||||
usePlaceholder() {
|
||||
usePlaceholder () {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit() {
|
||||
useContainFit () {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
placeholderName() {
|
||||
placeholderName () {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
return this.type.toUpperCase()
|
||||
}
|
||||
return this.attachment.description
|
||||
},
|
||||
placeholderIconClass() {
|
||||
placeholderIconClass () {
|
||||
if (this.type === 'image') return 'image'
|
||||
if (this.type === 'video') return 'video'
|
||||
if (this.type === 'audio') return 'music'
|
||||
return 'file'
|
||||
},
|
||||
referrerpolicy() {
|
||||
referrerpolicy () {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
type() {
|
||||
type () {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
},
|
||||
hidden() {
|
||||
hidden () {
|
||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty() {
|
||||
return this.type === 'html' && !this.attachment.oembed
|
||||
isEmpty () {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
},
|
||||
useModal() {
|
||||
useModal () {
|
||||
let modalTypes = []
|
||||
switch (this.size) {
|
||||
case 'hide':
|
||||
|
|
@ -128,62 +125,61 @@ const Attachment = {
|
|||
}
|
||||
return modalTypes.includes(this.type)
|
||||
},
|
||||
videoTag() {
|
||||
videoTag () {
|
||||
return this.useModal ? 'button' : 'span'
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
watch: {
|
||||
'attachment.description'(newVal) {
|
||||
'attachment.description' (newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription(newVal) {
|
||||
localDescription (newVal) {
|
||||
this.onEdit(newVal)
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkClicked({ target }) {
|
||||
linkClicked ({ target }) {
|
||||
if (target.tagName === 'A') {
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
openModal() {
|
||||
openModal (event) {
|
||||
if (this.useModal) {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
} else if (this.type === 'unknown') {
|
||||
window.open(this.attachment.url)
|
||||
}
|
||||
},
|
||||
openModalForce() {
|
||||
openModalForce (event) {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
},
|
||||
onEdit(event) {
|
||||
onEdit (event) {
|
||||
this.edit && this.edit(this.attachment, event)
|
||||
},
|
||||
onRemove() {
|
||||
onRemove () {
|
||||
this.remove && this.remove(this.attachment)
|
||||
},
|
||||
onShiftUp() {
|
||||
onShiftUp () {
|
||||
this.shiftUp && this.shiftUp(this.attachment)
|
||||
},
|
||||
onShiftDn() {
|
||||
onShiftDn () {
|
||||
this.shiftDn && this.shiftDn(this.attachment)
|
||||
},
|
||||
stopFlash() {
|
||||
stopFlash () {
|
||||
this.$refs.flash.closePlayer()
|
||||
},
|
||||
setFlashLoaded(event) {
|
||||
setFlashLoaded (event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription() {
|
||||
toggleDescription () {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden(event) {
|
||||
toggleHidden (event) {
|
||||
if (
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
!this.showHidden &&
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
|
|
@ -204,12 +200,12 @@ const Attachment = {
|
|||
this.showHidden = !this.showHidden
|
||||
}
|
||||
},
|
||||
onImageLoad(image) {
|
||||
onImageLoad (image) {
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Attachment
|
||||
|
|
|
|||
|
|
@ -107,9 +107,9 @@
|
|||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 4.5em;
|
||||
top: calc(50% - 2.25rem);
|
||||
left: calc(50% - 2.25rem);
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
|
||||
|
||||
|
|
@ -177,8 +177,7 @@
|
|||
.text {
|
||||
flex: 2;
|
||||
margin: 8px;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
word-break: break-all;
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
|
|
|
|||
24
src/components/attachment/attachment.style.js
Normal file
24
src/components/attachment/attachment.style.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export default {
|
||||
name: 'Attachment',
|
||||
selector: '.Attachment',
|
||||
validInnerComponents: [
|
||||
'Border',
|
||||
'ButtonUnstyled',
|
||||
'Input'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'ButtonUnstyled',
|
||||
parent: { component: 'Attachment' },
|
||||
directives: {
|
||||
background: '#FFFFFF',
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="usePlaceholder"
|
||||
class="Attachment -placeholder button-default"
|
||||
class="Attachment -placeholder button-unstyled"
|
||||
:class="classNames"
|
||||
@click="openModal"
|
||||
>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
<FAIcon icon="trash-alt" />
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
>
|
||||
<button
|
||||
v-if="type === 'flash' && flashLoaded"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.attachment_stop_flash')"
|
||||
@click.prevent="stopFlash"
|
||||
>
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
</button>
|
||||
<button
|
||||
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')"
|
||||
@click.prevent="toggleDescription"
|
||||
>
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="!useModal && type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.show_attachment_in_modal')"
|
||||
@click.prevent="openModalForce"
|
||||
>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="nsfw && hideNsfwLocal"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.hide_attachment')"
|
||||
@click.prevent="toggleHidden"
|
||||
>
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftUp"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.move_up')"
|
||||
@click.prevent="onShiftUp"
|
||||
>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftDn"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.move_down')"
|
||||
@click.prevent="onShiftDn"
|
||||
>
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button -transparent"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.remove_attachment')"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
|
|
@ -238,8 +238,8 @@
|
|||
ref="flash"
|
||||
class="flash"
|
||||
:src="attachment.large_thumb_url || attachment.url"
|
||||
@player-opened="setFlashLoaded(true)"
|
||||
@player-closed="setFlashLoaded(false)"
|
||||
@playerOpened="setFlashLoaded(true)"
|
||||
@playerClosed="setFlashLoaded(false)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,27 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render() {
|
||||
render () {
|
||||
return h(resolveComponent(this.authForm))
|
||||
},
|
||||
computed: {
|
||||
authForm() {
|
||||
if (this.requiredTOTP) {
|
||||
return 'MFATOTPForm'
|
||||
}
|
||||
if (this.requiredRecovery) {
|
||||
return 'MFARecoveryForm'
|
||||
}
|
||||
authForm () {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery']),
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
},
|
||||
components: {
|
||||
MFARecoveryForm,
|
||||
MFATOTPForm,
|
||||
LoginForm,
|
||||
},
|
||||
LoginForm
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthForm
|
||||
|
|
|
|||
|
|
@ -2,55 +2,51 @@ const debounceMilliseconds = 500
|
|||
|
||||
export default {
|
||||
props: {
|
||||
query: {
|
||||
// function to query results and return a promise
|
||||
query: { // function to query results and return a promise
|
||||
type: Function,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
filter: {
|
||||
// function to filter results in real time
|
||||
type: Function,
|
||||
filter: { // function to filter results in real time
|
||||
type: Function
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Search...',
|
||||
},
|
||||
default: 'Search...'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
term: '',
|
||||
timeout: null,
|
||||
results: [],
|
||||
resultsVisible: false,
|
||||
resultsVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filtered() {
|
||||
filtered () {
|
||||
return this.filter ? this.filter(this.results) : this.results
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
term(val) {
|
||||
term (val) {
|
||||
this.fetchResults(val)
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchResults(term) {
|
||||
fetchResults (term) {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.results = []
|
||||
if (term) {
|
||||
this.query(term).then((results) => {
|
||||
this.results = results
|
||||
})
|
||||
this.query(term).then((results) => { this.results = results })
|
||||
}
|
||||
}, debounceMilliseconds)
|
||||
},
|
||||
onInputClick() {
|
||||
onInputClick () {
|
||||
this.resultsVisible = true
|
||||
},
|
||||
onClickOutside() {
|
||||
onClickOutside () {
|
||||
this.resultsVisible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const AvatarList = {
|
||||
props: ['users'],
|
||||
computed: {
|
||||
slicedUsers() {
|
||||
slicedUsers () {
|
||||
return this.users ? this.users.slice(0, 15) : []
|
||||
},
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserAvatar,
|
||||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarList
|
||||
|
|
|
|||
|
|
@ -1,27 +1,30 @@
|
|||
export default {
|
||||
name: 'Badge',
|
||||
selector: '.badge',
|
||||
validInnerComponents: ['Text', 'Icon'],
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon'
|
||||
],
|
||||
variants: {
|
||||
notification: '.-notification',
|
||||
notification: '.-notification'
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--badgeNotification': 'color | --cRed',
|
||||
},
|
||||
'--badgeNotification': 'color | --cRed'
|
||||
}
|
||||
},
|
||||
{
|
||||
directives: {
|
||||
background: '--cGreen',
|
||||
},
|
||||
background: '--cGreen'
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'notification',
|
||||
directives: {
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
],
|
||||
background: '--cRed'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,24 @@
|
|||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: ['user'],
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
UserLink,
|
||||
UserLink
|
||||
},
|
||||
methods: {
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BasicUserCard
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
flex: 1 0;
|
||||
margin: 0;
|
||||
|
||||
--emoji-size: 1em;
|
||||
--emoji-size: 14px;
|
||||
|
||||
&-collapsed-content {
|
||||
margin-left: 0.7em;
|
||||
|
|
|
|||
|
|
@ -1,48 +1,40 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
|
||||
const BlockCard = {
|
||||
props: ['userId'],
|
||||
data () {
|
||||
return {
|
||||
progress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship() {
|
||||
relationship () {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
blocked() {
|
||||
blocked () {
|
||||
return this.relationship.blocking
|
||||
},
|
||||
blockExpiryAvailable() {
|
||||
return this.user.block_expires_at !== undefined
|
||||
},
|
||||
blockExpiry() {
|
||||
return this.user.block_expires_at == null
|
||||
? 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: {
|
||||
BasicUserCard,
|
||||
BasicUserCard
|
||||
},
|
||||
methods: {
|
||||
unblockUser() {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
unblockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
},
|
||||
blockUser() {
|
||||
if (this.blockExpirationSupported) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
}
|
||||
},
|
||||
},
|
||||
blockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BlockCard
|
||||
|
|
|
|||
|
|
@ -1,35 +1,33 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<div class="block-card-content-container">
|
||||
<span
|
||||
v-if="blocked && blockExpiryAvailable"
|
||||
class="alert neutral"
|
||||
>
|
||||
{{ blockExpiry }}
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
v-if="blocked"
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="unblockUser"
|
||||
>
|
||||
{{ $t('user_card.unblock') }}
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</template>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="blockUser"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.block') }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<UserTimedFilterModal
|
||||
ref="timedBlockDialog"
|
||||
:user="user"
|
||||
:is-mute="false"
|
||||
/>
|
||||
</teleport>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faEllipsisH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisH)
|
||||
library.add(
|
||||
faEllipsisH
|
||||
)
|
||||
|
||||
const BookmarkFolderCard = {
|
||||
props: ['folder', 'allBookmarks'],
|
||||
props: [
|
||||
'folder',
|
||||
'allBookmarks'
|
||||
],
|
||||
computed: {
|
||||
firstLetter() {
|
||||
firstLetter () {
|
||||
return this.folder ? this.folder.name[0] : null
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BookmarkFolderCard
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
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 apiService from '../../services/api/api.service'
|
||||
|
||||
const BookmarkFolderEdit = {
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
nameDraft: '',
|
||||
|
|
@ -13,75 +11,70 @@ const BookmarkFolderEdit = {
|
|||
emojiDraft: '',
|
||||
emojiUrlDraft: null,
|
||||
emojiPickerExpanded: false,
|
||||
reallyDelete: false,
|
||||
reallyDelete: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker,
|
||||
EmojiPicker
|
||||
},
|
||||
created() {
|
||||
created () {
|
||||
if (!this.id) return
|
||||
const credentials = this.$store.state.users.currentUser.credentials
|
||||
apiService.fetchBookmarkFolders({ credentials }).then((folders) => {
|
||||
const folder = folders.find((folder) => folder.id === this.id)
|
||||
if (!folder) return
|
||||
apiService.fetchBookmarkFolders({ credentials })
|
||||
.then((folders) => {
|
||||
const folder = folders.find(folder => folder.id === this.id)
|
||||
if (!folder) return
|
||||
|
||||
this.nameDraft = this.name = folder.name
|
||||
this.emojiDraft = this.emoji = folder.emoji
|
||||
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
||||
})
|
||||
this.nameDraft = this.name = folder.name
|
||||
this.emojiDraft = this.emoji = folder.emoji
|
||||
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
id () {
|
||||
return this.$route.params.id
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectEmoji(event) {
|
||||
selectEmoji (event) {
|
||||
this.emojiDraft = event.insertion
|
||||
this.emojiUrlDraft = event.insertionUrl
|
||||
},
|
||||
showEmojiPicker() {
|
||||
showEmojiPicker () {
|
||||
if (!this.emojiPickerExpanded) {
|
||||
this.$refs.picker.showPicker()
|
||||
}
|
||||
},
|
||||
onShowPicker() {
|
||||
onShowPicker () {
|
||||
this.emojiPickerExpanded = true
|
||||
},
|
||||
onClosePicker() {
|
||||
onClosePicker () {
|
||||
this.emojiPickerExpanded = false
|
||||
},
|
||||
updateFolder() {
|
||||
useBookmarkFoldersStore()
|
||||
.updateBookmarkFolder({
|
||||
folderId: this.id,
|
||||
name: this.nameDraft,
|
||||
emoji: this.emojiDraft,
|
||||
})
|
||||
updateFolder () {
|
||||
this.$store.dispatch('setBookmarkFolder', { folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
})
|
||||
},
|
||||
createFolder() {
|
||||
useBookmarkFoldersStore()
|
||||
.createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
|
||||
createFolder () {
|
||||
this.$store.dispatch('createBookmarkFolder', { name: this.nameDraft, emoji: this.emojiDraft })
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
})
|
||||
.catch((e) => {
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
messageKey: 'bookmark_folders.error',
|
||||
messageArgs: [e.message],
|
||||
level: 'error',
|
||||
level: 'error'
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteFolder() {
|
||||
useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id })
|
||||
deleteFolder () {
|
||||
this.$store.dispatch('deleteBookmarkFolder', { folderId: this.id })
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BookmarkFolderEdit
|
||||
|
|
|
|||
|
|
@ -13,11 +13,10 @@
|
|||
icon="chevron-left"
|
||||
/>
|
||||
</button>
|
||||
<h1 class="title">
|
||||
<div class="title">
|
||||
<i18n-t
|
||||
v-if="id"
|
||||
keypath="bookmark_folders.editing_folder"
|
||||
scope="global"
|
||||
>
|
||||
<template #folderName>
|
||||
{{ name }}
|
||||
|
|
@ -26,9 +25,8 @@
|
|||
<i18n-t
|
||||
v-else
|
||||
keypath="bookmark_folders.creating_folder"
|
||||
scope="global"
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="input-wrap">
|
||||
|
|
|
|||
|
|
@ -1,28 +1,27 @@
|
|||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
||||
|
||||
const BookmarkFolders = {
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
isNew: false,
|
||||
isNew: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BookmarkFolderCard,
|
||||
BookmarkFolderCard
|
||||
},
|
||||
computed: {
|
||||
bookmarkFolders() {
|
||||
return useBookmarkFoldersStore().allFolders
|
||||
},
|
||||
bookmarkFolders () {
|
||||
return this.$store.state.bookmarkFolders.allFolders
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelNewFolder() {
|
||||
cancelNewFolder () {
|
||||
this.isNew = false
|
||||
},
|
||||
newFolder() {
|
||||
newFolder () {
|
||||
this.isNew = true
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BookmarkFolders
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="Bookmark-folders panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h1 class="title">
|
||||
<div class="title">
|
||||
{{ $t('nav.bookmark_folders') }}
|
||||
</h1>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ name: 'bookmark-folder-new' }"
|
||||
class="button-default btn new-folder-button"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
|
||||
import { mapState } from 'vuex'
|
||||
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 = {
|
||||
props: ['showPin'],
|
||||
components: {
|
||||
NavigationEntry,
|
||||
NavigationEntry
|
||||
},
|
||||
computed: {
|
||||
...mapState(useBookmarkFoldersStore, {
|
||||
folders: getBookmarkFolderEntries,
|
||||
}),
|
||||
},
|
||||
...mapState({
|
||||
folders: getBookmarkFolderEntries
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default BookmarkFoldersMenuContent
|
||||
|
|
|
|||
|
|
@ -7,12 +7,10 @@
|
|||
label: 'nav.all_bookmarks',
|
||||
icon: 'bookmark'
|
||||
}"
|
||||
:show-pin="showPin"
|
||||
/>
|
||||
<NavigationEntry
|
||||
v-for="item in folders"
|
||||
:key="item.id"
|
||||
:show-pin="showPin"
|
||||
:item="item"
|
||||
/>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,32 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
|
||||
const Bookmarks = {
|
||||
created() {
|
||||
created () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('startFetchingTimeline', {
|
||||
timeline: 'bookmarks',
|
||||
bookmarkFolderId: this.folderId || null,
|
||||
})
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
|
||||
},
|
||||
components: {
|
||||
Timeline,
|
||||
Timeline
|
||||
},
|
||||
computed: {
|
||||
folderId() {
|
||||
folderId () {
|
||||
return this.$route.params.id
|
||||
},
|
||||
timeline() {
|
||||
timeline () {
|
||||
return this.$store.state.statuses.timelines.bookmarks
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
folderId() {
|
||||
folderId () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
|
||||
this.$store.dispatch('startFetchingTimeline', {
|
||||
timeline: 'bookmarks',
|
||||
bookmarkFolderId: this.folderId || null,
|
||||
})
|
||||
},
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default Bookmarks
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ export default {
|
|||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
textColor: '$mod(--parent 10)',
|
||||
textAuto: 'no-auto',
|
||||
},
|
||||
},
|
||||
],
|
||||
textColor: '$mod(--parent, 10)',
|
||||
textAuto: 'no-auto'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<template>
|
||||
<Timeline
|
||||
:title="$t('nav.bubble')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'bubble'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./bubble_timeline.js"></script>
|
||||
|
|
@ -9,167 +9,104 @@ export default {
|
|||
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants
|
||||
// normal: '' // normal state is implicitly added, it is always included
|
||||
toggled: '.toggled',
|
||||
focused: ':focus-within',
|
||||
pressed: ':active',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
disabled: ':disabled',
|
||||
hover: ':hover:not(:disabled)',
|
||||
focused: ':focus-within',
|
||||
disabled: ':disabled'
|
||||
},
|
||||
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
|
||||
variants: {
|
||||
// Variants save on computation time since adding new variant just adds one more "set".
|
||||
// normal: '', // you can override normal variant, it will be appenended to the main class
|
||||
danger: '.-danger',
|
||||
transparent: '.-transparent',
|
||||
danger: '.danger'
|
||||
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
|
||||
// This (currently) is further multipled by number of places where component can exist.
|
||||
},
|
||||
editor: {
|
||||
aspect: '2 / 1',
|
||||
},
|
||||
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
|
||||
validInnerComponents: ['Text', 'Icon'],
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon'
|
||||
],
|
||||
// Default rules, used as "default theme", essentially.
|
||||
defaultRules: [
|
||||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--buttonDefaultHoverGlow': 'shadow | 0 0 1 2 --text / 0.4',
|
||||
'--buttonDefaultFocusGlow': 'shadow | 0 0 1 2 --link / 0.5',
|
||||
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
|
||||
'--buttonDefaultBevel':
|
||||
'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)',
|
||||
'--buttonPressedBevel':
|
||||
'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)',
|
||||
},
|
||||
'--defaultButtonHoverGlow': 'shadow | 0 0 4 --text',
|
||||
'--defaultButtonShadow': 'shadow | 0 0 2 #000000',
|
||||
'--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2), $borderSide(#000000, bottom, 0.2)',
|
||||
'--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
|
||||
}
|
||||
},
|
||||
{
|
||||
// component: 'Button', // no need to specify components every time unless you're specifying how other component should look
|
||||
// like within it
|
||||
directives: {
|
||||
background: '--fg',
|
||||
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
|
||||
roundness: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'danger',
|
||||
directives: {
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'transparent',
|
||||
directives: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
variant: 'transparent',
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
variant: 'transparent',
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text',
|
||||
},
|
||||
shadow: ['--defaultButtonShadow', '--defaultButtonBevel'],
|
||||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['focused'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel'],
|
||||
},
|
||||
shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['pressed'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
|
||||
},
|
||||
shadow: ['--defaultButtonShadow', '--pressedButtonBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['pressed', 'hover'],
|
||||
state: ['hover', 'pressed'],
|
||||
directives: {
|
||||
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow'],
|
||||
},
|
||||
shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
|
||||
},
|
||||
background: '--inheritedBackground,-14.2',
|
||||
shadow: ['--defaultButtonShadow', '--pressedButtonBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'hover'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'hover', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'disabled'],
|
||||
directives: {
|
||||
background: '$blend(--accent 0.25 --parent)',
|
||||
shadow: ['--buttonPressedBevel'],
|
||||
},
|
||||
background: '--inheritedBackground,-14.2',
|
||||
shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||
shadow: ['--buttonDefaultBevel'],
|
||||
},
|
||||
background: '$blend(--inheritedBackground, 0.25, --parent)',
|
||||
shadow: ['--defaultButtonBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
state: ['disabled'],
|
||||
state: ['disabled']
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
state: ['disabled'],
|
||||
state: ['disabled']
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
],
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,92 +1,96 @@
|
|||
export default {
|
||||
name: 'ButtonUnstyled',
|
||||
selector: '.button-unstyled',
|
||||
notEditable: true,
|
||||
transparent: true,
|
||||
states: {
|
||||
toggled: '.toggled',
|
||||
disabled: ':disabled',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
focused: ':focus-within:not(:is(:focus-visible))',
|
||||
hover: ':hover:not(:disabled)',
|
||||
focused: ':focus-within'
|
||||
},
|
||||
validInnerComponents: ['Text', 'Link', 'Icon', 'Badge'],
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Badge'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
shadow: [],
|
||||
},
|
||||
background: '#ffffff',
|
||||
opacity: 0,
|
||||
shadow: []
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['hover'],
|
||||
state: ['hover']
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled'],
|
||||
state: ['toggled']
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'hover'],
|
||||
state: ['toggled', 'hover']
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'focused'],
|
||||
state: ['toggled', 'focused']
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'focused', 'hover'],
|
||||
state: ['toggled', 'focused', 'hover']
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['disabled'],
|
||||
state: ['disabled']
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['disabled'],
|
||||
state: ['disabled']
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
],
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
import _ from 'lodash'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import chatService from '../../services/chat_service/chat_service.js'
|
||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import ChatMessage from '../chat_message/chat_message.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import chatService from '../../services/chat_service/chat_service.js'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
getNewTopPosition,
|
||||
getScrollPosition,
|
||||
isBottomedOut,
|
||||
isScrollable,
|
||||
} from './chat_layout_utils.js'
|
||||
faChevronDown,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||
|
||||
library.add(faChevronDown, faChevronLeft)
|
||||
library.add(
|
||||
faChevronDown,
|
||||
faChevronLeft
|
||||
)
|
||||
|
||||
const BOTTOMED_OUT_OFFSET = 10
|
||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
|
||||
|
|
@ -30,95 +29,76 @@ const Chat = {
|
|||
components: {
|
||||
ChatMessage,
|
||||
ChatTitle,
|
||||
PostStatusForm,
|
||||
PostStatusForm
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
jumpToBottomButtonVisible: false,
|
||||
hoveredMessageChainId: undefined,
|
||||
lastScrollPosition: {},
|
||||
scrollableContainerHeight: '100%',
|
||||
errorLoadingChat: false,
|
||||
messageRetriers: {},
|
||||
messageRetriers: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
created () {
|
||||
this.startFetching()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
mounted() {
|
||||
mounted () {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
if (typeof document.hidden !== 'undefined') {
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
this.handleVisibilityChange,
|
||||
false,
|
||||
)
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
})
|
||||
},
|
||||
unmounted() {
|
||||
unmounted () {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (typeof document.hidden !== 'undefined')
|
||||
document.removeEventListener(
|
||||
'visibilitychange',
|
||||
this.handleVisibilityChange,
|
||||
false,
|
||||
)
|
||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
this.$store.dispatch('clearCurrentChat')
|
||||
},
|
||||
computed: {
|
||||
recipient() {
|
||||
recipient () {
|
||||
return this.currentChat && this.currentChat.account
|
||||
},
|
||||
recipientId() {
|
||||
recipientId () {
|
||||
return this.$route.params.recipient_id
|
||||
},
|
||||
formPlaceholder() {
|
||||
formPlaceholder () {
|
||||
if (this.recipient) {
|
||||
return this.$t('chats.message_user', {
|
||||
nickname: this.recipient.screen_name_ui,
|
||||
})
|
||||
return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
chatViewItems() {
|
||||
chatViewItems () {
|
||||
return chatService.getView(this.currentChatMessageService)
|
||||
},
|
||||
newMessageCount() {
|
||||
return (
|
||||
this.currentChatMessageService &&
|
||||
this.currentChatMessageService.newMessageCount
|
||||
)
|
||||
newMessageCount () {
|
||||
return this.currentChatMessageService && this.currentChatMessageService.newMessageCount
|
||||
},
|
||||
streamingEnabled() {
|
||||
return (
|
||||
this.mergedConfig.useStreamingApi &&
|
||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
)
|
||||
streamingEnabled () {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
},
|
||||
...mapGetters([
|
||||
'currentChat',
|
||||
'currentChatMessageService',
|
||||
'findOpenedChatByRecipientId',
|
||||
'mergedConfig',
|
||||
'mergedConfig'
|
||||
]),
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
backendInteractor: state => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
|
||||
mobileLayout: state => state.interface.layoutType === 'mobile',
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
chatViewItems() {
|
||||
chatViewItems () {
|
||||
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
||||
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||
|
|
@ -131,23 +111,23 @@ const Chat = {
|
|||
$route: function () {
|
||||
this.startFetching()
|
||||
},
|
||||
mastoUserSocketStatus(newValue) {
|
||||
mastoUserSocketStatus (newValue) {
|
||||
if (newValue === WSConnectionStatus.JOINED) {
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
|
||||
onMessageHover({ isHovered, messageChainId }) {
|
||||
onMessageHover ({ isHovered, messageChainId }) {
|
||||
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
||||
},
|
||||
onFilesDropped() {
|
||||
onFilesDropped () {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
})
|
||||
},
|
||||
handleVisibilityChange() {
|
||||
handleVisibilityChange () {
|
||||
this.$nextTick(() => {
|
||||
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
||||
this.scrollDown({ forceRead: true })
|
||||
|
|
@ -155,7 +135,7 @@ const Chat = {
|
|||
})
|
||||
},
|
||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||
handleResize(opts = {}) {
|
||||
handleResize (opts = {}) {
|
||||
const { delayed = false } = opts
|
||||
|
||||
if (delayed) {
|
||||
|
|
@ -176,56 +156,40 @@ const Chat = {
|
|||
this.lastScrollPosition = getScrollPosition()
|
||||
})
|
||||
},
|
||||
scrollDown(options = {}) {
|
||||
scrollDown (options = {}) {
|
||||
const { behavior = 'auto', forceRead = false } = options
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior,
|
||||
})
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior })
|
||||
})
|
||||
if (forceRead) {
|
||||
this.readChat()
|
||||
}
|
||||
},
|
||||
readChat() {
|
||||
if (
|
||||
!(
|
||||
this.currentChatMessageService && this.currentChatMessageService.maxId
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (document.hidden) {
|
||||
return
|
||||
}
|
||||
readChat () {
|
||||
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
||||
if (document.hidden) { return }
|
||||
const lastReadId = this.currentChatMessageService.maxId
|
||||
this.$store.dispatch('readChat', {
|
||||
id: this.currentChat.id,
|
||||
lastReadId,
|
||||
lastReadId
|
||||
})
|
||||
},
|
||||
bottomedOut(offset) {
|
||||
bottomedOut (offset) {
|
||||
return isBottomedOut(offset)
|
||||
},
|
||||
reachedTop() {
|
||||
reachedTop () {
|
||||
return window.scrollY <= 0
|
||||
},
|
||||
cullOlderCheck() {
|
||||
cullOlderCheck () {
|
||||
window.setTimeout(() => {
|
||||
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||
this.$store.dispatch(
|
||||
'cullOlderMessages',
|
||||
this.currentChatMessageService.chatId,
|
||||
)
|
||||
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
|
||||
}
|
||||
}, 5000)
|
||||
},
|
||||
handleScroll: _.throttle(function () {
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
if (!this.currentChat) {
|
||||
return
|
||||
}
|
||||
if (!this.currentChat) { return }
|
||||
|
||||
if (this.reachedTop()) {
|
||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||
|
|
@ -245,27 +209,22 @@ const Chat = {
|
|||
this.jumpToBottomButtonVisible = true
|
||||
}
|
||||
}, 200),
|
||||
handleScrollUp(positionBeforeLoading) {
|
||||
handleScrollUp (positionBeforeLoading) {
|
||||
const positionAfterLoading = getScrollPosition()
|
||||
window.scrollTo({
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading)
|
||||
})
|
||||
},
|
||||
fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||
fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||
const chatMessageService = this.currentChatMessageService
|
||||
if (!chatMessageService) {
|
||||
return
|
||||
}
|
||||
if (fetchLatest && this.streamingEnabled) {
|
||||
return
|
||||
}
|
||||
if (!chatMessageService) { return }
|
||||
if (fetchLatest && this.streamingEnabled) { return }
|
||||
|
||||
const chatId = chatMessageService.chatId
|
||||
const fetchOlderMessages = !!maxId
|
||||
const sinceId = fetchLatest && chatMessageService.maxId
|
||||
|
||||
return this.backendInteractor
|
||||
.chatMessages({ id: chatId, maxId, sinceId })
|
||||
return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
|
||||
.then((messages) => {
|
||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||
if (isFirstFetch) {
|
||||
|
|
@ -273,34 +232,28 @@ const Chat = {
|
|||
}
|
||||
|
||||
const positionBeforeUpdate = getScrollPosition()
|
||||
this.$store
|
||||
.dispatch('addChatMessages', { chatId, messages })
|
||||
.then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
|
||||
// In vertical screens, the first batch of fetched messages may not always take the
|
||||
// full height of the scrollable container.
|
||||
// If this is the case, we want to fetch the messages until the scrollable container
|
||||
// is fully populated so that the user has the ability to scroll up and load the history.
|
||||
if (!isScrollable() && messages.length > 0) {
|
||||
this.fetchChat({
|
||||
maxId: this.currentChatMessageService.minId,
|
||||
})
|
||||
}
|
||||
})
|
||||
// In vertical screens, the first batch of fetched messages may not always take the
|
||||
// full height of the scrollable container.
|
||||
// If this is the case, we want to fetch the messages until the scrollable container
|
||||
// is fully populated so that the user has the ability to scroll up and load the history.
|
||||
if (!isScrollable() && messages.length > 0) {
|
||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
async startFetching() {
|
||||
async startFetching () {
|
||||
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
||||
if (!chat) {
|
||||
try {
|
||||
chat = await this.backendInteractor.getOrCreateChat({
|
||||
accountId: this.recipientId,
|
||||
})
|
||||
chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
|
||||
} catch (e) {
|
||||
console.error('Error creating or getting a chat', e)
|
||||
this.errorLoadingChat = true
|
||||
|
|
@ -314,14 +267,13 @@ const Chat = {
|
|||
this.doStartFetching()
|
||||
}
|
||||
},
|
||||
doStartFetching() {
|
||||
doStartFetching () {
|
||||
this.$store.dispatch('startFetchingCurrentChat', {
|
||||
fetcher: () =>
|
||||
promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000),
|
||||
fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
||||
})
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
},
|
||||
handleAttachmentPosting() {
|
||||
handleAttachmentPosting () {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||
|
|
@ -329,11 +281,11 @@ const Chat = {
|
|||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
},
|
||||
sendMessage({ status, media, idempotencyKey }) {
|
||||
sendMessage ({ status, media, idempotencyKey }) {
|
||||
const params = {
|
||||
id: this.currentChat.id,
|
||||
content: status,
|
||||
idempotencyKey,
|
||||
idempotencyKey
|
||||
}
|
||||
|
||||
if (media[0]) {
|
||||
|
|
@ -345,72 +297,52 @@ const Chat = {
|
|||
chatId: this.currentChat.id,
|
||||
content: status,
|
||||
userId: this.currentUser.id,
|
||||
idempotencyKey,
|
||||
idempotencyKey
|
||||
})
|
||||
|
||||
this.$store
|
||||
.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
messages: [fakeMessage],
|
||||
})
|
||||
.then(() => {
|
||||
this.handleAttachmentPosting()
|
||||
})
|
||||
|
||||
return this.doSendMessage({
|
||||
params,
|
||||
fakeMessage,
|
||||
retriesLeft: MAX_RETRIES,
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
messages: [fakeMessage]
|
||||
}).then(() => {
|
||||
this.handleAttachmentPosting()
|
||||
})
|
||||
|
||||
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
|
||||
},
|
||||
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
if (retriesLeft <= 0) return
|
||||
|
||||
this.backendInteractor
|
||||
.sendChatMessage(params)
|
||||
.then((data) => {
|
||||
this.backendInteractor.sendChatMessage(params)
|
||||
.then(data => {
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
updateMaxId: false,
|
||||
messages: [{ ...data, fakeId: fakeMessage.id }],
|
||||
messages: [{ ...data, fakeId: fakeMessage.id }]
|
||||
})
|
||||
|
||||
return data
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error('Error sending message', error)
|
||||
this.$store.dispatch('handleMessageError', {
|
||||
chatId: this.currentChat.id,
|
||||
fakeId: fakeMessage.id,
|
||||
isRetry: retriesLeft !== MAX_RETRIES,
|
||||
isRetry: retriesLeft !== MAX_RETRIES
|
||||
})
|
||||
if (
|
||||
(error.statusCode >= 500 && error.statusCode < 600) ||
|
||||
error.message === 'Failed to fetch'
|
||||
) {
|
||||
this.messageRetriers[fakeMessage.id] = setTimeout(
|
||||
() => {
|
||||
this.doSendMessage({
|
||||
params,
|
||||
fakeMessage,
|
||||
retriesLeft: retriesLeft - 1,
|
||||
})
|
||||
},
|
||||
1000 * 2 ** (MAX_RETRIES - retriesLeft),
|
||||
)
|
||||
if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') {
|
||||
this.messageRetriers[fakeMessage.id] = setTimeout(() => {
|
||||
this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 })
|
||||
}, 1000 * (2 ** (MAX_RETRIES - retriesLeft)))
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
return Promise.resolve(fakeMessage)
|
||||
},
|
||||
goBack() {
|
||||
this.$router.push({
|
||||
name: 'chats',
|
||||
params: { username: this.currentUser.screen_name },
|
||||
})
|
||||
},
|
||||
},
|
||||
goBack () {
|
||||
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Chat
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
export default {
|
||||
name: 'Chat',
|
||||
selector: '.chat-message-list',
|
||||
validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'],
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Avatar',
|
||||
'ChatMessage'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg',
|
||||
blur: '5px',
|
||||
},
|
||||
},
|
||||
],
|
||||
blur: '5px'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@
|
|||
:disable-sensitivity-checkbox="true"
|
||||
:disable-submit="errorLoadingChat || !currentChat"
|
||||
:disable-preview="true"
|
||||
:disable-draft="true"
|
||||
:optimistic-posting="true"
|
||||
:post-handler="sendMessage"
|
||||
:submit-on-enter="!mobileLayout"
|
||||
|
|
@ -95,4 +94,6 @@
|
|||
</template>
|
||||
|
||||
<script src="./chat.js"></script>
|
||||
<style src="./chat.scss" lang="scss" />
|
||||
<style lang="scss">
|
||||
@import "./chat";
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,14 @@ export const getScrollPosition = () => {
|
|||
return {
|
||||
scrollTop: window.scrollY,
|
||||
scrollHeight: document.documentElement.scrollHeight,
|
||||
offsetHeight: window.innerHeight,
|
||||
offsetHeight: window.innerHeight
|
||||
}
|
||||
}
|
||||
|
||||
// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
|
||||
// Takes two scroll positions, before and after the update.
|
||||
export const getNewTopPosition = (previousPosition, newPosition) => {
|
||||
return (
|
||||
previousPosition.scrollTop +
|
||||
(newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||
)
|
||||
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||
}
|
||||
|
||||
export const isBottomedOut = (offset = 0) => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue