Merge branch 'tusooa/vite' into shigusegubu-themes3
2
.gitattributes
vendored
|
|
@ -1 +1 @@
|
||||||
/build/webpack.prod.conf.js export-subst
|
/build/commit_hash.js export-subst
|
||||||
|
|
|
||||||
3
.gitignore
vendored
|
|
@ -7,5 +7,6 @@ test/e2e/reports
|
||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
.idea/
|
.idea/
|
||||||
config/local.json
|
config/local.json
|
||||||
static/emoji.json
|
src/assets/emoji.json
|
||||||
logs/
|
logs/
|
||||||
|
__screenshots__/
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,15 @@ test:
|
||||||
APT_CACHE_DIR: apt-cache
|
APT_CACHE_DIR: apt-cache
|
||||||
script:
|
script:
|
||||||
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
- mkdir -pv $APT_CACHE_DIR && apt-get -qq update
|
||||||
- apt install firefox-esr -y --no-install-recommends
|
|
||||||
- firefox --version
|
|
||||||
- yarn
|
- yarn
|
||||||
- yarn unit
|
- 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
|
||||||
|
|
||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
// https://github.com/shelljs/shelljs
|
|
||||||
import('./check-versions.mjs').then(m => m.default())
|
|
||||||
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.info(
|
|
||||||
' 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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
18
build/commit_hash.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
40
build/copy_plugin.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import serveStatic from 'serve-static'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import { cp } from 'node:fs/promises'
|
||||||
|
|
||||||
|
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.log(`Copying '${inFs}' to ${copyTarget}...`)
|
||||||
|
await cp(inFs, copyTarget, { recursive: true })
|
||||||
|
console.log('Done.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default copyPlugin
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
/* 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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import('./check-versions.mjs').then(m => m.default())
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
options.pathFilter = context
|
|
||||||
app.use(proxyMiddleware.createProxyMiddleware(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.error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var uri = 'http://localhost:' + port
|
|
||||||
console.info('Listening at ' + uri + '\n')
|
|
||||||
// opn(uri)
|
|
||||||
})
|
|
||||||
30
build/service_worker_messages.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { languages, langCodeToJsonName } from '../src/i18n/languages.js'
|
||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { dirname, resolve } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
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
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
163
build/sw_plugin.js
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { dirname, resolve } from 'node:path'
|
||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { build } from 'vite'
|
||||||
|
import * as esbuild from 'esbuild'
|
||||||
|
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)))
|
||||||
|
|
||||||
|
export const devSwPlugin = ({
|
||||||
|
swSrc,
|
||||||
|
swDest,
|
||||||
|
transformSW,
|
||||||
|
alias
|
||||||
|
}) => {
|
||||||
|
const swFullSrc = resolve(projectRoot, swSrc)
|
||||||
|
const esbuildAlias = {}
|
||||||
|
Object.entries(alias).forEach(([source, dest]) => {
|
||||||
|
esbuildAlias[source] = dest.startsWith('/') ? projectRoot + dest : dest
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'dev-sw-plugin',
|
||||||
|
apply: 'serve',
|
||||||
|
configResolved (conf) {
|
||||||
|
},
|
||||||
|
resolveId (id) {
|
||||||
|
const name = id.startsWith('/') ? id.slice(1) : id
|
||||||
|
if (name === swDest) {
|
||||||
|
return swFullSrc
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
async load (id) {
|
||||||
|
if (id === swFullSrc) {
|
||||||
|
return readFile(swFullSrc, 'utf-8')
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeBundle: {
|
||||||
|
order: 'post',
|
||||||
|
sequential: true,
|
||||||
|
async handler () {
|
||||||
|
console.log('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,27 +1,23 @@
|
||||||
|
|
||||||
module.exports = {
|
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' }
|
||||||
updateEmoji () {
|
import fs from 'fs'
|
||||||
const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group')
|
|
||||||
const fs = require('fs')
|
|
||||||
|
|
||||||
Object.keys(emojis)
|
Object.keys(emojis)
|
||||||
.map(k => {
|
.map(k => {
|
||||||
emojis[k].map(e => {
|
emojis[k].map(e => {
|
||||||
delete e.unicode_version
|
delete e.unicode_version
|
||||||
delete e.emoji_version
|
delete e.emoji_version
|
||||||
delete e.skin_tone_support_unicode_version
|
delete e.skin_tone_support_unicode_version
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = {}
|
const res = {}
|
||||||
Object.keys(emojis)
|
Object.keys(emojis)
|
||||||
.map(k => {
|
.map(k => {
|
||||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||||
res[groupId] = emojis[k]
|
res[groupId] = emojis[k]
|
||||||
})
|
})
|
||||||
|
|
||||||
console.info('Updating emojis...')
|
console.info('Updating emojis...')
|
||||||
fs.writeFileSync('static/emoji.json', JSON.stringify(res))
|
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res))
|
||||||
console.info('Done.')
|
console.info('Done.')
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
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: /\.scss$/,
|
|
||||||
use: generateLoaders([
|
|
||||||
'css-loader',
|
|
||||||
'postcss-loader',
|
|
||||||
{
|
|
||||||
loader: 'sass-loader',
|
|
||||||
options: {
|
|
||||||
api: 'modern'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate loaders for standalone style files (outside of .vue)
|
|
||||||
exports.styleLoaders = function (options) {
|
|
||||||
return exports.cssLoaders(options)
|
|
||||||
}
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
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({
|
|
||||||
formatter: require('eslint-formatter-friendly'),
|
|
||||||
overrideConfigFile: path.resolve(__dirname, '..', 'eslint.config.mjs'),
|
|
||||||
configType: 'flat'
|
|
||||||
}),
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
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,
|
|
||||||
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': 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
|
|
||||||
})
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
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,
|
|
||||||
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': 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/boot-improvements.change
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Speed up initial boot.
|
||||||
1
changelog.d/cover-image-path.change
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
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
|
||||||
1
changelog.d/draft-save.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix draft saving when auto-save is off
|
||||||
1
changelog.d/no-non-esm-script.remove
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
BREAKING: drop support for browsers that do not support `<script type="module">`
|
||||||
1
changelog.d/no-prod-css-source-map.remove
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
BREAKING: css source map does not work in production (see https://github.com/vitejs/vite/issues/2830 )
|
||||||
1
changelog.d/post-more-actions-label.fix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add text label for more actions button in post status form
|
||||||
1
changelog.d/proper-static-emoji.change
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
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)
|
||||||
0
changelog.d/unify-vuex-modules-import.skip
Normal file
0
changelog.d/vite.skip
Normal file
|
|
@ -1,6 +0,0 @@
|
||||||
var merge = require('webpack-merge')
|
|
||||||
var prodEnv = require('./prod.env')
|
|
||||||
|
|
||||||
module.exports = merge(prodEnv, {
|
|
||||||
NODE_ENV: '"development"'
|
|
||||||
})
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
// 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.info('Using local dev server settings (/config/local.json):')
|
|
||||||
console.info(JSON.stringify(settings, null, 2))
|
|
||||||
} catch (e) {
|
|
||||||
console.info('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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
NODE_ENV: '"production"'
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
var merge = require('webpack-merge')
|
|
||||||
var devEnv = require('./dev.env')
|
|
||||||
|
|
||||||
module.exports = merge(devEnv, {
|
|
||||||
NODE_ENV: '"testing"'
|
|
||||||
})
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default [
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
...globals.mocha,
|
...globals.vitest,
|
||||||
...globals.chai,
|
...globals.chai,
|
||||||
...globals.commonjs,
|
...globals.commonjs,
|
||||||
...globals.serviceworker
|
...globals.serviceworker
|
||||||
|
|
|
||||||
10
index.html
|
|
@ -3,6 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||||
|
<link rel="preload" href="/static/config.json" as="fetch" crossorigin />
|
||||||
|
<link rel="preload" href="/api/pleroma/frontend_configurations" as="fetch" crossorigin />
|
||||||
|
<link rel="preload" href="/nodeinfo/2.1.json" as="fetch" crossorigin />
|
||||||
|
<link rel="preload" href="/api/v1/instance" as="fetch" crossorigin />
|
||||||
|
<link rel="preload" href="/static/pleromatan_apology_fox_small.webp" as="image" />
|
||||||
<!-- putting styles here to avoid having to wait for styles to load up -->
|
<!-- putting styles here to avoid having to wait for styles to load up -->
|
||||||
<style id="splashscreen">
|
<style id="splashscreen">
|
||||||
#splash {
|
#splash {
|
||||||
|
|
@ -148,7 +153,7 @@
|
||||||
<div class="chunk" id="chunk-E">
|
<div class="chunk" id="chunk-E">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img id="mascot" src="/static/pleromatan_apology.png">
|
<img id="mascot" src="/static/pleromatan_apology_small.webp">
|
||||||
</div>
|
</div>
|
||||||
<div id="status" class="css-ok">
|
<div id="status" class="css-ok">
|
||||||
<!-- (。>﹏<) -->
|
<!-- (。>﹏<) -->
|
||||||
|
|
@ -162,6 +167,7 @@
|
||||||
<div id="app" class="hidden"></div>
|
<div id="app" class="hidden"></div>
|
||||||
<div id="modal"></div>
|
<div id="modal"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
<div id="popovers" />
|
<div id="popovers"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
50
package.json
|
|
@ -5,10 +5,11 @@
|
||||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
|
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node build/dev-server.js",
|
"dev": "node build/update-emoji.js && vite dev",
|
||||||
"build": "node build/build.js",
|
"build": "node build/update-emoji.js && vite build",
|
||||||
"unit": "karma start test/unit/karma.conf.js --single-run",
|
"unit": "node build/update-emoji.js && vitest --run",
|
||||||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
"unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
|
||||||
|
"unit:watch": "node build/update-emoji.js && vitest",
|
||||||
"e2e": "node test/e2e/runner.js",
|
"e2e": "node test/e2e/runner.js",
|
||||||
"test": "yarn run unit && yarn run e2e",
|
"test": "yarn run unit && yarn run e2e",
|
||||||
"stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
|
"stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
|
||||||
|
|
@ -27,6 +28,7 @@
|
||||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
|
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
|
||||||
"@vuelidate/core": "2.0.3",
|
"@vuelidate/core": "2.0.3",
|
||||||
"@vuelidate/validators": "2.0.4",
|
"@vuelidate/validators": "2.0.4",
|
||||||
|
"@web3-storage/parse-link-header": "^3.1.0",
|
||||||
"body-scroll-lock": "3.1.5",
|
"body-scroll-lock": "3.1.5",
|
||||||
"chromatism": "3.0.0",
|
"chromatism": "3.0.0",
|
||||||
"click-outside-vue3": "4.0.1",
|
"click-outside-vue3": "4.0.1",
|
||||||
|
|
@ -56,23 +58,22 @@
|
||||||
"@babel/plugin-transform-runtime": "7.26.9",
|
"@babel/plugin-transform-runtime": "7.26.9",
|
||||||
"@babel/preset-env": "7.26.9",
|
"@babel/preset-env": "7.26.9",
|
||||||
"@babel/register": "7.25.9",
|
"@babel/register": "7.25.9",
|
||||||
"@intlify/vue-i18n-loader": "5.0.1",
|
|
||||||
"@ungap/event-target": "0.2.4",
|
"@ungap/event-target": "0.2.4",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||||
|
"@vitest/browser": "^3.0.7",
|
||||||
|
"@vitest/ui": "^3.0.7",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||||
"@vue/babel-plugin-jsx": "1.2.5",
|
"@vue/babel-plugin-jsx": "1.2.5",
|
||||||
"@vue/compiler-sfc": "3.5.13",
|
"@vue/compiler-sfc": "3.5.13",
|
||||||
"@vue/test-utils": "2.4.6",
|
"@vue/test-utils": "2.4.6",
|
||||||
"autoprefixer": "10.4.20",
|
"autoprefixer": "10.4.20",
|
||||||
"babel-loader": "9.2.1",
|
|
||||||
"babel-plugin-lodash": "3.3.4",
|
"babel-plugin-lodash": "3.3.4",
|
||||||
"chai": "4.5.0",
|
"chai": "4.5.0",
|
||||||
"chalk": "5.4.1",
|
"chalk": "5.4.1",
|
||||||
"chromedriver": "133.0.2",
|
"chromedriver": "133.0.2",
|
||||||
"connect-history-api-fallback": "2.0.0",
|
"connect-history-api-fallback": "2.0.0",
|
||||||
"copy-webpack-plugin": "12.0.2",
|
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"css-loader": "7.1.2",
|
|
||||||
"css-minimizer-webpack-plugin": "7.0.0",
|
|
||||||
"custom-event-polyfill": "1.0.7",
|
"custom-event-polyfill": "1.0.7",
|
||||||
"eslint": "9.20.1",
|
"eslint": "9.20.1",
|
||||||
"eslint-config-standard": "17.1.0",
|
"eslint-config-standard": "17.1.0",
|
||||||
|
|
@ -81,38 +82,23 @@
|
||||||
"eslint-plugin-n": "17.15.1",
|
"eslint-plugin-n": "17.15.1",
|
||||||
"eslint-plugin-promise": "7.2.1",
|
"eslint-plugin-promise": "7.2.1",
|
||||||
"eslint-plugin-vue": "9.32.0",
|
"eslint-plugin-vue": "9.32.0",
|
||||||
"eslint-webpack-plugin": "4.2.0",
|
|
||||||
"eventsource-polyfill": "0.9.6",
|
"eventsource-polyfill": "0.9.6",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"function-bind": "1.1.2",
|
"function-bind": "1.1.2",
|
||||||
"html-webpack-plugin": "5.6.3",
|
|
||||||
"http-proxy-middleware": "3.0.3",
|
"http-proxy-middleware": "3.0.3",
|
||||||
"iso-639-1": "3.1.5",
|
"iso-639-1": "3.1.5",
|
||||||
"json-loader": "0.5.7",
|
|
||||||
"karma": "6.4.4",
|
|
||||||
"karma-coverage": "2.2.1",
|
|
||||||
"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.4.0",
|
|
||||||
"karma-spec-reporter": "0.0.36",
|
|
||||||
"karma-webpack": "5.0.1",
|
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mini-css-extract-plugin": "2.9.2",
|
|
||||||
"mocha": "11.1.0",
|
|
||||||
"nightwatch": "2.6.25",
|
"nightwatch": "2.6.25",
|
||||||
"opn": "5.5.0",
|
"opn": "5.5.0",
|
||||||
"ora": "0.4.1",
|
"ora": "0.4.1",
|
||||||
|
"playwright": "1.49.1",
|
||||||
"postcss": "8.5.2",
|
"postcss": "8.5.2",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-loader": "7.3.4",
|
|
||||||
"postcss-scss": "^4.0.6",
|
"postcss-scss": "^4.0.6",
|
||||||
"sass": "1.85.0",
|
"sass": "1.85.0",
|
||||||
"sass-loader": "13.3.3",
|
|
||||||
"selenium-server": "3.141.59",
|
"selenium-server": "3.141.59",
|
||||||
"semver": "7.7.1",
|
"semver": "7.7.1",
|
||||||
"serviceworker-webpack5-plugin": "2.0.0",
|
"serve-static": "1.16.2",
|
||||||
"shelljs": "0.8.5",
|
"shelljs": "0.8.5",
|
||||||
"sinon": "15.2.0",
|
"sinon": "15.2.0",
|
||||||
"sinon-chai": "3.7.0",
|
"sinon-chai": "3.7.0",
|
||||||
|
|
@ -122,14 +108,12 @@
|
||||||
"stylelint-config-recommended-vue": "^1.4.0",
|
"stylelint-config-recommended-vue": "^1.4.0",
|
||||||
"stylelint-config-standard": "29.0.0",
|
"stylelint-config-standard": "29.0.0",
|
||||||
"stylelint-rscss": "0.4.0",
|
"stylelint-rscss": "0.4.0",
|
||||||
"stylelint-webpack-plugin": "^3.3.0",
|
"vite": "^6.1.0",
|
||||||
"vue-loader": "17.4.2",
|
"vite-plugin-eslint2": "^5.0.3",
|
||||||
"vue-style-loader": "4.1.3",
|
"vite-plugin-stylelint": "^6.0.0",
|
||||||
"webpack": "5.97.1",
|
"vitest": "^3.0.7"
|
||||||
"webpack-dev-middleware": "3.7.3",
|
|
||||||
"webpack-hot-middleware": "2.26.1",
|
|
||||||
"webpack-merge": "0.20.0"
|
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16.0.0"
|
"node": ">= 16.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
module.exports = {
|
import autoprefixer from 'autoprefixer'
|
||||||
|
|
||||||
|
export default {
|
||||||
plugins: [
|
plugins: [
|
||||||
require('autoprefixer')
|
autoprefixer
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
static/.gitignore → public/static/.gitignore
vendored
0
public/static/.gitkeep
Normal file
|
Before Width: | Height: | Size: 628 KiB After Width: | Height: | Size: 628 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 521 KiB After Width: | Height: | Size: 521 KiB |
BIN
public/static/pleromatan_apology_fox_small.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/static/pleromatan_apology_small.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
|
@ -931,7 +931,7 @@ option {
|
||||||
|
|
||||||
#splash {
|
#splash {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 2s;
|
transition: opacity 0.5s;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
|
|
@ -960,7 +960,7 @@ option {
|
||||||
|
|
||||||
&.dead {
|
&.dead {
|
||||||
animation-name: dead;
|
animation-name: dead;
|
||||||
animation-duration: 2s;
|
animation-duration: 0.5s;
|
||||||
animation-iteration-count: 1;
|
animation-iteration-count: 1;
|
||||||
transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
|
transform: rotateX(90deg) rotateY(0) rotateZ(-45deg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
../../static/pleromatan_apology.png
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
../../static/pleromatan_apology_fox.png
|
|
||||||
|
|
@ -230,12 +230,16 @@ const getStickers = async ({ store }) => {
|
||||||
const getAppSecret = async ({ store }) => {
|
const getAppSecret = async ({ store }) => {
|
||||||
const { state, commit } = store
|
const { state, commit } = store
|
||||||
const { oauth, instance } = state
|
const { oauth, instance } = state
|
||||||
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
|
if (oauth.userToken) {
|
||||||
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||||
.then((token) => {
|
} else {
|
||||||
commit('setAppToken', token.access_token)
|
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
|
||||||
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
.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 resolveStaffAccounts = ({ store, accounts }) => {
|
||||||
|
|
@ -377,10 +381,9 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
||||||
getInstanceConfig({ store })
|
getInstanceConfig({ store })
|
||||||
]).catch(e => Promise.reject(e))
|
]).catch(e => Promise.reject(e))
|
||||||
|
|
||||||
await store.dispatch('loadDrafts')
|
|
||||||
|
|
||||||
// Start fetching things that don't need to block the UI
|
// Start fetching things that don't need to block the UI
|
||||||
store.dispatch('fetchMutes')
|
store.dispatch('fetchMutes')
|
||||||
|
store.dispatch('loadDrafts')
|
||||||
useAnnouncementsStore().startFetchingAnnouncements()
|
useAnnouncementsStore().startFetchingAnnouncements()
|
||||||
getTOS({ store })
|
getTOS({ store })
|
||||||
getStickers({ store })
|
getStickers({ store })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
import apiService from '../../services/api/api.service'
|
import apiService from '../../services/api/api.service'
|
||||||
|
import { useInterfaceStore } from 'src/stores/interface'
|
||||||
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||||
|
|
||||||
const BookmarkFolderEdit = {
|
const BookmarkFolderEdit = {
|
||||||
data () {
|
data () {
|
||||||
|
|
@ -52,18 +54,18 @@ const BookmarkFolderEdit = {
|
||||||
this.emojiPickerExpanded = false
|
this.emojiPickerExpanded = false
|
||||||
},
|
},
|
||||||
updateFolder () {
|
updateFolder () {
|
||||||
this.$store.dispatch('setBookmarkFolder', { folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
|
useBookmarkFoldersStore().updateBookmarkFolder({ folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$router.push({ name: 'bookmark-folders' })
|
this.$router.push({ name: 'bookmark-folders' })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
createFolder () {
|
createFolder () {
|
||||||
this.$store.dispatch('createBookmarkFolder', { name: this.nameDraft, emoji: this.emojiDraft })
|
useBookmarkFoldersStore().createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$router.push({ name: 'bookmark-folders' })
|
this.$router.push({ name: 'bookmark-folders' })
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
this.$store.useInterfaceStore().pushGlobalNotice({
|
useInterfaceStore().pushGlobalNotice({
|
||||||
messageKey: 'bookmark_folders.error',
|
messageKey: 'bookmark_folders.error',
|
||||||
messageArgs: [e.message],
|
messageArgs: [e.message],
|
||||||
level: 'error'
|
level: 'error'
|
||||||
|
|
@ -71,7 +73,7 @@ const BookmarkFolderEdit = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteFolder () {
|
deleteFolder () {
|
||||||
this.$store.dispatch('deleteBookmarkFolder', { folderId: this.id })
|
useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id })
|
||||||
this.$router.push({ name: 'bookmark-folders' })
|
this.$router.push({ name: 'bookmark-folders' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
||||||
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||||
|
|
||||||
const BookmarkFolders = {
|
const BookmarkFolders = {
|
||||||
data () {
|
data () {
|
||||||
|
|
@ -11,7 +12,7 @@ const BookmarkFolders = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookmarkFolders () {
|
bookmarkFolders () {
|
||||||
return this.$store.state.bookmarkFolders.allFolders
|
return useBookmarkFoldersStore().allFolders
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'pinia'
|
||||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||||
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
|
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
|
||||||
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||||
|
|
||||||
export const BookmarkFoldersMenuContent = {
|
export const BookmarkFoldersMenuContent = {
|
||||||
props: [
|
props: [
|
||||||
|
|
@ -10,7 +11,7 @@ export const BookmarkFoldersMenuContent = {
|
||||||
NavigationEntry
|
NavigationEntry
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState(useBookmarkFoldersStore, {
|
||||||
folders: getBookmarkFolderEntries
|
folders: getBookmarkFolderEntries
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export const getListEntries = store => store.allLists.map(list => ({
|
||||||
iconLetter: list.title[0]
|
iconLetter: list.title[0]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const getBookmarkFolderEntries = state => state.bookmarkFolders.allFolders.map(folder => ({
|
export const getBookmarkFolderEntries = store => store.allFolders.map(folder => ({
|
||||||
name: 'bookmark-folder-' + folder.id,
|
name: 'bookmark-folder-' + folder.id,
|
||||||
routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
|
routeObject: { name: 'bookmark-folder', params: { id: folder.id } },
|
||||||
labelRaw: folder.name,
|
labelRaw: folder.name,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useListsStore } from 'src/stores/lists'
|
import { useListsStore } from 'src/stores/lists'
|
||||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||||
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faUsers,
|
faUsers,
|
||||||
|
|
@ -52,8 +53,10 @@ const NavPanel = {
|
||||||
...mapPiniaState(useAnnouncementsStore, {
|
...mapPiniaState(useAnnouncementsStore, {
|
||||||
supportsAnnouncements: store => store.supportsAnnouncements
|
supportsAnnouncements: store => store.supportsAnnouncements
|
||||||
}),
|
}),
|
||||||
|
...mapPiniaState(useBookmarkFoldersStore, {
|
||||||
|
bookmarks: getBookmarkFolderEntries
|
||||||
|
}),
|
||||||
...mapState({
|
...mapState({
|
||||||
bookmarks: getBookmarkFolderEntries,
|
|
||||||
currentUser: state => state.users.currentUser,
|
currentUser: state => state.users.currentUser,
|
||||||
followRequestCount: state => state.api.followRequests.length,
|
followRequestCount: state => state.api.followRequests.length,
|
||||||
privateMode: state => state.instance.private,
|
privateMode: state => state.instance.private,
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,14 @@ const PostStatusForm = {
|
||||||
this.newStatus.hasPoll
|
this.newStatus.hasPoll
|
||||||
) && this.saveable
|
) && this.saveable
|
||||||
},
|
},
|
||||||
|
hasEmptyDraft () {
|
||||||
|
return this.newStatus.id && !(
|
||||||
|
this.newStatus.status ||
|
||||||
|
this.newStatus.spoilerText ||
|
||||||
|
this.newStatus.files?.length ||
|
||||||
|
this.newStatus.hasPoll
|
||||||
|
)
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig']),
|
...mapGetters(['mergedConfig']),
|
||||||
...mapState(useInterfaceStore, {
|
...mapState(useInterfaceStore, {
|
||||||
mobileLayout: store => store.mobileLayout
|
mobileLayout: store => store.mobileLayout
|
||||||
|
|
@ -784,7 +792,7 @@ const PostStatusForm = {
|
||||||
this.$emit('draft-done')
|
this.$emit('draft-done')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (this.newStatus.id) {
|
} else if (this.hasEmptyDraft) {
|
||||||
// There is a draft, but there is nothing in it, clear it
|
// There is a draft, but there is nothing in it, clear it
|
||||||
return this.abandonDraft()
|
return this.abandonDraft()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:offset="{ y: 5 }"
|
:offset="{ y: 5 }"
|
||||||
|
:trigger-attrs="{ 'aria-label': $t('post_status.more_post_actions') }"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_con
|
||||||
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
||||||
import StillImage from 'src/components/still-image/still-image.vue'
|
import StillImage from 'src/components/still-image/still-image.vue'
|
||||||
import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
|
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||||
|
import { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.js'
|
||||||
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
|
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
|
||||||
|
|
||||||
import './rich_content.scss'
|
import './rich_content.scss'
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
import FloatSetting from '../helpers/float_setting.vue'
|
import FloatSetting from '../helpers/float_setting.vue'
|
||||||
import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
|
import UnitSetting from '../helpers/unit_setting.vue'
|
||||||
|
import { defaultHorizontalUnits } from '../helpers/unit_setting.js'
|
||||||
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
||||||
import Preview from './theme_tab/theme_preview.vue'
|
import Preview from './theme_tab/theme_preview.vue'
|
||||||
import FontControl from 'src/components/font_control/font_control.vue'
|
import FontControl from 'src/components/font_control/font_control.vue'
|
||||||
|
|
|
||||||
|
|
@ -221,12 +221,15 @@ export default {
|
||||||
|
|
||||||
// ## Components stuff
|
// ## Components stuff
|
||||||
// Getting existing components
|
// Getting existing components
|
||||||
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
|
const componentsContext = import.meta.glob(
|
||||||
const componentKeysAll = componentsContext.keys()
|
['/src/**/*.style.js', '/src/**/*.style.json'],
|
||||||
|
{ eager: true }
|
||||||
|
)
|
||||||
|
const componentKeysAll = Object.keys(componentsContext)
|
||||||
const componentsMap = new Map(
|
const componentsMap = new Map(
|
||||||
componentKeysAll
|
componentKeysAll
|
||||||
.map(
|
.map(
|
||||||
key => [key, componentsContext(key).default]
|
key => [key, componentsContext[key].default]
|
||||||
).filter(([, component]) => !component.virtual && !component.notEditable)
|
).filter(([, component]) => !component.virtual && !component.notEditable)
|
||||||
)
|
)
|
||||||
exports.componentsMap = componentsMap
|
exports.componentsMap = componentsMap
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faChevronRight, faFolder } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronRight, faFolder } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'pinia'
|
||||||
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||||
|
|
||||||
import Popover from 'src/components/popover/popover.vue'
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
import StillImage from 'src/components/still-image/still-image.vue'
|
import StillImage from 'src/components/still-image/still-image.vue'
|
||||||
|
|
@ -20,8 +21,8 @@ const StatusBookmarkFolderMenu = {
|
||||||
StillImage
|
StillImage
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState(useBookmarkFoldersStore, {
|
||||||
folders: state => state.bookmarkFolders.allFolders
|
folders: store => store.allFolders
|
||||||
}),
|
}),
|
||||||
folderId () {
|
folderId () {
|
||||||
return this.status.bookmark_folder_id
|
return this.status.bookmark_folder_id
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,4 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./status_bookmark_folder_menu.js"></script>
|
<script src="./status_bookmark_folder_menu.js"></script>
|
||||||
<stlye src="./status_bookmark_folder_menu.scss" />
|
<style src="./status_bookmark_folder_menu.scss" />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { ListsMenuContent } from '../lists_menu/lists_menu_content.vue'
|
import ListsMenuContent from '../lists_menu/lists_menu_content.vue'
|
||||||
import { BookmarkFoldersMenuContent } from '../bookmark_folders_menu/bookmark_folders_menu_content.vue'
|
import BookmarkFoldersMenuContent from '../bookmark_folders_menu/bookmark_folders_menu_content.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { TIMELINES } from 'src/components/navigation/navigation.js'
|
import { TIMELINES } from 'src/components/navigation/navigation.js'
|
||||||
import { filterNavigation } from 'src/components/navigation/filter.js'
|
import { filterNavigation } from 'src/components/navigation/filter.js'
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useInterfaceStore } from 'src/stores/interface'
|
import { useInterfaceStore } from 'src/stores/interface'
|
||||||
import { useListsStore } from 'src/stores/lists'
|
import { useListsStore } from 'src/stores/lists'
|
||||||
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||||
|
|
||||||
library.add(faChevronDown)
|
library.add(faChevronDown)
|
||||||
|
|
||||||
|
|
@ -100,7 +101,7 @@ const TimelineMenu = {
|
||||||
return useListsStore().findListTitle(this.$route.params.id)
|
return useListsStore().findListTitle(this.$route.params.id)
|
||||||
}
|
}
|
||||||
if (route === 'bookmark-folder') {
|
if (route === 'bookmark-folder') {
|
||||||
return this.$store.getters.findBookmarkFolderName(this.$route.params.id)
|
return useBookmarkFoldersStore().findBookmarkFolderName(this.$route.params.id)
|
||||||
}
|
}
|
||||||
const i18nkey = timelineNames(this.bookmarkFolders)[this.$route.name]
|
const i18nkey = timelineNames(this.bookmarkFolders)[this.$route.name]
|
||||||
return i18nkey ? this.$t(i18nkey) : route
|
return i18nkey ? this.$t(i18nkey) : route
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import Modal from 'src/components/modal/modal.vue'
|
import Modal from 'src/components/modal/modal.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import pleromaTan from 'src/assets/pleromatan_apology.png'
|
|
||||||
import pleromaTanFox from 'src/assets/pleromatan_apology_fox.png'
|
|
||||||
import pleromaTanMask from 'src/assets/pleromatan_apology_mask.png'
|
import pleromaTanMask from 'src/assets/pleromatan_apology_mask.png'
|
||||||
import pleromaTanFoxMask from 'src/assets/pleromatan_apology_fox_mask.png'
|
import pleromaTanFoxMask from 'src/assets/pleromatan_apology_fox_mask.png'
|
||||||
|
|
||||||
|
|
@ -14,6 +12,9 @@ library.add(
|
||||||
|
|
||||||
export const CURRENT_UPDATE_COUNTER = 1
|
export const CURRENT_UPDATE_COUNTER = 1
|
||||||
|
|
||||||
|
const pleromaTan = '/static/pleromatan_apology.png'
|
||||||
|
const pleromaTanFox = '/static/pleromatan_apology_fox.png'
|
||||||
|
|
||||||
const UpdateNotification = {
|
const UpdateNotification = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,8 @@
|
||||||
"auto_save_saved": "Saved.",
|
"auto_save_saved": "Saved.",
|
||||||
"auto_save_saving": "Saving...",
|
"auto_save_saving": "Saving...",
|
||||||
"save_to_drafts_button": "Save to drafts",
|
"save_to_drafts_button": "Save to drafts",
|
||||||
"save_to_drafts_and_close_button": "Save to drafts and close"
|
"save_to_drafts_and_close_button": "Save to drafts and close",
|
||||||
|
"more_post_actions": "More post actions..."
|
||||||
},
|
},
|
||||||
"registration": {
|
"registration": {
|
||||||
"bio_optional": "Bio (optional)",
|
"bio_optional": "Bio (optional)",
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const ensureFinalFallback = codes => {
|
||||||
return codeList.includes('en') ? codeList : codeList.concat(['en'])
|
return codeList.includes('en') ? codeList : codeList.concat(['en'])
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
languages,
|
languages,
|
||||||
langCodeToJsonName,
|
langCodeToJsonName,
|
||||||
langCodeToCldrName,
|
langCodeToCldrName,
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,23 @@
|
||||||
|
|
||||||
import { isEqual } from 'lodash'
|
import { isEqual } from 'lodash'
|
||||||
import { languages, langCodeToJsonName } from './languages.js'
|
import { languages, langCodeToJsonName } from './languages.js'
|
||||||
|
import enMessages from './en.json'
|
||||||
|
|
||||||
const ULTIMATE_FALLBACK_LOCALE = 'en'
|
const ULTIMATE_FALLBACK_LOCALE = 'en'
|
||||||
|
|
||||||
const hasLanguageFile = (code) => languages.includes(code)
|
const hasLanguageFile = (code) => languages.includes(code)
|
||||||
|
|
||||||
|
const languageFileMap = import.meta.glob('./*.json')
|
||||||
|
|
||||||
const loadLanguageFile = (code) => {
|
const loadLanguageFile = (code) => {
|
||||||
return import(
|
const jsonName = langCodeToJsonName(code)
|
||||||
/* webpackInclude: /\.json$/ */
|
return languageFileMap[`./${jsonName}.json`]()
|
||||||
/* webpackChunkName: "i18n/[request]" */
|
|
||||||
`./${langCodeToJsonName(code)}.json`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
languages,
|
languages,
|
||||||
default: {
|
default: {
|
||||||
en: require('./en.json').default
|
en: enMessages
|
||||||
},
|
},
|
||||||
setLanguage: async (i18n, language) => {
|
setLanguage: async (i18n, language) => {
|
||||||
const languages = (Array.isArray(language) ? language : [language]).filter(k => k)
|
const languages = (Array.isArray(language) ? language : [language]).filter(k => k)
|
||||||
|
|
|
||||||
37
src/main.js
|
|
@ -5,21 +5,7 @@ import { createPinia } from 'pinia'
|
||||||
import 'custom-event-polyfill'
|
import 'custom-event-polyfill'
|
||||||
import './lib/event_target_polyfill.js'
|
import './lib/event_target_polyfill.js'
|
||||||
|
|
||||||
import instanceModule from './modules/instance.js'
|
import vuexModules from './modules/index.js'
|
||||||
import statusesModule from './modules/statuses.js'
|
|
||||||
import notificationsModule from './modules/notifications.js'
|
|
||||||
import usersModule from './modules/users.js'
|
|
||||||
import apiModule from './modules/api.js'
|
|
||||||
import configModule from './modules/config.js'
|
|
||||||
import profileConfigModule from './modules/profileConfig.js'
|
|
||||||
import serverSideStorageModule from './modules/serverSideStorage.js'
|
|
||||||
import adminSettingsModule from './modules/adminSettings.js'
|
|
||||||
import oauthModule from './modules/oauth.js'
|
|
||||||
import authFlowModule from './modules/auth_flow.js'
|
|
||||||
import oauthTokensModule from './modules/oauth_tokens.js'
|
|
||||||
import draftsModule from './modules/drafts.js'
|
|
||||||
import chatsModule from './modules/chats.js'
|
|
||||||
import bookmarkFoldersModule from './modules/bookmark_folders.js'
|
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
|
@ -85,29 +71,12 @@ const persistedStateOptions = {
|
||||||
console.error('Storage error', e)
|
console.error('Storage error', e)
|
||||||
storageError = e
|
storageError = e
|
||||||
}
|
}
|
||||||
document.querySelector('#mascot').src = `/static/pleromatan_apology${isFox}.png`
|
document.querySelector('#mascot').src = `/static/pleromatan_apology${isFox}_small.webp`
|
||||||
document.querySelector('#status').removeAttribute('class')
|
document.querySelector('#status').removeAttribute('class')
|
||||||
document.querySelector('#status').textContent = i18n.global.t('splash.loading')
|
document.querySelector('#status').textContent = i18n.global.t('splash.loading')
|
||||||
document.querySelector('#splash-credit').textContent = i18n.global.t('update.art_by', { linkToArtist: 'pipivovott' })
|
document.querySelector('#splash-credit').textContent = i18n.global.t('update.art_by', { linkToArtist: 'pipivovott' })
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
modules: {
|
modules: vuexModules,
|
||||||
instance: instanceModule,
|
|
||||||
// TODO refactor users/statuses modules, they depend on each other
|
|
||||||
users: usersModule,
|
|
||||||
statuses: statusesModule,
|
|
||||||
notifications: notificationsModule,
|
|
||||||
api: apiModule,
|
|
||||||
config: configModule,
|
|
||||||
profileConfig: profileConfigModule,
|
|
||||||
serverSideStorage: serverSideStorageModule,
|
|
||||||
adminSettings: adminSettingsModule,
|
|
||||||
oauth: oauthModule,
|
|
||||||
authFlow: authFlowModule,
|
|
||||||
oauthTokens: oauthTokensModule,
|
|
||||||
drafts: draftsModule,
|
|
||||||
chats: chatsModule,
|
|
||||||
bookmarkFolders: bookmarkFoldersModule
|
|
||||||
},
|
|
||||||
plugins,
|
plugins,
|
||||||
options: {
|
options: {
|
||||||
devtools: process.env.NODE_ENV !== 'production'
|
devtools: process.env.NODE_ENV !== 'production'
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { remove, find } from 'lodash'
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
allFolders: []
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mutations = {
|
|
||||||
setBookmarkFolders (state, value) {
|
|
||||||
state.allFolders = value
|
|
||||||
},
|
|
||||||
setBookmarkFolder (state, { id, name, emoji, emoji_url: emojiUrl }) {
|
|
||||||
const entry = find(state.allFolders, { id })
|
|
||||||
if (!entry) {
|
|
||||||
state.allFolders.push({ id, name, emoji, emoji_url: emojiUrl })
|
|
||||||
} else {
|
|
||||||
entry.name = name
|
|
||||||
entry.emoji = emoji
|
|
||||||
entry.emoji_url = emojiUrl
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deleteBookmarkFolder (state, { folderId }) {
|
|
||||||
remove(state.allFolders, folder => folder.id === folderId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
setBookmarkFolders ({ commit }, value) {
|
|
||||||
commit('setBookmarkFolders', value)
|
|
||||||
},
|
|
||||||
createBookmarkFolder ({ rootState, commit }, { name, emoji }) {
|
|
||||||
return rootState.api.backendInteractor.createBookmarkFolder({ name, emoji })
|
|
||||||
.then((folder) => {
|
|
||||||
commit('setBookmarkFolder', folder)
|
|
||||||
return folder
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setBookmarkFolder ({ rootState, commit }, { folderId, name, emoji }) {
|
|
||||||
return rootState.api.backendInteractor.updateBookmarkFolder({ folderId, name, emoji })
|
|
||||||
.then((folder) => {
|
|
||||||
commit('setBookmarkFolder', folder)
|
|
||||||
return folder
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteBookmarkFolder ({ rootState, commit }, { folderId }) {
|
|
||||||
rootState.api.backendInteractor.deleteBookmarkFolder({ folderId })
|
|
||||||
commit('deleteBookmarkFolder', { folderId })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getters = {
|
|
||||||
findBookmarkFolderName: state => id => {
|
|
||||||
const folder = state.allFolders.find(folder => folder.id === id)
|
|
||||||
|
|
||||||
if (!folder) return
|
|
||||||
return folder.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookmarkFolders = {
|
|
||||||
state: defaultState,
|
|
||||||
mutations,
|
|
||||||
actions,
|
|
||||||
getters
|
|
||||||
}
|
|
||||||
|
|
||||||
export default bookmarkFolders
|
|
||||||
31
src/modules/index.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import instance from './instance.js'
|
||||||
|
import statuses from './statuses.js'
|
||||||
|
import notifications from './notifications.js'
|
||||||
|
import users from './users.js'
|
||||||
|
import api from './api.js'
|
||||||
|
import config from './config.js'
|
||||||
|
import profileConfig from './profileConfig.js'
|
||||||
|
import serverSideStorage from './serverSideStorage.js'
|
||||||
|
import adminSettings from './adminSettings.js'
|
||||||
|
import oauth from './oauth.js'
|
||||||
|
import authFlow from './auth_flow.js'
|
||||||
|
import oauthTokens from './oauth_tokens.js'
|
||||||
|
import drafts from './drafts.js'
|
||||||
|
import chats from './chats.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
instance,
|
||||||
|
statuses,
|
||||||
|
notifications,
|
||||||
|
users,
|
||||||
|
api,
|
||||||
|
config,
|
||||||
|
profileConfig,
|
||||||
|
serverSideStorage,
|
||||||
|
adminSettings,
|
||||||
|
oauth,
|
||||||
|
authFlow,
|
||||||
|
oauthTokens,
|
||||||
|
drafts,
|
||||||
|
chats
|
||||||
|
}
|
||||||
|
|
@ -179,9 +179,9 @@ const defaultState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadAnnotations = (lang) => {
|
const loadAnnotations = (lang) => {
|
||||||
|
const code = langCodeToCldrName(lang)
|
||||||
return import(
|
return import(
|
||||||
/* webpackChunkName: "emoji-annotations/[request]" */
|
`../../node_modules/@kazvmoe-infra/unicode-emoji-json/annotations/${code}.json`
|
||||||
`@kazvmoe-infra/unicode-emoji-json/annotations/${langCodeToCldrName(lang)}.json`
|
|
||||||
)
|
)
|
||||||
.then(k => k.default)
|
.then(k => k.default)
|
||||||
}
|
}
|
||||||
|
|
@ -304,7 +304,7 @@ const instance = {
|
||||||
},
|
},
|
||||||
async getStaticEmoji ({ commit }) {
|
async getStaticEmoji ({ commit }) {
|
||||||
try {
|
try {
|
||||||
const values = (await import(/* webpackChunkName: 'emoji' */ '../../static/emoji.json')).default
|
const values = (await import('/src/assets/emoji.json')).default
|
||||||
|
|
||||||
const emoji = Object.keys(values).reduce((res, groupId) => {
|
const emoji = Object.keys(values).reduce((res, groupId) => {
|
||||||
res[groupId] = values[groupId].map(e => ({
|
res[groupId] = values[groupId].map(e => ({
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,8 @@ const getLatestScrobble = (state, user) => {
|
||||||
|
|
||||||
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
|
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
|
||||||
}
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
console.warn('cannot fetch scrobbles', e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import apiService from '../api/api.service.js'
|
import apiService from '../api/api.service.js'
|
||||||
import { promiseInterval } from '../promise_interval/promise_interval.js'
|
import { promiseInterval } from '../promise_interval/promise_interval.js'
|
||||||
|
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||||
|
|
||||||
const fetchAndUpdate = ({ store, credentials }) => {
|
const fetchAndUpdate = ({ credentials }) => {
|
||||||
return apiService.fetchBookmarkFolders({ credentials })
|
return apiService.fetchBookmarkFolders({ credentials })
|
||||||
.then(bookmarkFolders => {
|
.then(bookmarkFolders => {
|
||||||
store.commit('setBookmarkFolders', bookmarkFolders)
|
useBookmarkFoldersStore().setBookmarkFolders(bookmarkFolders)
|
||||||
}, () => {})
|
}, () => {})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import escape from 'escape-html'
|
import escape from 'escape-html'
|
||||||
import parseLinkHeader from 'parse-link-header'
|
import { parseLinkHeader } from '@web3-storage/parse-link-header'
|
||||||
import { isStatusNotification } from '../notification_utils/notification_utils.js'
|
import { isStatusNotification } from '../notification_utils/notification_utils.js'
|
||||||
import punycode from 'punycode.js'
|
import punycode from 'punycode.js'
|
||||||
|
|
||||||
|
|
@ -484,8 +484,8 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
|
||||||
const flakeId = opts.flakeId
|
const flakeId = opts.flakeId
|
||||||
const parsedLinkHeader = parseLinkHeader(linkHeader)
|
const parsedLinkHeader = parseLinkHeader(linkHeader)
|
||||||
if (!parsedLinkHeader) return
|
if (!parsedLinkHeader) return
|
||||||
const maxId = parsedLinkHeader.next.max_id
|
const maxId = parsedLinkHeader.next?.max_id
|
||||||
const minId = parsedLinkHeader.prev.min_id
|
const minId = parsedLinkHeader.prev?.min_id
|
||||||
|
|
||||||
return {
|
return {
|
||||||
maxId: flakeId ? maxId : parseInt(maxId, 10),
|
maxId: flakeId ? maxId : parseInt(maxId, 10),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
const createRuffleService = () => {
|
const createRuffleService = () => {
|
||||||
let ruffleInstance = null
|
let ruffleInstance = null
|
||||||
|
|
||||||
const getRuffle = () => new Promise((resolve, reject) => {
|
const getRuffle = async () => new Promise((resolve, reject) => {
|
||||||
if (ruffleInstance) {
|
if (ruffleInstance) {
|
||||||
resolve(ruffleInstance)
|
resolve(ruffleInstance)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ruffle needs these to be set before it's loaded
|
// Ruffle needs these to be set before it's loaded
|
||||||
// https://github.com/ruffle-rs/ruffle/issues/3952
|
// https://github.com/ruffle-rs/ruffle/issues/3952
|
||||||
window.RufflePlayer = {}
|
window.RufflePlayer = {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import runtime from 'serviceworker-webpack5-plugin/lib/runtime'
|
/* global process */
|
||||||
|
|
||||||
function urlBase64ToUint8Array (base64String) {
|
function urlBase64ToUint8Array (base64String) {
|
||||||
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
||||||
const base64 = (base64String + padding)
|
const base64 = (base64String + padding)
|
||||||
|
|
@ -19,7 +18,8 @@ function isPushSupported () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateServiceWorker () {
|
function getOrCreateServiceWorker () {
|
||||||
return runtime.register()
|
const swType = process.env.HAS_MODULE_SERVICE_WORKER ? 'module' : 'classic'
|
||||||
|
return navigator.serviceWorker.register('/sw-pleroma.js', { type: swType })
|
||||||
.catch((err) => console.error('Unable to get or create a service worker.', err))
|
.catch((err) => console.error('Unable to get or create a service worker.', err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,14 +98,14 @@ export async function initServiceWorker (store) {
|
||||||
|
|
||||||
export async function showDesktopNotification (content) {
|
export async function showDesktopNotification (content) {
|
||||||
if (!isSWSupported) return
|
if (!isSWSupported) return
|
||||||
const { active: sw } = await window.navigator.serviceWorker.getRegistration()
|
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
|
||||||
if (!sw) return console.error('No serviceworker found!')
|
if (!sw) return console.error('No serviceworker found!')
|
||||||
sw.postMessage({ type: 'desktopNotification', content })
|
sw.postMessage({ type: 'desktopNotification', content })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function closeDesktopNotification ({ id }) {
|
export async function closeDesktopNotification ({ id }) {
|
||||||
if (!isSWSupported) return
|
if (!isSWSupported) return
|
||||||
const { active: sw } = await window.navigator.serviceWorker.getRegistration()
|
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
|
||||||
if (!sw) return console.error('No serviceworker found!')
|
if (!sw) return console.error('No serviceworker found!')
|
||||||
if (id >= 0) {
|
if (id >= 0) {
|
||||||
sw.postMessage({ type: 'desktopNotificationClose', content: { id } })
|
sw.postMessage({ type: 'desktopNotificationClose', content: { id } })
|
||||||
|
|
@ -116,7 +116,7 @@ export async function closeDesktopNotification ({ id }) {
|
||||||
|
|
||||||
export async function updateFocus () {
|
export async function updateFocus () {
|
||||||
if (!isSWSupported) return
|
if (!isSWSupported) return
|
||||||
const { active: sw } = await window.navigator.serviceWorker.getRegistration()
|
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
|
||||||
if (!sw) return console.error('No serviceworker found!')
|
if (!sw) return console.error('No serviceworker found!')
|
||||||
sw.postMessage({ type: 'updateFocus' })
|
sw.postMessage({ type: 'updateFocus' })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,9 +146,12 @@ const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVar
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading all style.js[on] files dynamically
|
// Loading all style.js[on] files dynamically
|
||||||
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
|
const componentsContext = import.meta.glob(
|
||||||
componentsContext.keys().forEach(key => {
|
['/src/**/*.style.js', '/src/**/*.style.json'],
|
||||||
const component = componentsContext(key).default
|
{ eager: true }
|
||||||
|
)
|
||||||
|
Object.keys(componentsContext).forEach(key => {
|
||||||
|
const component = componentsContext[key].default
|
||||||
if (components[component.name] != null) {
|
if (components[component.name] != null) {
|
||||||
console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`)
|
console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
src/stores/bookmark_folders.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { remove, find } from 'lodash'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useBookmarkFoldersStore = defineStore('bookmarkFolders', {
|
||||||
|
state: () => ({
|
||||||
|
allFolders: []
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
findBookmarkFolderName () {
|
||||||
|
return (id) => {
|
||||||
|
const folder = this.allFolders.find(folder => folder.id === id)
|
||||||
|
|
||||||
|
if (!folder) return
|
||||||
|
return folder.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setBookmarkFolders (value) {
|
||||||
|
this.allFolders = value
|
||||||
|
},
|
||||||
|
setBookmarkFolder ({ id, name, emoji, emoji_url: emojiUrl }) {
|
||||||
|
const entry = find(this.allFolders, { id })
|
||||||
|
if (!entry) {
|
||||||
|
this.allFolders.push({ id, name, emoji, emoji_url: emojiUrl })
|
||||||
|
} else {
|
||||||
|
entry.name = name
|
||||||
|
entry.emoji = emoji
|
||||||
|
entry.emoji_url = emojiUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createBookmarkFolder ({ name, emoji }) {
|
||||||
|
return window.vuex.state.api.backendInteractor.createBookmarkFolder({ name, emoji })
|
||||||
|
.then((folder) => {
|
||||||
|
this.setBookmarkFolder(folder)
|
||||||
|
return folder
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateBookmarkFolder ({ folderId, name, emoji }) {
|
||||||
|
return window.vuex.state.api.backendInteractor.updateBookmarkFolder({ folderId, name, emoji })
|
||||||
|
.then((folder) => {
|
||||||
|
this.setBookmarkFolder(folder)
|
||||||
|
return folder
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteBookmarkFolder ({ folderId }) {
|
||||||
|
window.vuex.state.api.backendInteractor.deleteBookmarkFolder({ folderId })
|
||||||
|
remove(this.allFolders, folder => folder.id === folderId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -4,7 +4,10 @@ import { storage } from 'src/lib/storage.js'
|
||||||
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
|
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
|
||||||
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
|
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
import messages from './i18n/service_worker_messages.js'
|
// Collects all messages for service workers
|
||||||
|
// Needed because service workers cannot use dynamic imports
|
||||||
|
// See /build/sw_plugin.js for more information
|
||||||
|
import messages from 'virtual:pleroma-fe/service_worker_messages'
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
// By default, use the browser locale, we will update it if neccessary
|
// By default, use the browser locale, we will update it if neccessary
|
||||||
|
|
@ -139,3 +142,5 @@ self.addEventListener('notificationclick', (event) => {
|
||||||
if (clients.openWindow) return clients.openWindow('/')
|
if (clients.openWindow) return clients.openWindow('/')
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('sw here')
|
||||||
|
|
|
||||||
21
test/fixtures/mock_store.js
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { createStore } from 'vuex'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
import vuexModules from 'src/modules/index.js'
|
||||||
|
|
||||||
|
const tweakModules = modules => {
|
||||||
|
const res = {}
|
||||||
|
Object.entries(modules).forEach(([name, module]) => {
|
||||||
|
const m = { ...module }
|
||||||
|
m.state = cloneDeep(module.state)
|
||||||
|
res[name] = m
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMockStore = () => {
|
||||||
|
return createStore({
|
||||||
|
modules: tweakModules(vuexModules),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default makeMockStore
|
||||||
126
test/fixtures/setup_test.js
vendored
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { config } from '@vue/test-utils'
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
|
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||||
|
import routes from 'src/boot/routes'
|
||||||
|
import makeMockStore from './mock_store'
|
||||||
|
|
||||||
|
export const $t = msg => msg
|
||||||
|
const $i18n = { t: msg => msg }
|
||||||
|
|
||||||
|
const applyAfterStore = (store, afterStore) => {
|
||||||
|
afterStore(store)
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDefaultOpts = ({ afterStore = () => {} } = {}) => ({
|
||||||
|
global: {
|
||||||
|
plugins: [
|
||||||
|
applyAfterStore(makeMockStore(), afterStore),
|
||||||
|
VueVirtualScroller,
|
||||||
|
createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: routes({ state: {
|
||||||
|
users: {
|
||||||
|
currentUser: {}
|
||||||
|
},
|
||||||
|
instance: {}
|
||||||
|
}})
|
||||||
|
}),
|
||||||
|
(Vue) => { Vue.directive('body-scroll-lock', {}) }
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
I18nT: true,
|
||||||
|
teleport: true,
|
||||||
|
FAIcon: true,
|
||||||
|
FALayers: true,
|
||||||
|
},
|
||||||
|
mocks: {
|
||||||
|
$t,
|
||||||
|
$i18n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://github.com/vuejs/vue-test-utils/issues/960
|
||||||
|
const customBehaviors = () => {
|
||||||
|
const filterByText = keyword => {
|
||||||
|
const match = keyword instanceof RegExp
|
||||||
|
? (target) => target && keyword.test(target)
|
||||||
|
: (target) => keyword === target
|
||||||
|
|
||||||
|
return wrapper => (
|
||||||
|
match(wrapper.text()) ||
|
||||||
|
match(wrapper.attributes('aria-label')) ||
|
||||||
|
match(wrapper.attributes('title'))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
findComponentByText(searchedComponent, text) {
|
||||||
|
return this.findAllComponents(searchedComponent)
|
||||||
|
.filter(filterByText(text))
|
||||||
|
.at(0)
|
||||||
|
},
|
||||||
|
findByText(searchedElement, text) {
|
||||||
|
return this.findAll(searchedElement)
|
||||||
|
.filter(filterByText(text))
|
||||||
|
.at(0)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config.plugins.VueWrapper.install(customBehaviors)
|
||||||
|
|
||||||
|
export const mountOpts = (allOpts = {}) => {
|
||||||
|
const { afterStore, ...opts } = allOpts
|
||||||
|
const defaultOpts = getDefaultOpts({ afterStore })
|
||||||
|
const mergedOpts = {
|
||||||
|
...opts,
|
||||||
|
global: {
|
||||||
|
...defaultOpts.global
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.global) {
|
||||||
|
mergedOpts.global.plugins = mergedOpts.global.plugins.concat(opts.global.plugins || [])
|
||||||
|
Object.entries(opts.global).forEach(([k, v]) => {
|
||||||
|
if (k === 'plugins') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (defaultOpts.global[k]) {
|
||||||
|
mergedOpts.global[k] = {
|
||||||
|
...defaultOpts.global[k],
|
||||||
|
...v,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mergedOpts.global[k] = v
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/78033718/how-can-i-wait-for-an-emitted-event-of-a-mounted-component-in-vue-test-utils
|
||||||
|
export const waitForEvent = (wrapper, event, {
|
||||||
|
timeout = 1000,
|
||||||
|
timesEmitted = 1
|
||||||
|
} = {}) => {
|
||||||
|
const tick = 10
|
||||||
|
|
||||||
|
return vi.waitFor(
|
||||||
|
() => {
|
||||||
|
const e = wrapper.emitted(event)
|
||||||
|
if (e && e.length >= timesEmitted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw new Error('event is not emitted')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout,
|
||||||
|
interval: tick
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"expect": true,
|
|
||||||
"sinon": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
// require all test files (files that ends with .spec.js)
|
|
||||||
const testsContext = require.context('./specs', true, /\.spec$/)
|
|
||||||
testsContext.keys().forEach(testsContext)
|
|
||||||
|
|
||||||
// require all src files except main.js for coverage.
|
|
||||||
// you can also change this to match only the subset of files that
|
|
||||||
// you want coverage for.
|
|
||||||
// const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
|
|
||||||
// srcContext.keys().forEach(srcContext)
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
// This is a karma config file. For more details see
|
|
||||||
// http://karma-runner.github.io/0.13/config/configuration-file.html
|
|
||||||
// we are also using it with karma-webpack
|
|
||||||
// https://github.com/webpack/karma-webpack
|
|
||||||
|
|
||||||
// var path = require('path')
|
|
||||||
const merge = require('webpack-merge')
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
|
||||||
const baseConfig = require('../../build/webpack.base.conf')
|
|
||||||
const utils = require('../../build/utils')
|
|
||||||
const webpack = require('webpack')
|
|
||||||
// var projectRoot = path.resolve(__dirname, '../../')
|
|
||||||
|
|
||||||
const webpackConfig = merge(baseConfig, {
|
|
||||||
// use inline sourcemap for karma-sourcemap-loader
|
|
||||||
module: {
|
|
||||||
rules: utils.styleLoaders()
|
|
||||||
},
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': require('../../config/test.env')
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: 'index.html',
|
|
||||||
template: 'index.html',
|
|
||||||
inject: true
|
|
||||||
})
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// no need for app entry during tests
|
|
||||||
delete webpackConfig.entry
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
// to run in additional browsers:
|
|
||||||
// 1. install corresponding karma launcher
|
|
||||||
// http://karma-runner.github.io/0.13/config/browsers.html
|
|
||||||
// 2. add it to the `browsers` array below.
|
|
||||||
browsers: ['FirefoxHeadless'],
|
|
||||||
frameworks: ['mocha', 'sinon-chai'],
|
|
||||||
reporters: ['mocha'],
|
|
||||||
customLaunchers: {
|
|
||||||
FirefoxHeadless: {
|
|
||||||
base: 'Firefox',
|
|
||||||
flags: [
|
|
||||||
'-headless'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
files: [
|
|
||||||
'./index.js'
|
|
||||||
],
|
|
||||||
preprocessors: {
|
|
||||||
'./index.js': ['webpack', 'sourcemap']
|
|
||||||
},
|
|
||||||
webpack: webpackConfig,
|
|
||||||
webpackMiddleware: {
|
|
||||||
noInfo: true
|
|
||||||
},
|
|
||||||
mochaReporter: {
|
|
||||||
showDiff: true
|
|
||||||
},
|
|
||||||
coverageReporter: {
|
|
||||||
dir: './coverage',
|
|
||||||
reporters: [
|
|
||||||
{ type: 'lcov', subdir: '.' },
|
|
||||||
{ type: 'text-summary' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
153
test/unit/specs/components/draft.spec.js
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { mount, flushPromises } from '@vue/test-utils'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||||
|
import { mountOpts, waitForEvent, $t } from '../../../fixtures/setup_test'
|
||||||
|
|
||||||
|
const autoSaveOrNot = (caseFn, caseTitle, runFn) => {
|
||||||
|
caseFn(`${caseTitle} with auto-save`, function () {
|
||||||
|
return runFn.bind(this)(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
caseFn(`${caseTitle} with no auto-save`, function () {
|
||||||
|
return runFn.bind(this)(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveManually = async (wrapper) => {
|
||||||
|
const morePostActions = wrapper.findByText('button', $t('post_status.more_post_actions'))
|
||||||
|
await morePostActions.trigger('click')
|
||||||
|
|
||||||
|
const btn = wrapper.findByText('button', $t('post_status.save_to_drafts_button'))
|
||||||
|
await btn.trigger('click')
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitSaveTime = 4000
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Draft saving', () => {
|
||||||
|
autoSaveOrNot(it, 'should save when the button is clicked', async (autoSave) => {
|
||||||
|
const wrapper = mount(PostStatusForm, mountOpts())
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'autoSaveDraft',
|
||||||
|
value: autoSave
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
|
|
||||||
|
const textarea = wrapper.get('textarea')
|
||||||
|
await textarea.setValue('mew mew')
|
||||||
|
|
||||||
|
await saveManually(wrapper)
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
||||||
|
expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should auto-save if it is enabled', async function () {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
const wrapper = mount(PostStatusForm, mountOpts())
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'autoSaveDraft',
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
|
const textarea = wrapper.get('textarea')
|
||||||
|
await textarea.setValue('mew mew')
|
||||||
|
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
|
await vi.advanceTimersByTimeAsync(waitSaveTime)
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
||||||
|
expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should auto-save when close if auto-save is on', async () => {
|
||||||
|
const wrapper = mount(PostStatusForm, mountOpts({
|
||||||
|
props: {
|
||||||
|
closeable: true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'autoSaveDraft',
|
||||||
|
value: true
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
|
const textarea = wrapper.get('textarea')
|
||||||
|
await textarea.setValue('mew mew')
|
||||||
|
wrapper.vm.requestClose()
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
||||||
|
await waitForEvent(wrapper, 'can-close')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should save when close if auto-save is off, and unsavedPostAction is save', async () => {
|
||||||
|
const wrapper = mount(PostStatusForm, mountOpts({
|
||||||
|
props: {
|
||||||
|
closeable: true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'autoSaveDraft',
|
||||||
|
value: false
|
||||||
|
})
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'unsavedPostAction',
|
||||||
|
value: 'save'
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
|
const textarea = wrapper.get('textarea')
|
||||||
|
await textarea.setValue('mew mew')
|
||||||
|
wrapper.vm.requestClose()
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
||||||
|
await waitForEvent(wrapper, 'can-close')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => {
|
||||||
|
const wrapper = mount(PostStatusForm, mountOpts({
|
||||||
|
props: {
|
||||||
|
closeable: true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'autoSaveDraft',
|
||||||
|
value: false
|
||||||
|
})
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'unsavedPostAction',
|
||||||
|
value: 'discard'
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
|
const textarea = wrapper.get('textarea')
|
||||||
|
await textarea.setValue('mew mew')
|
||||||
|
wrapper.vm.requestClose()
|
||||||
|
await waitForEvent(wrapper, 'can-close')
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => {
|
||||||
|
const wrapper = mount(PostStatusForm, mountOpts({
|
||||||
|
props: {
|
||||||
|
closeable: true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'autoSaveDraft',
|
||||||
|
value: false
|
||||||
|
})
|
||||||
|
await wrapper.vm.$store.dispatch('setOption', {
|
||||||
|
name: 'unsavedPostAction',
|
||||||
|
value: 'confirm'
|
||||||
|
})
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
|
||||||
|
const textarea = wrapper.get('textarea')
|
||||||
|
await textarea.setValue('mew mew')
|
||||||
|
wrapper.vm.requestClose()
|
||||||
|
await nextTick()
|
||||||
|
const saveButton = wrapper.findByText('button', $t('post_status.close_confirm_save_button'))
|
||||||
|
expect(saveButton).to.be.ok
|
||||||
|
await saveButton.trigger('click')
|
||||||
|
console.log('clicked')
|
||||||
|
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
|
||||||
|
await flushPromises()
|
||||||
|
await waitForEvent(wrapper, 'can-close')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -18,7 +18,13 @@ const generateInput = (value, padEmoji = true) => {
|
||||||
$t: (msg) => msg
|
$t: (msg) => msg
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
FAIcon: true
|
FAIcon: true,
|
||||||
|
Popover: {
|
||||||
|
template: `<div><slot trigger /></div>`,
|
||||||
|
methods: {
|
||||||
|
updateStyles () {}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
'click-outside': vClickOutside
|
'click-outside': vClickOutside
|
||||||
|
|
@ -104,43 +110,37 @@ describe('EmojiInput', () => {
|
||||||
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Eat some spam!:spam:')
|
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Eat some spam!:spam:')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('correctly sets caret after insertion at beginning', (done) => {
|
it('correctly sets caret after insertion at beginning', async () => {
|
||||||
const initialString = '1234'
|
const initialString = '1234'
|
||||||
const wrapper = generateInput(initialString)
|
const wrapper = generateInput(initialString)
|
||||||
const input = wrapper.find('input')
|
const input = wrapper.find('input')
|
||||||
input.setValue(initialString)
|
input.setValue(initialString)
|
||||||
wrapper.setData({ caret: 0 })
|
wrapper.setData({ caret: 0 })
|
||||||
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
||||||
wrapper.vm.$nextTick(() => {
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.vm.caret).to.eql(5)
|
expect(wrapper.vm.caret).to.eql(5)
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('correctly sets caret after insertion at end', (done) => {
|
it('correctly sets caret after insertion at end', async () => {
|
||||||
const initialString = '1234'
|
const initialString = '1234'
|
||||||
const wrapper = generateInput(initialString)
|
const wrapper = generateInput(initialString)
|
||||||
const input = wrapper.find('input')
|
const input = wrapper.find('input')
|
||||||
input.setValue(initialString)
|
input.setValue(initialString)
|
||||||
wrapper.setData({ caret: initialString.length })
|
wrapper.setData({ caret: initialString.length })
|
||||||
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
||||||
wrapper.vm.$nextTick(() => {
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.vm.caret).to.eql(10)
|
expect(wrapper.vm.caret).to.eql(10)
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('correctly sets caret after insertion if padEmoji setting is set to false', (done) => {
|
it('correctly sets caret after insertion if padEmoji setting is set to false', async () => {
|
||||||
const initialString = '1234'
|
const initialString = '1234'
|
||||||
const wrapper = generateInput(initialString, false)
|
const wrapper = generateInput(initialString, false)
|
||||||
const input = wrapper.find('input')
|
const input = wrapper.find('input')
|
||||||
input.setValue(initialString)
|
input.setValue(initialString)
|
||||||
wrapper.setData({ caret: initialString.length })
|
wrapper.setData({ caret: initialString.length })
|
||||||
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
|
||||||
wrapper.vm.$nextTick(() => {
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.vm.caret).to.eql(8)
|
expect(wrapper.vm.caret).to.eql(8)
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import * as DateUtils from 'src/services/date_utils/date_utils.js'
|
import * as DateUtils from 'src/services/date_utils/date_utils.js'
|
||||||
|
|
||||||
|
beforeEach(() => { vi.useFakeTimers() })
|
||||||
|
afterEach(() => { vi.useRealTimers() })
|
||||||
|
|
||||||
describe('DateUtils', () => {
|
describe('DateUtils', () => {
|
||||||
describe('relativeTime', () => {
|
describe('relativeTime', () => {
|
||||||
it('returns now with low enough amount of seconds', () => {
|
it('returns now with low enough amount of seconds', () => {
|
||||||
|
|
|
||||||