forked from AkkomaGang/akkoma-fe
Compare commits
20 commits
40e86998e6
...
9b75ca414f
Author | SHA1 | Date | |
---|---|---|---|
9b75ca414f | |||
b07cf33a04 | |||
142f90c4cf | |||
83c6f7f9f9 | |||
65adfb01c3 | |||
65511042e3 | |||
235f3b2d94 | |||
2382696698 | |||
ae2d72131b | |||
98d38e3b73 | |||
47c05363f8 | |||
87d9c1ae15 | |||
5ad0da1766 | |||
97e9b2597a | |||
94bbf8f0a3 | |||
ce9d316a51 | |||
6ce12fc153 | |||
6a2cdcfc15 | |||
d7688fafd3 | |||
3d3425eda9 |
418 changed files with 11724 additions and 8451 deletions
6
.babelrc
6
.babelrc
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-runtime",
|
||||
"lodash",
|
||||
"@vue/babel-plugin-jsx"
|
||||
],
|
||||
"comments": false
|
||||
}
|
||||
|
|
|
@ -5,14 +5,9 @@ module.exports = {
|
|||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue',
|
||||
'import'
|
||||
],
|
||||
plugins: ['vue', 'import'],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow paren-less arrow functions
|
||||
|
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"singleAttributePerLine": true
|
||||
}
|
|
@ -11,14 +11,17 @@ var webpackConfig = require('./webpack.prod.conf')
|
|||
|
||||
console.log(
|
||||
' Tip:\n' +
|
||||
' Built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
' 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 assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||
var assetsPath = path.join(
|
||||
config.build.assetsRoot,
|
||||
config.build.assetsSubDirectory
|
||||
)
|
||||
rm('-rf', assetsPath)
|
||||
mkdir('-p', assetsPath)
|
||||
cp('-R', 'static/*', assetsPath)
|
||||
|
@ -26,11 +29,13 @@ 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')
|
||||
process.stdout.write(
|
||||
stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n'
|
||||
)
|
||||
})
|
||||
|
|
|
@ -2,8 +2,7 @@ var semver = require('semver')
|
|||
var chalk = require('chalk')
|
||||
var packageConfig = require('../package.json')
|
||||
var exec = function (cmd) {
|
||||
return require('child_process')
|
||||
.execSync(cmd).toString().trim()
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
var versionRequirements = [
|
||||
|
@ -24,16 +23,23 @@ module.exports = function () {
|
|||
for (var i = 0; i < versionRequirements.length; i++) {
|
||||
var mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
warnings.push(
|
||||
mod.name +
|
||||
': ' +
|
||||
chalk.red(mod.currentVersion) +
|
||||
' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'To use this template, you must update following to modules:'
|
||||
)
|
||||
)
|
||||
console.log()
|
||||
for (var i = 0; i < warnings.length; i++) {
|
||||
var warning = warnings[i]
|
||||
|
|
|
@ -6,9 +6,10 @@ 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 webpackConfig =
|
||||
process.env.NODE_ENV === 'testing'
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
|
||||
// default port where dev server listens for incoming traffic
|
||||
var port = process.env.PORT || config.dev.port
|
||||
|
@ -50,7 +51,10 @@ app.use(devMiddleware)
|
|||
app.use(hotMiddleware)
|
||||
|
||||
// serve pure static assets
|
||||
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
var staticPath = path.posix.join(
|
||||
config.dev.assetsPublicPath,
|
||||
config.dev.assetsSubDirectory
|
||||
)
|
||||
app.use(staticPath, express.static('./static'))
|
||||
|
||||
module.exports = app.listen(port, function (err) {
|
||||
|
|
|
@ -4,7 +4,8 @@ var sass = require('sass')
|
|||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
var assetsSubDirectory =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
|
@ -13,7 +14,7 @@ exports.assetsPath = function (_path) {
|
|||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
function generateLoaders (loaders) {
|
||||
function generateLoaders(loaders) {
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
|
@ -27,11 +28,11 @@ exports.cssLoaders = function (options) {
|
|||
return [
|
||||
{
|
||||
test: /\.(post)?css$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader']),
|
||||
use: generateLoaders(['css-loader', 'postcss-loader'])
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']),
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader'])
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
|
@ -52,8 +53,8 @@ exports.cssLoaders = function (options) {
|
|||
},
|
||||
{
|
||||
test: /\.styl(us)?$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']),
|
||||
},
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader'])
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ var { VueLoaderPlugin } = require('vue-loader')
|
|||
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 cssSourceMapDev = env === 'development' && config.dev.cssSourceMap
|
||||
var cssSourceMapProd = env === 'production' && config.build.productionSourceMap
|
||||
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
|
||||
|
||||
var now = Date.now()
|
||||
|
@ -18,9 +18,12 @@ module.exports = {
|
|||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
|
||||
hashFunction: 'sha256', // Workaround for builds with OpenSSL 3.
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
publicPath:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsPublicPath
|
||||
: config.dev.assetsPublicPath,
|
||||
filename: '[name].js'
|
||||
},
|
||||
optimization: {
|
||||
|
@ -30,17 +33,15 @@ module.exports = {
|
|||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.vue', '.mjs'],
|
||||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
modules: [path.join(__dirname, '../node_modules')],
|
||||
fallback: {
|
||||
"url": require.resolve("url/"),
|
||||
url: require.resolve('url/')
|
||||
},
|
||||
alias: {
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components'),
|
||||
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'
|
||||
}
|
||||
},
|
||||
|
@ -66,14 +67,15 @@ module.exports = {
|
|||
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
|
||||
include: [
|
||||
// Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||
path.resolve(__dirname, '../src/i18n')
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: "javascript/auto"
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
|
@ -114,10 +116,8 @@ module.exports = {
|
|||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin()
|
||||
]
|
||||
plugins: [new VueLoaderPlugin()]
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ 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])
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(
|
||||
baseWebpackConfig.entry[name]
|
||||
)
|
||||
})
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
|
@ -20,10 +22,10 @@ module.exports = merge(baseWebpackConfig, {
|
|||
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
|
||||
COMMIT_HASH: JSON.stringify('DEV'),
|
||||
DEV_OVERRIDES: JSON.stringify(config.dev.settings),
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
|
|
|
@ -2,23 +2,27 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
const WorkboxPlugin = require('workbox-webpack-plugin');
|
||||
const WorkboxPlugin = require('workbox-webpack-plugin')
|
||||
var { merge } = require('webpack-merge')
|
||||
var baseWebpackConfig = require('./webpack.base.conf')
|
||||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var env = process.env.NODE_ENV === 'testing'
|
||||
var env =
|
||||
process.env.NODE_ENV === 'testing'
|
||||
? require('../config/test.env')
|
||||
: config.build.env
|
||||
|
||||
let commitHash = require('child_process')
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString();
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString()
|
||||
|
||||
var webpackConfig = merge(baseWebpackConfig, {
|
||||
mode: 'production',
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true })
|
||||
rules: utils.styleLoaders({
|
||||
sourceMap: config.dev.cssSourceMap,
|
||||
extract: true
|
||||
})
|
||||
},
|
||||
devtool: 'source-map',
|
||||
optimization: {
|
||||
|
@ -36,15 +40,15 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
||||
swDest: 'sw-pleroma.js',
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024
|
||||
}),
|
||||
// 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
|
||||
COMMIT_HASH: JSON.stringify(commitHash),
|
||||
DEV_OVERRIDES: JSON.stringify(undefined),
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false
|
||||
}),
|
||||
// extract css into its own file
|
||||
new MiniCssExtractPlugin({
|
||||
|
@ -54,9 +58,8 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
// 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,
|
||||
filename:
|
||||
process.env.NODE_ENV === 'testing' ? 'index.html' : config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
|
@ -69,7 +72,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'auto'
|
||||
}),
|
||||
})
|
||||
// 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
|
||||
|
@ -87,9 +90,7 @@ if (config.build.productionGzip) {
|
|||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
'\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
|
|
|
@ -38,7 +38,7 @@ module.exports = {
|
|||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
'/manifest.json': {
|
||||
'/manifest.json': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
|
@ -59,7 +59,7 @@ module.exports = {
|
|||
cookieDomainRewrite: 'localhost',
|
||||
ws: true,
|
||||
headers: {
|
||||
'Origin': target
|
||||
Origin: target
|
||||
}
|
||||
},
|
||||
'/oauth/revoke': {
|
||||
|
@ -76,7 +76,7 @@ module.exports = {
|
|||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
}
|
||||
},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
|
|
48
index.html
48
index.html
|
@ -1,18 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1,user-scalable=no"
|
||||
/>
|
||||
<title>Akkoma</title>
|
||||
<link rel="stylesheet" href="/static/font/css/fontello.css">
|
||||
<link rel="stylesheet" href="/static/font/css/animation.css">
|
||||
<link rel="stylesheet" href="/static/font/tiresias.css">
|
||||
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||
<link rel="stylesheet" href="/static/mfm.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/font/css/fontello.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/font/css/animation.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/font/tiresias.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/font/css/lato.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/mfm.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/custom.css"
|
||||
/>
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="/favicon.png"
|
||||
/>
|
||||
<link
|
||||
rel="manifest"
|
||||
href="/manifest.json"
|
||||
/>
|
||||
</head>
|
||||
<body class="hidden">
|
||||
<noscript>To use Akkoma, please enable JavaScript.</noscript>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"@vue/babel-plugin-jsx": "1.1.1",
|
||||
"@vue/compiler-sfc": "^3.1.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"autoprefixer": "6.7.7",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "^4.3.7",
|
||||
|
@ -67,11 +67,13 @@
|
|||
"css-loader": "^6.7.2",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
|
@ -101,9 +103,11 @@
|
|||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"ora": "0.4.1",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss-loader": "^7.0.2",
|
||||
"postcss-sass": "^0.5.0",
|
||||
"prettier": "2.8.1",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "^1.56.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
plugins: [require('autoprefixer')]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
"extends": ["config:base"]
|
||||
}
|
||||
|
|
83
src/App.js
83
src/App.js
|
@ -24,7 +24,9 @@ export default {
|
|||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
Notifications: defineAsyncComponent(() =>
|
||||
import('./components/notifications/notifications.vue')
|
||||
),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
@ -44,17 +46,20 @@ export default {
|
|||
data: () => ({
|
||||
mobileActivePanel: 'timeline'
|
||||
}),
|
||||
created () {
|
||||
created() {
|
||||
// Load the locale from the storage
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'interfaceLanguage',
|
||||
value: val
|
||||
})
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
classes() {
|
||||
return [
|
||||
{
|
||||
'-reverse': this.reverseLayout,
|
||||
|
@ -64,48 +69,76 @@ export default {
|
|||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
userBackground() {
|
||||
return this.currentUser.background_image
|
||||
},
|
||||
instanceBackground() {
|
||||
return this.mergedConfig.hideInstanceWallpaper
|
||||
? null
|
||||
: this.$store.state.instance.background
|
||||
},
|
||||
background () { return this.userBackground || this.instanceBackground },
|
||||
bgStyle () {
|
||||
background() {
|
||||
return this.userBackground || this.instanceBackground
|
||||
},
|
||||
bgStyle() {
|
||||
if (this.background) {
|
||||
return {
|
||||
'--body-background-image': `url(${this.background})`
|
||||
}
|
||||
}
|
||||
},
|
||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
suggestionsEnabled() {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
newPostButtonShown () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
newPostButtonShown() {
|
||||
return (
|
||||
this.$store.getters.mergedConfig.alwaysShowNewPostButton ||
|
||||
this.layoutType === 'mobile'
|
||||
)
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
layoutType () { return this.$store.state.interface.layoutType },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
reverseLayout () {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
||||
showFeaturesPanel() {
|
||||
return this.$store.state.instance.showFeaturesPanel
|
||||
},
|
||||
editingAvailable() {
|
||||
return this.$store.state.instance.editingAvailable
|
||||
},
|
||||
layoutType() {
|
||||
return this.$store.state.interface.layoutType
|
||||
},
|
||||
privateMode() {
|
||||
return this.$store.state.instance.private
|
||||
},
|
||||
reverseLayout() {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } =
|
||||
this.$store.getters.mergedConfig
|
||||
if (this.layoutType !== 'wide') {
|
||||
return reverseSetting
|
||||
} else {
|
||||
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
||||
return thirdColumnMode === 'notifications'
|
||||
? reverseSetting
|
||||
: !reverseSetting
|
||||
}
|
||||
},
|
||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||
noSticky() {
|
||||
return this.$store.getters.mergedConfig.disableStickyHeaders
|
||||
},
|
||||
showScrollbars() {
|
||||
return this.$store.getters.mergedConfig.showScrollbars
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
updateMobileState () {
|
||||
updateMobileState() {
|
||||
this.$store.dispatch('setLayoutWidth', windowWidth())
|
||||
this.$store.dispatch('setLayoutHeight', windowHeight())
|
||||
}
|
||||
|
|
125
src/App.scss
125
src/App.scss
|
@ -12,8 +12,8 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-family: var(--interfaceFont, sans-serif);
|
||||
font-family: $system-sans-serif;
|
||||
font-family: var(--interfaceFont, $system-sans-serif);
|
||||
margin: 0;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
|
@ -22,84 +22,13 @@ body {
|
|||
overscroll-behavior-y: none;
|
||||
overflow-x: clip;
|
||||
overflow-y: scroll;
|
||||
background: var(--bg);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ## Custom scrollbars
|
||||
// Only show custom scrollbars on devices which
|
||||
// have a cursor/pointer to operate them
|
||||
@media (any-pointer: fine) {
|
||||
* {
|
||||
scrollbar-color: var(--btn) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button,
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--btn);
|
||||
box-shadow: var(--buttonShadow);
|
||||
border-radius: var(--btnRadius);
|
||||
}
|
||||
|
||||
// horizontal/vertical/increment/decrement are webkit-specific stuff
|
||||
// that indicates whether we're affecting vertical scrollbar, increase button etc
|
||||
// stylelint-disable selector-pseudo-class-no-unknown
|
||||
&::-webkit-scrollbar-button {
|
||||
--___bgPadding: 2px;
|
||||
|
||||
color: var(--btnText);
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
|
||||
&:horizontal {
|
||||
background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));
|
||||
|
||||
&:increment {
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--btnText) 50%, transparent 51%),
|
||||
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%);
|
||||
background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);
|
||||
}
|
||||
|
||||
&:decrement {
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--btnText) 51%),
|
||||
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%);
|
||||
background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);
|
||||
}
|
||||
}
|
||||
|
||||
&:vertical {
|
||||
background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;
|
||||
|
||||
&:increment {
|
||||
background-image:
|
||||
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%),
|
||||
linear-gradient(45deg, transparent 50%, var(--btnText) 51%);
|
||||
background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;
|
||||
}
|
||||
|
||||
&:decrement {
|
||||
background-image:
|
||||
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%),
|
||||
linear-gradient(45deg, var(--btnText) 50%, transparent 51%);
|
||||
background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
// stylelint-enable selector-pseudo-class-no-unknown
|
||||
}
|
||||
// Body should have background to scrollbar otherwise it will use white (body color?)
|
||||
html {
|
||||
scrollbar-color: var(--selectedMenu) var(--wallpaper);
|
||||
background: var(--wallpaper);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $fallback--link;
|
||||
|
@ -110,7 +39,7 @@ h4 {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
i[class*='icon-'],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--icon;
|
||||
color: var(--icon, $fallback--icon);
|
||||
|
@ -126,8 +55,9 @@ nav {
|
|||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
|
||||
box-shadow: var(--topBarShadow);
|
||||
box-sizing: border-box;
|
||||
height: var(--navbar-height);
|
||||
height: calc(var(--navbar-height) + 1px);
|
||||
position: fixed;
|
||||
backdrop-filter: blur(12px) saturate(1.2);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
|
@ -182,7 +112,7 @@ nav {
|
|||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: var(--miniColumn) var(--maxiColumn);
|
||||
grid-template-areas: "sidebar content";
|
||||
grid-template-areas: 'sidebar content';
|
||||
grid-template-rows: 1fr;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
|
@ -228,7 +158,9 @@ nav {
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-left: calc(var(--___paddingIncrease) * -1);
|
||||
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
padding-left: calc(
|
||||
var(--___paddingIncrease) + var(--___columnMargin) / 2
|
||||
);
|
||||
|
||||
// On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode
|
||||
// might implement old style of hiding scrollbars later if there's demand
|
||||
|
@ -236,7 +168,9 @@ nav {
|
|||
&:not(.-show-scrollbar) {
|
||||
scrollbar-width: none;
|
||||
margin-right: calc(var(--___paddingIncrease) * -1);
|
||||
padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
padding-right: calc(
|
||||
var(--___paddingIncrease) + var(--___columnMargin) / 2
|
||||
);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: block;
|
||||
|
@ -276,21 +210,21 @@ nav {
|
|||
|
||||
&.-reverse:not(.-wide):not(.-mobile) {
|
||||
grid-template-columns: var(--maxiColumn) var(--miniColumn);
|
||||
grid-template-areas: "content sidebar";
|
||||
grid-template-areas: 'content sidebar';
|
||||
}
|
||||
|
||||
&.-wide {
|
||||
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
|
||||
grid-template-areas: "sidebar content notifs";
|
||||
grid-template-areas: 'sidebar content notifs';
|
||||
|
||||
&.-reverse {
|
||||
grid-template-areas: "notifs content sidebar";
|
||||
grid-template-areas: 'notifs content sidebar';
|
||||
}
|
||||
}
|
||||
|
||||
&.-mobile {
|
||||
grid-template-columns: 100vw;
|
||||
grid-template-areas: "content";
|
||||
grid-template-areas: 'content';
|
||||
padding: 0;
|
||||
|
||||
.column {
|
||||
|
@ -347,7 +281,7 @@ nav {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
i[class*='icon-'],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
|
@ -363,7 +297,9 @@ nav {
|
|||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
|
||||
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
|
||||
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
|
@ -396,7 +332,9 @@ nav {
|
|||
color: var(--btnToggledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggled, $fallback--fg);
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
|
||||
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
|
||||
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
|
||||
svg,
|
||||
|
@ -461,7 +399,8 @@ textarea,
|
|||
border: none;
|
||||
border-radius: $fallback--inputRadius;
|
||||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
|
||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
|
||||
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset, 0 0 2px 0 rgba(0, 0, 0, 1) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
@ -479,13 +418,13 @@ textarea,
|
|||
padding: 0 var(--_padding);
|
||||
|
||||
&:disabled,
|
||||
&[disabled=disabled],
|
||||
&[disabled='disabled'],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&[type=range] {
|
||||
&[type='range'] {
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
@ -493,7 +432,7 @@ textarea,
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
&[type=radio] {
|
||||
&[type='radio'] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
|
@ -533,7 +472,7 @@ textarea,
|
|||
}
|
||||
}
|
||||
|
||||
&[type=checkbox] {
|
||||
&[type='checkbox'] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
|
@ -594,8 +533,8 @@ option {
|
|||
.hide-number-spinner {
|
||||
-moz-appearance: textfield;
|
||||
|
||||
&[type=number]::-webkit-inner-spin-button,
|
||||
&[type=number]::-webkit-outer-spin-button {
|
||||
&[type='number']::-webkit-inner-spin-button,
|
||||
&[type='number']::-webkit-outer-spin-button {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
:to="{ name: 'login' }"
|
||||
class="panel-body"
|
||||
>
|
||||
{{ $t("login.hint") }}
|
||||
{{ $t('login.hint') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<router-view />
|
||||
|
|
|
@ -4,7 +4,7 @@ $darkened-background: whitesmoke;
|
|||
|
||||
$fallback--bg: #121a24;
|
||||
$fallback--fg: #182230;
|
||||
$fallback--faint: rgba(185, 185, 186, .5);
|
||||
$fallback--faint: rgba(185, 185, 186, 0.5);
|
||||
$fallback--text: #b9b9ba;
|
||||
$fallback--link: #d8a070;
|
||||
$fallback--icon: #666;
|
||||
|
@ -16,8 +16,8 @@ $fallback--cBlue: #0095ff;
|
|||
$fallback--cGreen: #0fa00f;
|
||||
$fallback--cOrange: orange;
|
||||
|
||||
$fallback--alertError: rgba(211,16,20,.5);
|
||||
$fallback--alertWarning: rgba(111,111,20,.5);
|
||||
$fallback--alertError: rgba(211, 16, 20, 0.5);
|
||||
$fallback--alertWarning: rgba(111, 111, 20, 0.5);
|
||||
|
||||
$fallback--panelRadius: 10px;
|
||||
$fallback--checkboxRadius: 2px;
|
||||
|
@ -28,6 +28,14 @@ $fallback--avatarRadius: 4px;
|
|||
$fallback--avatarAltRadius: 10px;
|
||||
$fallback--attachmentRadius: 10px;
|
||||
|
||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1),
|
||||
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
|
||||
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
$system-sans-serif: -apple-system, BlinkMacSystemFont, avenir next, avenir,
|
||||
segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
|
||||
sans-serif;
|
||||
$system-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,
|
||||
monospace;
|
||||
|
|
|
@ -3,13 +3,19 @@ import { createApp } from 'vue'
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeLayers
|
||||
} from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import App from '../App.vue'
|
||||
import routes from './routes'
|
||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||
|
||||
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
|
||||
import {
|
||||
windowWidth,
|
||||
windowHeight
|
||||
} from '../services/window_utils/window_utils'
|
||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
|
@ -23,7 +29,9 @@ const parsedInitialResults = () => {
|
|||
return null
|
||||
}
|
||||
if (!staticInitialResults) {
|
||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||
staticInitialResults = JSON.parse(
|
||||
document.getElementById('initial-results').textContent
|
||||
)
|
||||
}
|
||||
return staticInitialResults
|
||||
}
|
||||
|
@ -71,18 +79,30 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const textlimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'textlimit',
|
||||
value: textlimit
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'accountApprovalRequired',
|
||||
value: data.approval_required
|
||||
})
|
||||
// don't override cookie if set
|
||||
if (!Cookies.get('userLanguage')) {
|
||||
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
|
||||
store.dispatch('setOption', {
|
||||
name: 'interfaceLanguage',
|
||||
value: resolveLanguage(data.languages)
|
||||
})
|
||||
}
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'vapidPublicKey',
|
||||
value: vapidPublicKey
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load instance config, potentially fatal')
|
||||
|
@ -97,10 +117,12 @@ const getBackendProvidedConfig = async ({ store }) => {
|
|||
const data = await res.json()
|
||||
return data.pleroma_fe
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||
console.error(
|
||||
'Could not load backend-provided frontend config, potentially fatal'
|
||||
)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +133,7 @@ const getStaticConfig = async () => {
|
|||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
|
@ -154,16 +176,12 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
value: typeof config.logoMask === 'undefined'
|
||||
? true
|
||||
: config.logoMask
|
||||
value: typeof config.logoMask === 'undefined' ? true : config.logoMask
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMargin',
|
||||
value: typeof config.logoMargin === 'undefined'
|
||||
? 0
|
||||
: config.logoMargin
|
||||
value: typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
|
@ -191,7 +209,7 @@ const getTOS = async ({ store }) => {
|
|||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load TOS")
|
||||
|
@ -204,9 +222,12 @@ const getInstancePanel = async ({ store }) => {
|
|||
const res = await preloadFetch('/instance/panel.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'instanceSpecificPanelContent',
|
||||
value: html
|
||||
})
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load instance panel")
|
||||
|
@ -219,25 +240,30 @@ const getStickers = async ({ store }) => {
|
|||
const res = await window.fetch('/static/stickers.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const stickers = (await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
var meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
const stickers = (
|
||||
await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
var meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)
|
||||
).sort((a, b) => {
|
||||
return a.meta.title.localeCompare(b.meta.title)
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'stickers',
|
||||
value: stickers
|
||||
})
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load stickers")
|
||||
|
@ -252,13 +278,19 @@ const getAppSecret = async ({ store }) => {
|
|||
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
||||
.then((token) => {
|
||||
commit('setAppToken', token.access_token)
|
||||
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
commit(
|
||||
'setBackendInteractor',
|
||||
backendInteractorService(store.getters.getToken())
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
const nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'staffAccounts',
|
||||
value: nicknames
|
||||
})
|
||||
}
|
||||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
|
@ -268,65 +300,137 @@ const getNodeInfo = async ({ store }) => {
|
|||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
const features = metadata.features
|
||||
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'name',
|
||||
value: metadata.nodeName
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'registrationOpen',
|
||||
value: data.openRegistrations
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'mediaProxyAvailable',
|
||||
value: features.includes('media_proxy')
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'safeDM',
|
||||
value: features.includes('safe_dm_mentions')
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pollsAvailable',
|
||||
value: features.includes('polls')
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'editingAvailable',
|
||||
value: features.includes('editing')
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pollLimits',
|
||||
value: metadata.pollLimits
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'mailerEnabled',
|
||||
value: metadata.mailerEnabled
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'translationEnabled',
|
||||
value: features.includes('akkoma:machine_translation')
|
||||
})
|
||||
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
||||
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'uploadlimit',
|
||||
value: parseInt(uploadLimits.general)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'avatarlimit',
|
||||
value: parseInt(uploadLimits.avatar)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'backgroundlimit',
|
||||
value: parseInt(uploadLimits.background)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'bannerlimit',
|
||||
value: parseInt(uploadLimits.banner)
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'fieldsLimits',
|
||||
value: metadata.fieldsLimits
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'restrictedNicknames',
|
||||
value: metadata.restrictedNicknames
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'postFormats',
|
||||
value: metadata.postFormats
|
||||
})
|
||||
|
||||
const suggestions = metadata.suggestions
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'suggestionsEnabled',
|
||||
value: suggestions.enabled
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'suggestionsWeb',
|
||||
value: suggestions.web
|
||||
})
|
||||
|
||||
const software = data.software
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'backendVersion',
|
||||
value: software.version
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pleromaBackend',
|
||||
value: software.name === 'pleroma'
|
||||
})
|
||||
|
||||
const priv = metadata.private
|
||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'frontendVersion',
|
||||
value: frontendVersion
|
||||
})
|
||||
|
||||
const federation = metadata.federation
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'tagPolicyAvailable',
|
||||
value: typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
value:
|
||||
typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federationPolicy',
|
||||
value: federation
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'localBubbleInstances',
|
||||
value: metadata.localBubbleInstances
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
value: typeof federation.enabled === 'undefined'
|
||||
? true
|
||||
: federation.enabled
|
||||
value:
|
||||
typeof federation.enabled === 'undefined' ? true : federation.enabled
|
||||
})
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'accountActivationRequired',
|
||||
value: accountActivationRequired
|
||||
})
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load nodeinfo')
|
||||
|
@ -336,11 +440,16 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const setConfig = async ({ store }) => {
|
||||
// apiConfig, staticConfig
|
||||
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||
const configInfos = await Promise.all([
|
||||
getBackendProvidedConfig({ store }),
|
||||
getStaticConfig()
|
||||
])
|
||||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(
|
||||
getAppSecret({ store })
|
||||
)
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
|
@ -363,7 +472,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
FaviconService.initFaviconService()
|
||||
|
||||
const overrides = window.___pleromafe_dev_overrides || {}
|
||||
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||
const server =
|
||||
typeof overrides.target !== 'undefined'
|
||||
? overrides.target
|
||||
: window.location.origin
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
|
||||
await setConfig({ store })
|
||||
|
@ -373,7 +485,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
const customThemePresent = customThemeSource || customTheme
|
||||
|
||||
if (customThemePresent) {
|
||||
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
|
||||
if (
|
||||
customThemeSource &&
|
||||
customThemeSource.themeEngineVersion === CURRENT_VERSION
|
||||
) {
|
||||
applyTheme(customThemeSource)
|
||||
} else {
|
||||
applyTheme(customTheme)
|
||||
|
@ -404,7 +519,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
history: createWebHistory(),
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
if (to.matched.some((m) => m.meta.dontScroll)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,51 +35,145 @@ export default (store) => {
|
|||
}
|
||||
|
||||
let routes = [
|
||||
{ name: 'root',
|
||||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect: _to => {
|
||||
return (store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
redirect: (_to) => {
|
||||
return (
|
||||
(store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
)
|
||||
}
|
||||
},
|
||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{
|
||||
name: 'public-external-timeline',
|
||||
path: '/main/all',
|
||||
component: PublicAndExternalTimeline
|
||||
},
|
||||
{
|
||||
name: 'public-timeline',
|
||||
path: '/main/public',
|
||||
component: PublicTimeline
|
||||
},
|
||||
{
|
||||
name: 'bubble-timeline',
|
||||
path: '/main/bubble',
|
||||
component: BubbleTimeline
|
||||
},
|
||||
{
|
||||
name: 'friends',
|
||||
path: '/main/friends',
|
||||
component: FriendsTimeline,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'remote-user-profile-acct',
|
||||
{
|
||||
name: 'conversation',
|
||||
path: '/notice/:id',
|
||||
component: ConversationPage,
|
||||
meta: { dontScroll: true }
|
||||
},
|
||||
{
|
||||
name: 'remote-user-profile-acct',
|
||||
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'remote-user-profile',
|
||||
{
|
||||
name: 'remote-user-profile',
|
||||
path: '/remote-users/:hostname/:username',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{
|
||||
name: 'external-user-profile',
|
||||
path: '/users/:id',
|
||||
component: UserProfile,
|
||||
meta: { dontScroll: true }
|
||||
},
|
||||
{
|
||||
name: 'interactions',
|
||||
path: '/users/:username/interactions',
|
||||
component: Interactions,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'dms',
|
||||
path: '/users/:username/dms',
|
||||
component: DMs,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{ name: 'registration-request-sent', path: '/registration-request-sent', component: RegistrationRequestSent },
|
||||
{ name: 'awaiting-email-confirmation', path: '/awaiting-email-confirmation', component: AwaitingEmailConfirmation },
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{
|
||||
name: 'registration-request-sent',
|
||||
path: '/registration-request-sent',
|
||||
component: RegistrationRequestSent
|
||||
},
|
||||
{
|
||||
name: 'awaiting-email-confirmation',
|
||||
path: '/awaiting-email-confirmation',
|
||||
component: AwaitingEmailConfirmation
|
||||
},
|
||||
{
|
||||
name: 'password-reset',
|
||||
path: '/password-reset',
|
||||
component: PasswordReset,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
name: 'registration-token',
|
||||
path: '/registration/:token',
|
||||
component: Registration
|
||||
},
|
||||
{
|
||||
name: 'friend-requests',
|
||||
path: '/friend-requests',
|
||||
component: FollowRequests,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
path: '/:username/notifications',
|
||||
component: Notifications,
|
||||
props: () => ({ disableTeleport: true }),
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{
|
||||
name: 'oauth-callback',
|
||||
path: '/oauth-callback',
|
||||
component: OAuthCallback,
|
||||
props: (route) => ({ code: route.query.code })
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: '/search',
|
||||
component: Search,
|
||||
props: (route) => ({ query: route.query.query })
|
||||
},
|
||||
{
|
||||
name: 'who-to-follow',
|
||||
path: '/who-to-follow',
|
||||
component: WhoToFollow,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: 'lists', path: '/lists', component: Lists },
|
||||
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
|
||||
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } }
|
||||
{
|
||||
name: 'announcements',
|
||||
path: '/announcements',
|
||||
component: AnnouncementsPage
|
||||
},
|
||||
{
|
||||
name: 'user-profile',
|
||||
path: '/:_(users)?/:name',
|
||||
component: UserProfile,
|
||||
meta: { dontScroll: true }
|
||||
}
|
||||
]
|
||||
|
||||
return routes
|
||||
|
|
|
@ -15,13 +15,17 @@ const About = {
|
|||
LocalBubblePanel
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
showFeaturesPanel() {
|
||||
return this.$store.state.instance.showFeaturesPanel
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
showLocalBubblePanel () {
|
||||
showLocalBubblePanel() {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./about.js" ></script>
|
||||
<script src="./about.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
<style lang="scss"></style>
|
||||
|
|
|
@ -3,19 +3,13 @@ import Popover from '../popover/popover.vue'
|
|||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { mapState } from 'vuex'
|
||||
import {
|
||||
faEllipsisV
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faEllipsisV
|
||||
)
|
||||
library.add(faEllipsisV)
|
||||
|
||||
const AccountActions = {
|
||||
props: [
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
props: ['user', 'relationship'],
|
||||
data() {
|
||||
return {
|
||||
showingConfirmBlock: false
|
||||
}
|
||||
|
@ -26,56 +20,59 @@ const AccountActions = {
|
|||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
refetchRelationship () {
|
||||
refetchRelationship() {
|
||||
return this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
showConfirmBlock () {
|
||||
showConfirmBlock() {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmBlock () {
|
||||
hideConfirmBlock() {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats () {
|
||||
showRepeats() {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
hideRepeats () {
|
||||
hideRepeats() {
|
||||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
blockUser() {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showConfirmBlock()
|
||||
}
|
||||
},
|
||||
doBlockUser () {
|
||||
doBlockUser() {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser () {
|
||||
unblockUser() {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers () {
|
||||
removeUserFromFollowers() {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
},
|
||||
reportUser () {
|
||||
reportUser() {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||
},
|
||||
muteDomain () {
|
||||
this.$store.dispatch('muteDomain', this.user.screen_name.split('@')[1])
|
||||
muteDomain() {
|
||||
this.$store
|
||||
.dispatch('muteDomain', this.user.screen_name.split('@')[1])
|
||||
.then(() => this.refetchRelationship())
|
||||
},
|
||||
unmuteDomain () {
|
||||
this.$store.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
|
||||
unmuteDomain() {
|
||||
this.$store
|
||||
.dispatch('unmuteDomain', this.user.screen_name.split('@')[1])
|
||||
.then(() => this.refetchRelationship())
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock () {
|
||||
shouldConfirmBlock() {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
pleromaChatMessagesAvailable: (state) =>
|
||||
state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="relationship.following">
|
||||
<button
|
||||
|
@ -71,7 +71,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled ellipsis-button">
|
||||
<FAIcon
|
||||
class="icon"
|
||||
|
@ -93,10 +93,8 @@
|
|||
keypath="user_card.block_confirm"
|
||||
tag="span"
|
||||
>
|
||||
<template v-slot:user>
|
||||
<span
|
||||
v-text="user.screen_name_ui"
|
||||
/>
|
||||
<template #user>
|
||||
<span v-text="user.screen_name_ui" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
|
|
|
@ -8,7 +8,7 @@ const Announcement = {
|
|||
AnnouncementEditor,
|
||||
RichContent
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
|
@ -25,78 +25,93 @@ const Announcement = {
|
|||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser
|
||||
}),
|
||||
content () {
|
||||
content() {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead () {
|
||||
isRead() {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt () {
|
||||
publishedAt() {
|
||||
const time = this.announcement['published_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
},
|
||||
startsAt () {
|
||||
startsAt() {
|
||||
const time = this.announcement['starts_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
},
|
||||
endsAt () {
|
||||
endsAt() {
|
||||
const time = this.announcement['ends_at']
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
},
|
||||
inactive () {
|
||||
inactive() {
|
||||
return this.announcement.inactive
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsRead () {
|
||||
markAsRead() {
|
||||
if (!this.isRead) {
|
||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||
return this.$store.dispatch(
|
||||
'markAnnouncementAsRead',
|
||||
this.announcement.id
|
||||
)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement () {
|
||||
deleteAnnouncement() {
|
||||
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate (time, locale) {
|
||||
formatTimeOrDate(time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
return this.announcement['all_day']
|
||||
? d.toLocaleDateString(locale)
|
||||
: d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode () {
|
||||
enterEditMode() {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
|
||||
this.editedAnnouncement.startsAt = this.announcement['starts_at']
|
||||
this.editedAnnouncement.endsAt = this.announcement['ends_at']
|
||||
this.editedAnnouncement.allDay = this.announcement['all_day']
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit () {
|
||||
this.$store.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
submitEdit() {
|
||||
this.$store
|
||||
.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit () {
|
||||
cancelEdit() {
|
||||
this.editing = false
|
||||
},
|
||||
clearError () {
|
||||
clearError() {
|
||||
this.editError = undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
class="times"
|
||||
>
|
||||
<span v-if="publishedAt">
|
||||
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
|
||||
{{
|
||||
$t('announcements.published_time_display', { time: publishedAt })
|
||||
}}
|
||||
</span>
|
||||
<span v-if="startsAt">
|
||||
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
||||
|
@ -99,7 +101,7 @@
|
|||
<script src="./announcement.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
@import '../../variables';
|
||||
|
||||
.announcement {
|
||||
border-bottom-width: 1px;
|
||||
|
@ -108,7 +110,8 @@
|
|||
border-radius: 0;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
.heading,
|
||||
.body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,22 +10,26 @@
|
|||
:disabled="disabled"
|
||||
/>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
|
||||
<label for="announcement-start-time">{{
|
||||
$t('announcements.start_time_prompt')
|
||||
}}</label>
|
||||
<input
|
||||
id="announcement-start-time"
|
||||
v-model="announcement.startsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
/>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
|
||||
<label for="announcement-end-time">{{
|
||||
$t('announcements.end_time_prompt')
|
||||
}}</label>
|
||||
<input
|
||||
id="announcement-end-time"
|
||||
v-model="announcement.endsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
/>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<Checkbox
|
||||
|
@ -33,7 +37,9 @@
|
|||
v-model="announcement.allDay"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||
<label for="announcement-all-day">{{
|
||||
$t('announcements.all_day_prompt')
|
||||
}}</label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -7,7 +7,7 @@ const AnnouncementsPage = {
|
|||
Announcement,
|
||||
AnnouncementEditor
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
|
@ -19,34 +19,35 @@ const AnnouncementsPage = {
|
|||
error: undefined
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.$store.dispatch('fetchAnnouncements')
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser
|
||||
}),
|
||||
announcements () {
|
||||
announcements() {
|
||||
return this.$store.state.announcements.announcements
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement () {
|
||||
postAnnouncement() {
|
||||
this.posting = true
|
||||
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
this.$store
|
||||
.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
clearError() {
|
||||
this.error = undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
>
|
||||
<section v-if="currentUser && currentUser.role === 'admin'">
|
||||
<div class="post-form">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
||||
|
@ -50,9 +48,7 @@
|
|||
v-for="announcement in announcements"
|
||||
:key="announcement.id"
|
||||
>
|
||||
<announcement
|
||||
:announcement="announcement"
|
||||
/>
|
||||
<announcement :announcement="announcement" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,13 +57,14 @@
|
|||
<script src="./announcements_page.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
@import '../../variables';
|
||||
|
||||
.announcements-page {
|
||||
.post-form {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
.heading,
|
||||
.body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
export default {
|
||||
emits: ['resetAsyncComponent'],
|
||||
methods: {
|
||||
retry () {
|
||||
retry() {
|
||||
this.$emit('resetAsyncComponent')
|
||||
}
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ export default {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
.btn {
|
||||
margin: .5em;
|
||||
padding: .5em 2em;
|
||||
margin: 0.5em;
|
||||
padding: 0.5em 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -46,14 +46,16 @@ const Attachment = {
|
|||
'shiftDn',
|
||||
'edit'
|
||||
],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
img:
|
||||
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
|
||||
document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
|
@ -66,7 +68,7 @@ const Attachment = {
|
|||
VideoAttachment
|
||||
},
|
||||
computed: {
|
||||
classNames () {
|
||||
classNames() {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
|
@ -78,37 +80,37 @@ const Attachment = {
|
|||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
]
|
||||
},
|
||||
usePlaceholder () {
|
||||
usePlaceholder() {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit () {
|
||||
useContainFit() {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
placeholderName () {
|
||||
placeholderName() {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
return this.type.toUpperCase()
|
||||
}
|
||||
return this.attachment.description
|
||||
},
|
||||
placeholderIconClass () {
|
||||
placeholderIconClass() {
|
||||
if (this.type === 'image') return 'image'
|
||||
if (this.type === 'video') return 'video'
|
||||
if (this.type === 'audio') return 'music'
|
||||
return 'file'
|
||||
},
|
||||
referrerpolicy () {
|
||||
referrerpolicy() {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
type () {
|
||||
type() {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
},
|
||||
hidden () {
|
||||
hidden() {
|
||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty () {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
isEmpty() {
|
||||
return this.type === 'html' && !this.attachment.oembed
|
||||
},
|
||||
useModal () {
|
||||
useModal() {
|
||||
let modalTypes = []
|
||||
switch (this.size) {
|
||||
case 'hide':
|
||||
|
@ -123,29 +125,29 @@ const Attachment = {
|
|||
}
|
||||
return modalTypes.includes(this.type)
|
||||
},
|
||||
videoTag () {
|
||||
videoTag() {
|
||||
return this.useModal ? 'button' : 'span'
|
||||
},
|
||||
statusForm () {
|
||||
statusForm() {
|
||||
return this.$parent.$parent
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
watch: {
|
||||
'attachment.description' (newVal) {
|
||||
'attachment.description'(newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription (newVal) {
|
||||
localDescription(newVal) {
|
||||
this.onEdit(newVal)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkClicked ({ target }) {
|
||||
linkClicked({ target }) {
|
||||
if (target.tagName === 'A') {
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
openModal (event) {
|
||||
openModal(event) {
|
||||
if (this.useModal) {
|
||||
this.$emit('setMedia')
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
|
@ -153,34 +155,35 @@ const Attachment = {
|
|||
window.open(this.attachment.url)
|
||||
}
|
||||
},
|
||||
openModalForce (event) {
|
||||
openModalForce(event) {
|
||||
this.$emit('setMedia')
|
||||
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||
},
|
||||
onEdit (event) {
|
||||
onEdit(event) {
|
||||
this.edit && this.edit(this.attachment, event)
|
||||
},
|
||||
onRemove () {
|
||||
onRemove() {
|
||||
this.remove && this.remove(this.attachment)
|
||||
},
|
||||
onShiftUp () {
|
||||
onShiftUp() {
|
||||
this.shiftUp && this.shiftUp(this.attachment)
|
||||
},
|
||||
onShiftDn () {
|
||||
onShiftDn() {
|
||||
this.shiftDn && this.shiftDn(this.attachment)
|
||||
},
|
||||
stopFlash () {
|
||||
stopFlash() {
|
||||
this.$refs.flash.closePlayer()
|
||||
},
|
||||
setFlashLoaded (event) {
|
||||
setFlashLoaded(event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription () {
|
||||
toggleDescription() {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden (event) {
|
||||
toggleHidden(event) {
|
||||
if (
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
!this.showHidden &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
|
@ -201,14 +204,16 @@ const Attachment = {
|
|||
this.showHidden = !this.showHidden
|
||||
}
|
||||
},
|
||||
onImageLoad (image) {
|
||||
onImageLoad(image) {
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||
},
|
||||
resize (e) {
|
||||
resize(e) {
|
||||
const target = e.target || e
|
||||
if (!(target instanceof window.Element)) { return }
|
||||
if (!(target instanceof window.Element)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Reset to default height for empty form, nothing else to do here.
|
||||
if (target.value === '') {
|
||||
|
@ -219,14 +224,16 @@ const Attachment = {
|
|||
|
||||
const paddingString = getComputedStyle(target)['padding']
|
||||
// remove -px suffix
|
||||
const padding = Number(paddingString.substring(0, paddingString.length - 2))
|
||||
const padding = Number(
|
||||
paddingString.substring(0, paddingString.length - 2)
|
||||
)
|
||||
|
||||
target.style.height = 'auto'
|
||||
const newHeight = Math.floor(target.scrollHeight - padding * 2)
|
||||
target.style.height = `${newHeight}px`
|
||||
this.$emit('resize', newHeight)
|
||||
},
|
||||
postStatus (event) {
|
||||
postStatus(event) {
|
||||
this.statusForm.postStatus(event, this.statusForm.newStatus)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,7 +117,6 @@
|
|||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
@click.prevent
|
||||
>
|
||||
<FAIcon :icon="placeholderIconClass" />
|
||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
||||
<b>{{ nsfw ? 'NSFW / ' : '' }}</b
|
||||
>{{ edit ? '' : placeholderName }}
|
||||
</a>
|
||||
<div
|
||||
v-if="edit || remove"
|
||||
|
@ -30,7 +31,11 @@
|
|||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||
v-if="
|
||||
size !== 'hide' &&
|
||||
!hideDescription &&
|
||||
(edit || localDescription || showDescription)
|
||||
"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
|
@ -41,7 +46,7 @@
|
|||
class="description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
/>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
|
@ -68,7 +73,7 @@
|
|||
:key="nsfwImage"
|
||||
class="nsfw"
|
||||
:src="nsfwImage"
|
||||
>
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="type === 'video'"
|
||||
class="play-icon"
|
||||
|
@ -88,7 +93,12 @@
|
|||
<FAIcon icon="stop" />
|
||||
</button>
|
||||
<button
|
||||
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||
v-if="
|
||||
attachment.description &&
|
||||
size !== 'small' &&
|
||||
!edit &&
|
||||
type !== 'unknown'
|
||||
"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
|
@ -140,7 +150,7 @@
|
|||
<a
|
||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||
class="image-container"
|
||||
:class="{'-hidden': hidden && preloadImage }"
|
||||
:class="{ '-hidden': hidden && preloadImage }"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
@click.stop.prevent="openModal"
|
||||
|
@ -218,11 +228,13 @@
|
|||
v-if="attachment.thumb_url"
|
||||
class="image"
|
||||
>
|
||||
<img :src="attachment.thumb_url">
|
||||
<img :src="attachment.thumb_url" />
|
||||
</div>
|
||||
<div class="text">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
||||
<h1>
|
||||
<a :href="attachment.url">{{ attachment.oembed.title }}</a>
|
||||
</h1>
|
||||
<div v-html="attachment.oembed.oembedHTML" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
|
@ -244,7 +256,11 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||
v-if="
|
||||
size !== 'hide' &&
|
||||
!hideDescription &&
|
||||
(edit || (localDescription && showDescription))
|
||||
"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
|
|
|
@ -6,13 +6,17 @@ import { mapGetters } from 'vuex'
|
|||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render () {
|
||||
render() {
|
||||
return h(resolveComponent(this.authForm))
|
||||
},
|
||||
computed: {
|
||||
authForm () {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
authForm() {
|
||||
if (this.requiredTOTP) {
|
||||
return 'MFATOTPForm'
|
||||
}
|
||||
if (this.requiredRecovery) {
|
||||
return 'MFARecoveryForm'
|
||||
}
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
|
|
|
@ -2,11 +2,13 @@ const debounceMilliseconds = 500
|
|||
|
||||
export default {
|
||||
props: {
|
||||
query: { // function to query results and return a promise
|
||||
query: {
|
||||
// function to query results and return a promise
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
filter: { // function to filter results in real time
|
||||
filter: {
|
||||
// function to filter results in real time
|
||||
type: Function
|
||||
},
|
||||
placeholder: {
|
||||
|
@ -14,7 +16,7 @@ export default {
|
|||
default: 'Search...'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
term: '',
|
||||
timeout: null,
|
||||
|
@ -23,29 +25,31 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
filtered () {
|
||||
filtered() {
|
||||
return this.filter ? this.filter(this.results) : this.results
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
term (val) {
|
||||
term(val) {
|
||||
this.fetchResults(val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchResults (term) {
|
||||
fetchResults(term) {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.results = []
|
||||
if (term) {
|
||||
this.query(term).then((results) => { this.results = results })
|
||||
this.query(term).then((results) => {
|
||||
this.results = results
|
||||
})
|
||||
}
|
||||
}, debounceMilliseconds)
|
||||
},
|
||||
onInputClick () {
|
||||
onInputClick() {
|
||||
this.resultsVisible = true
|
||||
},
|
||||
onClickOutside () {
|
||||
onClickOutside() {
|
||||
this.resultsVisible = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
:placeholder="placeholder"
|
||||
class="autosuggest-input"
|
||||
@click="onInputClick"
|
||||
>
|
||||
/>
|
||||
<div
|
||||
v-if="resultsVisible && filtered.length > 0"
|
||||
class="autosuggest-results"
|
||||
|
|
|
@ -4,7 +4,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
|||
const AvatarList = {
|
||||
props: ['users'],
|
||||
computed: {
|
||||
slicedUsers () {
|
||||
slicedUsers() {
|
||||
return this.users ? this.users.slice(0, 15) : []
|
||||
}
|
||||
},
|
||||
|
@ -12,8 +12,12 @@ const AvatarList = {
|
|||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./avatar_list.js" ></script>
|
||||
<script src="./avatar_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export default {
|
||||
computed: {
|
||||
}
|
||||
computed: {}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,8 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
data () {
|
||||
props: ['user'],
|
||||
data() {
|
||||
return {
|
||||
userExpanded: false
|
||||
}
|
||||
|
@ -18,11 +16,15 @@ const BasicUserCard = {
|
|||
RichContent
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
toggleUserExpanded() {
|
||||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,19 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
|||
|
||||
const BlockCard = {
|
||||
props: ['userId'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
progress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
user() {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship () {
|
||||
relationship() {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
blocked () {
|
||||
blocked() {
|
||||
return this.relationship.blocking
|
||||
}
|
||||
},
|
||||
|
@ -22,13 +22,13 @@ const BlockCard = {
|
|||
BasicUserCard
|
||||
},
|
||||
methods: {
|
||||
unblockUser () {
|
||||
unblockUser() {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
},
|
||||
blockUser () {
|
||||
blockUser() {
|
||||
this.progress = true
|
||||
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
|
|
|
@ -2,14 +2,14 @@ import Timeline from '../timeline/timeline.vue'
|
|||
|
||||
const Bookmarks = {
|
||||
computed: {
|
||||
timeline () {
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.bookmarks
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,15 +4,16 @@ const PublicTimeline = {
|
|||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.bubble
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PublicTimeline
|
||||
|
|
|
@ -11,14 +11,17 @@ export default {
|
|||
name: 'Timeago',
|
||||
props: ['date'],
|
||||
computed: {
|
||||
displayDate () {
|
||||
displayDate() {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
if (this.date.getTime() === today.getTime()) {
|
||||
return this.$t('display_date.today')
|
||||
} else {
|
||||
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
|
||||
return this.date.toLocaleDateString(
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
{ day: 'numeric', month: 'long' }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:checked="modelValue"
|
||||
:indeterminate="indeterminate"
|
||||
@change="$emit('update:modelValue', $event.target.checked)"
|
||||
>
|
||||
/>
|
||||
<i class="checkbox-indicator" />
|
||||
<span
|
||||
v-if="!!$slots.default"
|
||||
|
@ -22,12 +22,8 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['update:modelValue'],
|
||||
props: [
|
||||
'modelValue',
|
||||
'indeterminate',
|
||||
'disabled'
|
||||
]
|
||||
props: ['modelValue', 'indeterminate', 'disabled'],
|
||||
emits: ['update:modelValue']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -71,7 +67,7 @@ export default {
|
|||
&.disabled {
|
||||
.checkbox-indicator::before,
|
||||
.label {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.label {
|
||||
color: $fallback--faint;
|
||||
|
@ -79,7 +75,7 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
input[type='checkbox'] {
|
||||
display: none;
|
||||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
|
@ -92,11 +88,10 @@ export default {
|
|||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
& > span {
|
||||
margin-left: .5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
flex: 0 0 0;
|
||||
max-width: 9em;
|
||||
align-items: stretch;
|
||||
padding: .2em 8px;
|
||||
padding: 0.2em 8px;
|
||||
|
||||
input {
|
||||
background: none;
|
||||
|
@ -40,9 +40,10 @@
|
|||
}
|
||||
.transparentIndicator {
|
||||
// forgot to install counter-strike source, ooops
|
||||
background-color: #FF00FF;
|
||||
background-color: #ff00ff;
|
||||
position: relative;
|
||||
&::before, &::after {
|
||||
&::before,
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
background-color: #000000;
|
||||
|
@ -64,5 +65,4 @@
|
|||
.label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,12 @@
|
|||
:model-value="present"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
@update:modelValue="
|
||||
$emit(
|
||||
'update:modelValue',
|
||||
typeof modelValue === 'undefined' ? fallback : undefined
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="input color-input-field">
|
||||
<input
|
||||
|
@ -24,7 +29,7 @@
|
|||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
/>
|
||||
<input
|
||||
v-if="validColor"
|
||||
:id="name"
|
||||
|
@ -33,7 +38,7 @@
|
|||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
/>
|
||||
<div
|
||||
v-if="transparentColor"
|
||||
class="transparentIndicator"
|
||||
|
@ -41,12 +46,11 @@
|
|||
<div
|
||||
v-if="computedColor"
|
||||
class="computedIndicator"
|
||||
:style="{backgroundColor: fallback}"
|
||||
:style="{ backgroundColor: fallback }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
|
@ -93,21 +97,22 @@ export default {
|
|||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
present () {
|
||||
present() {
|
||||
return typeof this.modelValue !== 'undefined'
|
||||
},
|
||||
validColor () {
|
||||
validColor() {
|
||||
return hex2rgb(this.modelValue || this.fallback)
|
||||
},
|
||||
transparentColor () {
|
||||
transparentColor() {
|
||||
return this.modelValue === 'transparent'
|
||||
},
|
||||
computedColor () {
|
||||
computedColor() {
|
||||
return this.modelValue && this.modelValue.startsWith('--')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
|
||||
<style lang="scss">
|
||||
.color-control {
|
||||
|
|
|
@ -22,13 +22,12 @@ const ConfirmModal = {
|
|||
type: String
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
onCancel () {
|
||||
onCancel() {
|
||||
this.$emit('cancelled')
|
||||
},
|
||||
onAccept () {
|
||||
onAccept() {
|
||||
this.$emit('accepted')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
</dialog-modal>
|
||||
</template>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../_variables';
|
||||
|
||||
|
@ -35,5 +37,3 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
|
|
@ -43,11 +43,7 @@ import {
|
|||
faThumbsUp
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAdjust,
|
||||
faExclamationTriangle,
|
||||
faThumbsUp
|
||||
)
|
||||
library.add(faAdjust, faExclamationTriangle, faThumbsUp)
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -65,19 +61,35 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
hint () {
|
||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||
hint() {
|
||||
const levelVal = this.contrast.aaa
|
||||
? 'aaa'
|
||||
: this.contrast.aa
|
||||
? 'aa'
|
||||
: 'bad'
|
||||
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||
const context = this.$t('settings.style.common.contrast.context.text')
|
||||
const ratio = this.contrast.text
|
||||
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
|
||||
return this.$t('settings.style.common.contrast.hint', {
|
||||
level,
|
||||
context,
|
||||
ratio
|
||||
})
|
||||
},
|
||||
hint_18pt () {
|
||||
const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad')
|
||||
hint_18pt() {
|
||||
const levelVal = this.contrast.laaa
|
||||
? 'aaa'
|
||||
: this.contrast.laa
|
||||
? 'aa'
|
||||
: 'bad'
|
||||
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||
const context = this.$t('settings.style.common.contrast.context.18pt')
|
||||
const ratio = this.contrast.text
|
||||
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
|
||||
return this.$t('settings.style.common.contrast.hint', {
|
||||
level,
|
||||
context,
|
||||
ratio
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ const conversationPage = {
|
|||
Conversation
|
||||
},
|
||||
computed: {
|
||||
statusId () {
|
||||
statusId() {
|
||||
return this.$route.params.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,7 @@ import {
|
|||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAngleDoubleDown,
|
||||
faAngleDoubleLeft,
|
||||
faChevronLeft
|
||||
)
|
||||
library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
|
||||
|
||||
const sortById = (a, b) => {
|
||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||
|
@ -39,16 +35,17 @@ const sortAndFilterConversation = (conversation, statusoid) => {
|
|||
if (statusoid.type === 'retweet') {
|
||||
conversation = filter(
|
||||
conversation,
|
||||
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id)
|
||||
(status) =>
|
||||
status.type === 'retweet' || status.id !== statusoid.retweeted_status.id
|
||||
)
|
||||
} else {
|
||||
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||
}
|
||||
return conversation.filter(_ => _).sort(sortById)
|
||||
return conversation.filter((_) => _).sort(sortById)
|
||||
}
|
||||
|
||||
const conversation = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
highlight: null,
|
||||
expanded: false,
|
||||
|
@ -66,74 +63,78 @@ const conversation = {
|
|||
'profileUserId',
|
||||
'virtualHidden'
|
||||
],
|
||||
created () {
|
||||
created() {
|
||||
if (this.isPage) {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
maxDepthToShowByDefault () {
|
||||
maxDepthToShowByDefault() {
|
||||
// maxDepthInThread = max number of depths that is *visible*
|
||||
// since our depth starts with 0 and "showing" means "showing children"
|
||||
// there is a -2 here
|
||||
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||
return maxDepth >= 1 ? maxDepth : 1
|
||||
},
|
||||
streamingEnabled () {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
streamingEnabled() {
|
||||
return (
|
||||
this.mergedConfig.useStreamingApi &&
|
||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
)
|
||||
},
|
||||
displayStyle () {
|
||||
displayStyle() {
|
||||
return this.$store.getters.mergedConfig.conversationDisplay
|
||||
},
|
||||
isTreeView () {
|
||||
isTreeView() {
|
||||
return !this.isLinearView
|
||||
},
|
||||
treeViewIsSimple () {
|
||||
treeViewIsSimple() {
|
||||
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||
},
|
||||
isLinearView () {
|
||||
isLinearView() {
|
||||
return this.displayStyle === 'linear'
|
||||
},
|
||||
shouldFadeAncestors () {
|
||||
shouldFadeAncestors() {
|
||||
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||
},
|
||||
otherRepliesButtonPosition () {
|
||||
otherRepliesButtonPosition() {
|
||||
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||
},
|
||||
showOtherRepliesButtonBelowStatus () {
|
||||
showOtherRepliesButtonBelowStatus() {
|
||||
return this.otherRepliesButtonPosition === 'below'
|
||||
},
|
||||
showOtherRepliesButtonInsideStatus () {
|
||||
showOtherRepliesButtonInsideStatus() {
|
||||
return this.otherRepliesButtonPosition === 'inside'
|
||||
},
|
||||
suspendable () {
|
||||
suspendable() {
|
||||
if (this.isTreeView) {
|
||||
return Object.entries(this.statusContentProperties)
|
||||
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
|
||||
return Object.entries(this.statusContentProperties).every(
|
||||
([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0
|
||||
)
|
||||
}
|
||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
||||
return this.$refs.statusComponent.every(s => s.suspendable)
|
||||
return this.$refs.statusComponent.every((s) => s.suspendable)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
hideStatus () {
|
||||
hideStatus() {
|
||||
return this.virtualHidden && this.suspendable
|
||||
},
|
||||
status () {
|
||||
status() {
|
||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||
},
|
||||
originalStatusId () {
|
||||
originalStatusId() {
|
||||
if (this.status.retweeted_status) {
|
||||
return this.status.retweeted_status.id
|
||||
} else {
|
||||
return this.statusId
|
||||
}
|
||||
},
|
||||
conversationId () {
|
||||
conversationId() {
|
||||
return this.getConversationId(this.statusId)
|
||||
},
|
||||
conversation () {
|
||||
conversation() {
|
||||
if (!this.status) {
|
||||
return []
|
||||
}
|
||||
|
@ -142,155 +143,203 @@ const conversation = {
|
|||
return [this.status]
|
||||
}
|
||||
|
||||
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
|
||||
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
|
||||
const conversation = clone(
|
||||
this.$store.state.statuses.conversationsObject[this.conversationId]
|
||||
)
|
||||
const statusIndex = findIndex(conversation, {
|
||||
id: this.originalStatusId
|
||||
})
|
||||
if (statusIndex !== -1) {
|
||||
conversation[statusIndex] = this.status
|
||||
}
|
||||
|
||||
return sortAndFilterConversation(conversation, this.status)
|
||||
},
|
||||
statusMap () {
|
||||
statusMap() {
|
||||
return this.conversation.reduce((res, s) => {
|
||||
res[s.id] = s
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
threadTree () {
|
||||
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
||||
table[status.id] = index
|
||||
return table
|
||||
}, {})
|
||||
threadTree() {
|
||||
const reverseLookupTable = this.conversation.reduce(
|
||||
(table, status, index) => {
|
||||
table[status.id] = index
|
||||
return table
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const threads = this.conversation.reduce((a, cur) => {
|
||||
const id = cur.id
|
||||
a.forest[id] = this.getReplies(id)
|
||||
.map(s => s.id)
|
||||
const threads = this.conversation.reduce(
|
||||
(a, cur) => {
|
||||
const id = cur.id
|
||||
a.forest[id] = this.getReplies(id).map((s) => s.id)
|
||||
|
||||
return a
|
||||
}, {
|
||||
forest: {}
|
||||
})
|
||||
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
return a
|
||||
},
|
||||
{
|
||||
forest: {}
|
||||
}
|
||||
)
|
||||
|
||||
processed[id] = true
|
||||
return [{
|
||||
status: this.conversation[reverseLookupTable[id]],
|
||||
id,
|
||||
depth
|
||||
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
|
||||
}).reduce((a, b) => a.concat(b), [])
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) =>
|
||||
topLevel
|
||||
.map((id) => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
}
|
||||
|
||||
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
|
||||
processed[id] = true
|
||||
return [
|
||||
{
|
||||
status: this.conversation[reverseLookupTable[id]],
|
||||
id,
|
||||
depth
|
||||
},
|
||||
walk(forest, forest[id], depth + 1, processed)
|
||||
].reduce((a, b) => a.concat(b), [])
|
||||
})
|
||||
.reduce((a, b) => a.concat(b), [])
|
||||
|
||||
const linearized = walk(
|
||||
threads.forest,
|
||||
this.topLevel.map((k) => k.id)
|
||||
)
|
||||
|
||||
return linearized
|
||||
},
|
||||
replyIds () {
|
||||
return this.conversation.map(k => k.id)
|
||||
replyIds() {
|
||||
return this.conversation
|
||||
.map((k) => k.id)
|
||||
.reduce((res, id) => {
|
||||
res[id] = (this.replies[id] || []).map(k => k.id)
|
||||
res[id] = (this.replies[id] || []).map((k) => k.id)
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
totalReplyCount () {
|
||||
totalReplyCount() {
|
||||
const sizes = {}
|
||||
const subTreeSizeFor = (id) => {
|
||||
if (sizes[id]) {
|
||||
return sizes[id]
|
||||
}
|
||||
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
|
||||
sizes[id] =
|
||||
1 +
|
||||
this.replyIds[id]
|
||||
.map((cid) => subTreeSizeFor(cid))
|
||||
.reduce((a, b) => a + b, 0)
|
||||
return sizes[id]
|
||||
}
|
||||
this.conversation.map(k => k.id).map(subTreeSizeFor)
|
||||
this.conversation.map((k) => k.id).map(subTreeSizeFor)
|
||||
return Object.keys(sizes).reduce((res, id) => {
|
||||
res[id] = sizes[id] - 1 // exclude itself
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
totalReplyDepth () {
|
||||
totalReplyDepth() {
|
||||
const depths = {}
|
||||
const subTreeDepthFor = (id) => {
|
||||
if (depths[id]) {
|
||||
return depths[id]
|
||||
}
|
||||
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
|
||||
depths[id] =
|
||||
1 +
|
||||
this.replyIds[id]
|
||||
.map((cid) => subTreeDepthFor(cid))
|
||||
.reduce((a, b) => (a > b ? a : b), 0)
|
||||
return depths[id]
|
||||
}
|
||||
this.conversation.map(k => k.id).map(subTreeDepthFor)
|
||||
this.conversation.map((k) => k.id).map(subTreeDepthFor)
|
||||
return Object.keys(depths).reduce((res, id) => {
|
||||
res[id] = depths[id] - 1 // exclude itself
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
depths () {
|
||||
depths() {
|
||||
return this.threadTree.reduce((a, k) => {
|
||||
a[k.id] = k.depth
|
||||
return a
|
||||
}, {})
|
||||
},
|
||||
topLevel () {
|
||||
const topLevel = this.conversation.reduce((tl, cur) =>
|
||||
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
|
||||
topLevel() {
|
||||
const topLevel = this.conversation.reduce(
|
||||
(tl, cur) =>
|
||||
tl.filter(
|
||||
(k) =>
|
||||
this.getReplies(cur.id)
|
||||
.map((v) => v.id)
|
||||
.indexOf(k.id) === -1
|
||||
),
|
||||
this.conversation
|
||||
)
|
||||
return topLevel
|
||||
},
|
||||
otherTopLevelCount () {
|
||||
otherTopLevelCount() {
|
||||
return this.topLevel.length - 1
|
||||
},
|
||||
showingTopLevel () {
|
||||
showingTopLevel() {
|
||||
if (this.canDive && this.diveRoot) {
|
||||
return [this.statusMap[this.diveRoot]]
|
||||
}
|
||||
return this.topLevel
|
||||
},
|
||||
diveRoot () {
|
||||
diveRoot() {
|
||||
const statusId = this.inlineDivePosition || this.statusId
|
||||
const isTopLevel = !this.parentOf(statusId)
|
||||
return isTopLevel ? null : statusId
|
||||
},
|
||||
diveDepth () {
|
||||
diveDepth() {
|
||||
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||
},
|
||||
diveMode () {
|
||||
diveMode() {
|
||||
return this.canDive && !!this.diveRoot
|
||||
},
|
||||
shouldShowAllConversationButton () {
|
||||
shouldShowAllConversationButton() {
|
||||
// The "show all conversation" button tells the user that there exist
|
||||
// other toplevel statuses, so do not show it if there is only a single root
|
||||
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
|
||||
return (
|
||||
this.isTreeView &&
|
||||
this.isExpanded &&
|
||||
this.diveMode &&
|
||||
this.topLevel.length > 1
|
||||
)
|
||||
},
|
||||
shouldShowAncestors () {
|
||||
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
|
||||
shouldShowAncestors() {
|
||||
return (
|
||||
this.isTreeView &&
|
||||
this.isExpanded &&
|
||||
this.ancestorsOf(this.diveRoot).length
|
||||
)
|
||||
},
|
||||
replies () {
|
||||
replies() {
|
||||
let i = 1
|
||||
// eslint-disable-next-line camelcase
|
||||
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
|
||||
/* eslint-disable camelcase */
|
||||
const irid = in_reply_to_status_id
|
||||
/* eslint-enable camelcase */
|
||||
if (irid) {
|
||||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
name: `#${i}`,
|
||||
id: id
|
||||
})
|
||||
}
|
||||
i++
|
||||
return result
|
||||
}, {})
|
||||
return reduce(
|
||||
this.conversation,
|
||||
(result, { id, in_reply_to_status_id }) => {
|
||||
/* eslint-disable camelcase */
|
||||
const irid = in_reply_to_status_id
|
||||
/* eslint-enable camelcase */
|
||||
if (irid) {
|
||||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
name: `#${i}`,
|
||||
id: id
|
||||
})
|
||||
}
|
||||
i++
|
||||
return result
|
||||
},
|
||||
{}
|
||||
)
|
||||
},
|
||||
isExpanded () {
|
||||
isExpanded() {
|
||||
return !!(this.expanded || this.isPage)
|
||||
},
|
||||
hiddenStyle () {
|
||||
hiddenStyle() {
|
||||
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||
return this.virtualHidden ? { height } : {}
|
||||
},
|
||||
threadDisplayStatus () {
|
||||
threadDisplayStatus() {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
const id = k.id
|
||||
const depth = this.depths[id]
|
||||
|
@ -298,7 +347,7 @@ const conversation = {
|
|||
if (this.threadDisplayStatusObject[id]) {
|
||||
return this.threadDisplayStatusObject[id]
|
||||
}
|
||||
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
|
||||
if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
|
||||
return 'showing'
|
||||
} else {
|
||||
return 'hidden'
|
||||
|
@ -309,7 +358,7 @@ const conversation = {
|
|||
return a
|
||||
}, {})
|
||||
},
|
||||
statusContentProperties () {
|
||||
statusContentProperties() {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
const id = k.id
|
||||
const props = (() => {
|
||||
|
@ -334,20 +383,20 @@ const conversation = {
|
|||
return a
|
||||
}, {})
|
||||
},
|
||||
canDive () {
|
||||
canDive() {
|
||||
return this.isTreeView && this.isExpanded
|
||||
},
|
||||
focused () {
|
||||
focused() {
|
||||
return (id) => {
|
||||
return (this.isExpanded) && id === this.highlight
|
||||
return this.isExpanded && id === this.highlight
|
||||
}
|
||||
},
|
||||
maybeHighlight () {
|
||||
maybeHighlight() {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus
|
||||
})
|
||||
},
|
||||
components: {
|
||||
|
@ -355,53 +404,59 @@ const conversation = {
|
|||
ThreadTree
|
||||
},
|
||||
watch: {
|
||||
statusId (newVal, oldVal) {
|
||||
statusId(newVal, oldVal) {
|
||||
const newConversationId = this.getConversationId(newVal)
|
||||
const oldConversationId = this.getConversationId(oldVal)
|
||||
if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
|
||||
if (
|
||||
newConversationId &&
|
||||
oldConversationId &&
|
||||
newConversationId === oldConversationId
|
||||
) {
|
||||
this.setHighlight(this.originalStatusId)
|
||||
} else {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
expanded (value) {
|
||||
expanded(value) {
|
||||
if (value) {
|
||||
this.fetchConversation()
|
||||
} else {
|
||||
this.resetDisplayState()
|
||||
}
|
||||
},
|
||||
virtualHidden (value) {
|
||||
this.$store.dispatch(
|
||||
'setVirtualHeight',
|
||||
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
|
||||
)
|
||||
virtualHidden(value) {
|
||||
this.$store.dispatch('setVirtualHeight', {
|
||||
statusId: this.statusId,
|
||||
height: `${this.$el.clientHeight}px`
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchConversation () {
|
||||
fetchConversation() {
|
||||
if (this.status) {
|
||||
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchConversation({ id: this.statusId })
|
||||
.then(({ ancestors, descendants }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
this.setHighlight(this.originalStatusId)
|
||||
})
|
||||
} else {
|
||||
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchStatus({ id: this.statusId })
|
||||
.then((status) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
||||
this.fetchConversation()
|
||||
})
|
||||
}
|
||||
},
|
||||
getReplies (id) {
|
||||
getReplies(id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
getHighlight () {
|
||||
getHighlight() {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
setHighlight (id) {
|
||||
setHighlight(id) {
|
||||
if (!id) return
|
||||
this.highlight = id
|
||||
|
||||
|
@ -412,32 +467,38 @@ const conversation = {
|
|||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||
},
|
||||
toggleExpanded () {
|
||||
toggleExpanded() {
|
||||
this.expanded = !this.expanded
|
||||
},
|
||||
getConversationId (statusId) {
|
||||
getConversationId(statusId) {
|
||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
||||
return get(
|
||||
status,
|
||||
'retweeted_status.statusnet_conversation_id',
|
||||
get(status, 'statusnet_conversation_id')
|
||||
)
|
||||
},
|
||||
setThreadDisplay (id, nextStatus) {
|
||||
setThreadDisplay(id, nextStatus) {
|
||||
this.threadDisplayStatusObject = {
|
||||
...this.threadDisplayStatusObject,
|
||||
[id]: nextStatus
|
||||
}
|
||||
},
|
||||
toggleThreadDisplay (id) {
|
||||
toggleThreadDisplay(id) {
|
||||
const curStatus = this.threadDisplayStatus[id]
|
||||
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||
this.setThreadDisplay(id, nextStatus)
|
||||
},
|
||||
setThreadDisplayRecursively (id, nextStatus) {
|
||||
setThreadDisplayRecursively(id, nextStatus) {
|
||||
this.setThreadDisplay(id, nextStatus)
|
||||
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
|
||||
this.getReplies(id)
|
||||
.map((k) => k.id)
|
||||
.map((id) => this.setThreadDisplayRecursively(id, nextStatus))
|
||||
},
|
||||
showThreadRecursively (id) {
|
||||
showThreadRecursively(id) {
|
||||
this.setThreadDisplayRecursively(id, 'showing')
|
||||
},
|
||||
setStatusContentProperty (id, name, value) {
|
||||
setStatusContentProperty(id, name, value) {
|
||||
this.statusContentPropertiesObject = {
|
||||
...this.statusContentPropertiesObject,
|
||||
[id]: {
|
||||
|
@ -446,10 +507,14 @@ const conversation = {
|
|||
}
|
||||
}
|
||||
},
|
||||
toggleStatusContentProperty (id, name) {
|
||||
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
||||
toggleStatusContentProperty(id, name) {
|
||||
this.setStatusContentProperty(
|
||||
id,
|
||||
name,
|
||||
!this.statusContentProperties[id][name]
|
||||
)
|
||||
},
|
||||
leastVisibleAncestor (id) {
|
||||
leastVisibleAncestor(id) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(cur)
|
||||
while (cur) {
|
||||
|
@ -463,18 +528,20 @@ const conversation = {
|
|||
// nothing found, fall back to toplevel
|
||||
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||
},
|
||||
diveIntoStatus (id, preventScroll) {
|
||||
diveIntoStatus(id, preventScroll) {
|
||||
this.tryScrollTo(id)
|
||||
},
|
||||
diveToTopLevel () {
|
||||
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
|
||||
diveToTopLevel() {
|
||||
this.tryScrollTo(
|
||||
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id
|
||||
)
|
||||
},
|
||||
// only used when we are not on a page
|
||||
undive () {
|
||||
undive() {
|
||||
this.inlineDivePosition = null
|
||||
this.setHighlight(this.statusId)
|
||||
},
|
||||
tryScrollTo (id) {
|
||||
tryScrollTo(id) {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
@ -503,13 +570,13 @@ const conversation = {
|
|||
this.setHighlight(id)
|
||||
})
|
||||
},
|
||||
goToCurrent () {
|
||||
goToCurrent() {
|
||||
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||
},
|
||||
statusById (id) {
|
||||
statusById(id) {
|
||||
return this.statusMap[id]
|
||||
},
|
||||
parentOf (id) {
|
||||
parentOf(id) {
|
||||
const status = this.statusById(id)
|
||||
if (!status) {
|
||||
return undefined
|
||||
|
@ -520,11 +587,11 @@ const conversation = {
|
|||
}
|
||||
return parentId
|
||||
},
|
||||
parentOrSelf (id) {
|
||||
parentOrSelf(id) {
|
||||
return this.parentOf(id) || id
|
||||
},
|
||||
// Ancestors of some status, from top to bottom
|
||||
ancestorsOf (id) {
|
||||
ancestorsOf(id) {
|
||||
const ancestors = []
|
||||
let cur = this.parentOf(id)
|
||||
while (cur) {
|
||||
|
@ -533,7 +600,7 @@ const conversation = {
|
|||
}
|
||||
return ancestors
|
||||
},
|
||||
topLevelAncestorOrSelfId (id) {
|
||||
topLevelAncestorOrSelfId(id) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(id)
|
||||
while (parent) {
|
||||
|
@ -542,7 +609,7 @@ const conversation = {
|
|||
}
|
||||
return cur
|
||||
},
|
||||
resetDisplayState () {
|
||||
resetDisplayState() {
|
||||
this.undive()
|
||||
this.threadDisplayStatusObject = {}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-if="!hideStatus"
|
||||
:style="hiddenStyle"
|
||||
class="Conversation"
|
||||
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
|
||||
:class="{ '-expanded': isExpanded, panel: isExpanded }"
|
||||
>
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
|
@ -35,13 +35,15 @@
|
|||
@click.prevent="diveToTopLevel"
|
||||
>
|
||||
<template #icon>
|
||||
<FAIcon
|
||||
icon="angle-double-left"
|
||||
/>
|
||||
<FAIcon icon="angle-double-left" />
|
||||
</template>
|
||||
<template #text>
|
||||
<span>
|
||||
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
|
||||
{{
|
||||
$tc('status.show_all_conversation', otherTopLevelCount, {
|
||||
numStatus: otherTopLevelCount
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -54,14 +56,20 @@
|
|||
v-for="status in ancestorsOf(diveRoot)"
|
||||
:key="status.id"
|
||||
class="thread-ancestor"
|
||||
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||
:class="{
|
||||
'thread-ancestor-has-other-replies':
|
||||
getReplies(status.id).length > 1,
|
||||
'-faded': shouldFadeAncestors
|
||||
}"
|
||||
>
|
||||
<status
|
||||
ref="statusComponent"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:show-pinned="
|
||||
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
|
||||
"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
@ -69,7 +77,6 @@
|
|||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
|
||||
:simple-tree="treeViewIsSimple"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
|
@ -78,28 +85,47 @@
|
|||
:total-reply-depth="totalReplyDepth"
|
||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||
:dive="() => diveIntoStatus(status.id)"
|
||||
|
||||
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
||||
:controlled-showing-tall="
|
||||
statusContentProperties[status.id].showingTall
|
||||
"
|
||||
:controlled-expanding-subject="
|
||||
statusContentProperties[status.id].expandingSubject
|
||||
"
|
||||
:controlled-showing-long-subject="
|
||||
statusContentProperties[status.id].showingLongSubject
|
||||
"
|
||||
:controlled-replying="statusContentProperties[status.id].replying"
|
||||
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
||||
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
||||
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
||||
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||
|
||||
:controlled-media-playing="
|
||||
statusContentProperties[status.id].mediaPlaying
|
||||
"
|
||||
:controlled-toggle-showing-tall="
|
||||
() => toggleStatusContentProperty(status.id, 'showingTall')
|
||||
"
|
||||
:controlled-toggle-expanding-subject="
|
||||
() => toggleStatusContentProperty(status.id, 'expandingSubject')
|
||||
"
|
||||
:controlled-toggle-showing-long-subject="
|
||||
() =>
|
||||
toggleStatusContentProperty(status.id, 'showingLongSubject')
|
||||
"
|
||||
:controlled-toggle-replying="
|
||||
() => toggleStatusContentProperty(status.id, 'replying')
|
||||
"
|
||||
:controlled-set-media-playing="
|
||||
(newVal) =>
|
||||
toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)
|
||||
"
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
<div
|
||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||
v-if="
|
||||
showOtherRepliesButtonBelowStatus &&
|
||||
getReplies(status.id).length > 1
|
||||
"
|
||||
class="thread-ancestor-dive-box"
|
||||
>
|
||||
<div
|
||||
class="thread-ancestor-dive-box-inner"
|
||||
>
|
||||
<div class="thread-ancestor-dive-box-inner">
|
||||
<i18n-t
|
||||
tag="button"
|
||||
scope="global"
|
||||
|
@ -108,13 +134,17 @@
|
|||
@click.prevent="diveIntoStatus(status.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<FAIcon
|
||||
icon="angle-double-right"
|
||||
/>
|
||||
<FAIcon icon="angle-double-right" />
|
||||
</template>
|
||||
<template #text>
|
||||
<span>
|
||||
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
|
||||
{{
|
||||
$tc(
|
||||
'status.ancestor_follow',
|
||||
getReplies(status.id).length - 1,
|
||||
{ numReplies: getReplies(status.id).length - 1 }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -127,7 +157,6 @@
|
|||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
:depth="0"
|
||||
|
||||
:status="status"
|
||||
:in-profile="inProfile"
|
||||
:conversation="conversation"
|
||||
|
@ -135,13 +164,11 @@
|
|||
:is-expanded="isExpanded"
|
||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:focused="focused"
|
||||
:get-replies="getReplies"
|
||||
:highlight="maybeHighlight"
|
||||
:set-highlight="setHighlight"
|
||||
:toggle-expanded="toggleExpanded"
|
||||
|
||||
:simple="treeViewIsSimple"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
|
@ -165,7 +192,9 @@
|
|||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:show-pinned="
|
||||
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
|
||||
"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
@ -173,7 +202,6 @@
|
|||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
|
@ -182,7 +210,6 @@
|
|||
:status-content-properties="statusContentProperties"
|
||||
:set-status-content-property="setStatusContentProperty"
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
|
@ -233,7 +260,8 @@
|
|||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
/* Make the button stretch along the whole row */
|
||||
&, &-inner {
|
||||
&,
|
||||
&-inner {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
@ -271,7 +299,8 @@
|
|||
border-left-color: $fallback--cRed;
|
||||
border-left-color: var(--cRed, $fallback--cRed);
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius)
|
||||
var(--panelRadius, $fallback--panelRadius);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
|
|
|
@ -46,76 +46,98 @@ export default {
|
|||
},
|
||||
data: () => ({
|
||||
searchBarHidden: true,
|
||||
supportsMask: window.CSS && window.CSS.supports && (
|
||||
window.CSS.supports('mask-size', 'contain') ||
|
||||
supportsMask:
|
||||
window.CSS &&
|
||||
window.CSS.supports &&
|
||||
(window.CSS.supports('mask-size', 'contain') ||
|
||||
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
||||
window.CSS.supports('-moz-mask-size', 'contain') ||
|
||||
window.CSS.supports('-ms-mask-size', 'contain') ||
|
||||
window.CSS.supports('-o-mask-size', 'contain')
|
||||
),
|
||||
window.CSS.supports('-o-mask-size', 'contain')),
|
||||
showingConfirmLogout: false
|
||||
}),
|
||||
computed: {
|
||||
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
|
||||
logoStyle () {
|
||||
enableMask() {
|
||||
return this.supportsMask && this.$store.state.instance.logoMask
|
||||
},
|
||||
logoStyle() {
|
||||
return {
|
||||
'visibility': this.enableMask ? 'hidden' : 'visible'
|
||||
visibility: this.enableMask ? 'hidden' : 'visible'
|
||||
}
|
||||
},
|
||||
logoMaskStyle () {
|
||||
return this.enableMask ? {
|
||||
'mask-image': `url(${this.$store.state.instance.logo})`
|
||||
} : {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
}
|
||||
logoMaskStyle() {
|
||||
return this.enableMask
|
||||
? {
|
||||
'mask-image': `url(${this.$store.state.instance.logo})`
|
||||
}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
}
|
||||
},
|
||||
logoBgStyle () {
|
||||
return Object.assign({
|
||||
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
||||
opacity: this.searchBarHidden ? 1 : 0
|
||||
}, this.enableMask ? {} : {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
})
|
||||
logoBgStyle() {
|
||||
return Object.assign(
|
||||
{
|
||||
margin: `${this.$store.state.instance.logoMargin} 0`,
|
||||
opacity: this.searchBarHidden ? 1 : 0
|
||||
},
|
||||
this.enableMask
|
||||
? {}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
}
|
||||
)
|
||||
},
|
||||
logo () { return this.$store.state.instance.logo },
|
||||
mergedConfig () {
|
||||
logo() {
|
||||
return this.$store.state.instance.logo
|
||||
},
|
||||
mergedConfig() {
|
||||
return this.$store.getters.mergedConfig
|
||||
},
|
||||
sitename () { return this.$store.state.instance.name },
|
||||
showNavShortcuts () {
|
||||
sitename() {
|
||||
return this.$store.state.instance.name
|
||||
},
|
||||
showNavShortcuts() {
|
||||
return this.mergedConfig.showNavShortcuts
|
||||
},
|
||||
showWiderShortcuts () {
|
||||
showWiderShortcuts() {
|
||||
return this.mergedConfig.showWiderShortcuts
|
||||
},
|
||||
hideSiteFavicon () {
|
||||
hideSiteFavicon() {
|
||||
return this.mergedConfig.hideSiteFavicon
|
||||
},
|
||||
hideSiteName () {
|
||||
hideSiteName() {
|
||||
return this.mergedConfig.hideSiteName
|
||||
},
|
||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||
logoLeft () { return this.$store.state.instance.logoLeft },
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
shouldConfirmLogout () {
|
||||
hideSitename() {
|
||||
return this.$store.state.instance.hideSitename
|
||||
},
|
||||
logoLeft() {
|
||||
return this.$store.state.instance.logoLeft
|
||||
},
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
privateMode() {
|
||||
return this.$store.state.instance.private
|
||||
},
|
||||
shouldConfirmLogout() {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
},
|
||||
showBubbleTimeline () {
|
||||
showBubbleTimeline() {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scrollToTop () {
|
||||
scrollToTop() {
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
onSearchBarToggled (hidden) {
|
||||
onSearchBarToggled(hidden) {
|
||||
this.searchBarHidden = hidden
|
||||
},
|
||||
openSettingsModal () {
|
||||
openSettingsModal() {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
openModModal () {
|
||||
openModModal() {
|
||||
this.$store.dispatch('openModModal')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
display: grid;
|
||||
grid-template-rows: var(--navbar-height);
|
||||
grid-template-columns: 2fr auto 2fr;
|
||||
grid-template-areas: "nav-left logo actions";
|
||||
grid-template-areas: 'nav-left logo actions';
|
||||
box-sizing: border-box;
|
||||
padding: 0 1.2em;
|
||||
margin: auto;
|
||||
|
@ -24,11 +24,12 @@
|
|||
|
||||
&.-logoLeft .inner-nav {
|
||||
grid-template-columns: auto 2fr 2fr;
|
||||
grid-template-areas: "logo nav-left actions";
|
||||
grid-template-areas: 'logo nav-left actions';
|
||||
}
|
||||
|
||||
.button-default {
|
||||
&, svg {
|
||||
&,
|
||||
svg {
|
||||
color: $fallback--text;
|
||||
color: var(--btnTopBarText, $fallback--text);
|
||||
}
|
||||
|
@ -49,7 +50,7 @@
|
|||
color: $fallback--text;
|
||||
color: var(--btnToggledTopBarText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
v-if="!hideSiteFavicon"
|
||||
class="favicon"
|
||||
src="/favicon.png"
|
||||
>
|
||||
/>
|
||||
<span
|
||||
v-if="!hideSiteName"
|
||||
class="site-name"
|
||||
|
@ -91,7 +91,7 @@
|
|||
<img
|
||||
:src="logo"
|
||||
:style="logoStyle"
|
||||
>
|
||||
/>
|
||||
</router-link>
|
||||
<div class="item right actions">
|
||||
<search-bar
|
||||
|
@ -106,7 +106,10 @@
|
|||
<router-link
|
||||
v-if="currentUser"
|
||||
class="nav-icon"
|
||||
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
|
||||
:to="{
|
||||
name: 'interactions',
|
||||
params: { username: currentUser.screen_name }
|
||||
}"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
@ -152,7 +155,10 @@
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||
v-if="
|
||||
(currentUser && currentUser.role === 'admin') ||
|
||||
currentUser.role === 'moderator'
|
||||
"
|
||||
class="button-unstyled nav-icon"
|
||||
@click.stop="openModModal"
|
||||
>
|
||||
|
|
|
@ -31,14 +31,14 @@
|
|||
.dark-overlay {
|
||||
&::before {
|
||||
bottom: 0;
|
||||
content: " ";
|
||||
content: ' ';
|
||||
display: block;
|
||||
cursor: default;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: rgba(27,31,35,.5);
|
||||
background: rgba(27, 31, 35, 0.5);
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@
|
|||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: .5em .5em;
|
||||
padding: 0.5em 0.5em;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
|
@ -84,9 +84,8 @@
|
|||
|
||||
button {
|
||||
width: auto;
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,7 @@ import Timeline from '../timeline/timeline.vue'
|
|||
|
||||
const DMs = {
|
||||
computed: {
|
||||
timeline () {
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.dms
|
||||
}
|
||||
},
|
||||
|
|
|
@ -6,18 +6,18 @@ const DomainMuteCard = {
|
|||
ProgressButton
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
user() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
muted () {
|
||||
muted() {
|
||||
return this.user.domainMutes.includes(this.domain)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
unmuteDomain () {
|
||||
unmuteDomain() {
|
||||
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||
},
|
||||
muteDomain () {
|
||||
muteDomain() {
|
||||
return this.$store.dispatch('muteDomain', this.domain)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
@ -19,7 +19,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
|
|
@ -8,27 +8,27 @@ const EditStatusModal = {
|
|||
PostStatusForm,
|
||||
Modal
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
resettingForm: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
isLoggedIn() {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
modalActivated () {
|
||||
modalActivated() {
|
||||
return this.$store.state.editStatus.modalActivated
|
||||
},
|
||||
isFormVisible () {
|
||||
isFormVisible() {
|
||||
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
||||
},
|
||||
params () {
|
||||
params() {
|
||||
return this.$store.state.editStatus.params || {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
params (newVal, oldVal) {
|
||||
params(newVal, oldVal) {
|
||||
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
||||
this.resettingForm = true
|
||||
this.$nextTick(() => {
|
||||
|
@ -36,14 +36,16 @@ const EditStatusModal = {
|
|||
})
|
||||
}
|
||||
},
|
||||
isFormVisible (val) {
|
||||
isFormVisible(val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
|
||||
this.$nextTick(
|
||||
() => this.$el && this.$el.querySelector('textarea').focus()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||
doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||
const params = {
|
||||
store: this.$store,
|
||||
statusId: this.$store.state.editStatus.params.statusId,
|
||||
|
@ -55,7 +57,8 @@ const EditStatusModal = {
|
|||
contentType
|
||||
}
|
||||
|
||||
return statusPosterService.editStatus(params)
|
||||
return statusPosterService
|
||||
.editStatus(params)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
|
@ -66,7 +69,7 @@ const EditStatusModal = {
|
|||
}
|
||||
})
|
||||
},
|
||||
closeModal () {
|
||||
closeModal() {
|
||||
this.$store.dispatch('closeEditStatusModal')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
@posted="closeModal"
|
||||
:disablePolls="true"
|
||||
:disableVisibilitySelector="true"
|
||||
:disable-polls="true"
|
||||
:disable-visibility-selector="true"
|
||||
:post-handler="doEditStatus"
|
||||
@posted="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -4,13 +4,9 @@ import { take } from 'lodash'
|
|||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSmileBeam
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faSmileBeam
|
||||
)
|
||||
library.add(faSmileBeam)
|
||||
|
||||
/**
|
||||
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
||||
|
@ -105,7 +101,7 @@ const EmojiInput = {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
input: undefined,
|
||||
highlighted: 0,
|
||||
|
@ -123,29 +119,34 @@ const EmojiInput = {
|
|||
EmojiPicker
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
padEmoji() {
|
||||
return this.$store.getters.mergedConfig.padEmoji
|
||||
},
|
||||
showSuggestions () {
|
||||
return this.focused &&
|
||||
showSuggestions() {
|
||||
return (
|
||||
this.focused &&
|
||||
this.suggestions &&
|
||||
this.suggestions.length > 0 &&
|
||||
!this.showPicker &&
|
||||
!this.temporarilyHideSuggestions
|
||||
)
|
||||
},
|
||||
textAtCaret () {
|
||||
textAtCaret() {
|
||||
return (this.wordAtCaret || {}).word || ''
|
||||
},
|
||||
wordAtCaret () {
|
||||
wordAtCaret() {
|
||||
if (this.modelValue && this.caret) {
|
||||
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||
const word =
|
||||
Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||
return word
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
const { root } = this.$refs
|
||||
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
|
||||
const input =
|
||||
root.querySelector('.emoji-input > input') ||
|
||||
root.querySelector('.emoji-input > textarea')
|
||||
if (!input) return
|
||||
this.input = input
|
||||
this.resize()
|
||||
|
@ -158,7 +159,7 @@ const EmojiInput = {
|
|||
input.addEventListener('transitionend', this.onTransition)
|
||||
input.addEventListener('input', this.onInput)
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
const { input } = this
|
||||
if (input) {
|
||||
input.removeEventListener('blur', this.onBlur)
|
||||
|
@ -183,27 +184,28 @@ const EmojiInput = {
|
|||
// Async: cancel if textAtCaret has changed during wait
|
||||
if (this.textAtCaret !== newWord) return
|
||||
if (matchedSuggestions.length <= 0) return
|
||||
this.suggestions = take(matchedSuggestions, 5)
|
||||
.map(({ imageUrl, ...rest }) => ({
|
||||
this.suggestions = take(matchedSuggestions, 5).map(
|
||||
({ imageUrl, ...rest }) => ({
|
||||
...rest,
|
||||
img: imageUrl || ''
|
||||
}))
|
||||
})
|
||||
)
|
||||
},
|
||||
suggestions: {
|
||||
handler (newValue) {
|
||||
handler(newValue) {
|
||||
this.$nextTick(this.resize)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
focusPickerInput () {
|
||||
focusPickerInput() {
|
||||
const pickerEl = this.$refs.picker.$el
|
||||
if (!pickerEl) return
|
||||
const pickerInput = pickerEl.querySelector('input')
|
||||
if (pickerInput) pickerInput.focus()
|
||||
},
|
||||
triggerShowPicker () {
|
||||
triggerShowPicker() {
|
||||
this.showPicker = true
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
this.$nextTick(() => {
|
||||
|
@ -218,7 +220,7 @@ const EmojiInput = {
|
|||
this.disableClickOutside = false
|
||||
}, 0)
|
||||
},
|
||||
togglePicker () {
|
||||
togglePicker() {
|
||||
this.input.focus()
|
||||
this.showPicker = !this.showPicker
|
||||
if (this.showPicker) {
|
||||
|
@ -227,12 +229,16 @@ const EmojiInput = {
|
|||
this.$nextTick(this.focusPickerInput)
|
||||
}
|
||||
},
|
||||
replace (replacement) {
|
||||
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||
replace(replacement) {
|
||||
const newValue = Completion.replaceWord(
|
||||
this.modelValue,
|
||||
this.wordAtCaret,
|
||||
replacement
|
||||
)
|
||||
this.$emit('update:modelValue', newValue)
|
||||
this.caret = 0
|
||||
},
|
||||
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
||||
insert({ insertion, keepOpen, surroundingSpace = true }) {
|
||||
const before = this.modelValue.substring(0, this.caret) || ''
|
||||
const after = this.modelValue.substring(this.caret) || ''
|
||||
|
||||
|
@ -251,19 +257,25 @@ const EmojiInput = {
|
|||
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
|
||||
*/
|
||||
const isSpaceRegex = /\s/
|
||||
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
|
||||
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
|
||||
const spaceBefore =
|
||||
surroundingSpace &&
|
||||
!isSpaceRegex.exec(before.slice(-1)) &&
|
||||
before.length &&
|
||||
this.padEmoji > 0
|
||||
? ' '
|
||||
: ''
|
||||
const spaceAfter =
|
||||
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
|
||||
? ' '
|
||||
: ''
|
||||
|
||||
const newValue = [
|
||||
before,
|
||||
spaceBefore,
|
||||
insertion,
|
||||
spaceAfter,
|
||||
after
|
||||
].join('')
|
||||
const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
|
||||
''
|
||||
)
|
||||
this.keepOpen = keepOpen
|
||||
this.$emit('update:modelValue', newValue)
|
||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
const position =
|
||||
this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
if (!keepOpen) {
|
||||
this.input.focus()
|
||||
}
|
||||
|
@ -275,12 +287,17 @@ const EmojiInput = {
|
|||
this.caret = position
|
||||
})
|
||||
},
|
||||
replaceText (e, suggestion) {
|
||||
replaceText(e, suggestion) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 0 || suggestion) {
|
||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
||||
const chosenSuggestion =
|
||||
suggestion || this.suggestions[this.highlighted]
|
||||
const replacement = chosenSuggestion.replacement
|
||||
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||
const newValue = Completion.replaceWord(
|
||||
this.modelValue,
|
||||
this.wordAtCaret,
|
||||
replacement
|
||||
)
|
||||
this.$emit('update:modelValue', newValue)
|
||||
this.highlighted = 0
|
||||
const position = this.wordAtCaret.start + replacement.length
|
||||
|
@ -295,7 +312,7 @@ const EmojiInput = {
|
|||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
cycleBackward (e) {
|
||||
cycleBackward(e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 1) {
|
||||
this.highlighted -= 1
|
||||
|
@ -307,7 +324,7 @@ const EmojiInput = {
|
|||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
cycleForward (e) {
|
||||
cycleForward(e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 1) {
|
||||
this.highlighted += 1
|
||||
|
@ -319,26 +336,28 @@ const EmojiInput = {
|
|||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
scrollIntoView () {
|
||||
scrollIntoView() {
|
||||
const rootRef = this.$refs['picker'].$el
|
||||
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
||||
* replies in notifs) or mobile post form. Note that getting and setting
|
||||
* scroll is different for `Window` and `Element`s
|
||||
*/
|
||||
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
window
|
||||
const currentScroll = scrollerRef === window
|
||||
? scrollerRef.scrollY
|
||||
: scrollerRef.scrollTop
|
||||
const scrollerHeight = scrollerRef === window
|
||||
? scrollerRef.innerHeight
|
||||
: scrollerRef.offsetHeight
|
||||
const scrollerRef =
|
||||
this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
window
|
||||
const currentScroll =
|
||||
scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
|
||||
const scrollerHeight =
|
||||
scrollerRef === window
|
||||
? scrollerRef.innerHeight
|
||||
: scrollerRef.offsetHeight
|
||||
|
||||
const scrollerBottomBorder = currentScroll + scrollerHeight
|
||||
// We check where the bottom border of root element is, this uses findOffset
|
||||
// to find offset relative to scrollable container (scroller)
|
||||
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
const rootBottomBorder =
|
||||
rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
|
||||
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
|
||||
// could also check top delta but there's no case for it
|
||||
|
@ -360,10 +379,10 @@ const EmojiInput = {
|
|||
}
|
||||
})
|
||||
},
|
||||
onTransition (e) {
|
||||
onTransition(e) {
|
||||
this.resize()
|
||||
},
|
||||
onBlur (e) {
|
||||
onBlur(e) {
|
||||
// Clicking on any suggestion removes focus from autocomplete,
|
||||
// preventing click handler ever executing.
|
||||
this.blurTimeout = setTimeout(() => {
|
||||
|
@ -372,10 +391,10 @@ const EmojiInput = {
|
|||
this.resize()
|
||||
}, 200)
|
||||
},
|
||||
onClick (e, suggestion) {
|
||||
onClick(e, suggestion) {
|
||||
this.replaceText(e, suggestion)
|
||||
},
|
||||
onFocus (e) {
|
||||
onFocus(e) {
|
||||
if (this.blurTimeout) {
|
||||
clearTimeout(this.blurTimeout)
|
||||
this.blurTimeout = null
|
||||
|
@ -389,7 +408,7 @@ const EmojiInput = {
|
|||
this.resize()
|
||||
this.temporarilyHideSuggestions = false
|
||||
},
|
||||
onKeyUp (e) {
|
||||
onKeyUp(e) {
|
||||
const { key } = e
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
|
@ -402,11 +421,11 @@ const EmojiInput = {
|
|||
this.temporarilyHideSuggestions = false
|
||||
}
|
||||
},
|
||||
onPaste (e) {
|
||||
onPaste(e) {
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
},
|
||||
onKeyDown (e) {
|
||||
onKeyDown(e) {
|
||||
const { ctrlKey, shiftKey, key } = e
|
||||
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
|
||||
this.insert({ insertion: '\n', surroundingSpace: false })
|
||||
|
@ -453,31 +472,31 @@ const EmojiInput = {
|
|||
this.showPicker = false
|
||||
this.resize()
|
||||
},
|
||||
onInput (e) {
|
||||
onInput(e) {
|
||||
this.showPicker = false
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
this.$emit('update:modelValue', e.target.value)
|
||||
},
|
||||
onClickInput (e) {
|
||||
onClickInput(e) {
|
||||
this.showPicker = false
|
||||
},
|
||||
onClickOutside (e) {
|
||||
onClickOutside(e) {
|
||||
if (this.disableClickOutside) return
|
||||
this.showPicker = false
|
||||
},
|
||||
onStickerUploaded (e) {
|
||||
onStickerUploaded(e) {
|
||||
this.showPicker = false
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
onStickerUploadFailed (e) {
|
||||
onStickerUploadFailed(e) {
|
||||
this.showPicker = false
|
||||
this.$emit('sticker-upload-Failed', e)
|
||||
},
|
||||
setCaret ({ target: { selectionStart } }) {
|
||||
setCaret({ target: { selectionStart } }) {
|
||||
this.caret = selectionStart
|
||||
},
|
||||
resize () {
|
||||
resize() {
|
||||
const panel = this.$refs.panel
|
||||
if (!panel) return
|
||||
const picker = this.$refs.picker.$el
|
||||
|
@ -488,9 +507,12 @@ const EmojiInput = {
|
|||
this.setPlacement(panelBody, panel, offsetBottom)
|
||||
this.setPlacement(picker, picker, offsetBottom)
|
||||
},
|
||||
setPlacement (container, target, offsetBottom) {
|
||||
setPlacement(container, target, offsetBottom) {
|
||||
if (!container || !target) return
|
||||
if (this.placement === 'bottom' || (this.placement === 'auto' && !this.overflowsBottom(container))) {
|
||||
if (
|
||||
this.placement === 'bottom' ||
|
||||
(this.placement === 'auto' && !this.overflowsBottom(container))
|
||||
) {
|
||||
target.style.top = offsetBottom + 'px'
|
||||
target.style.bottom = 'auto'
|
||||
} else {
|
||||
|
@ -498,7 +520,7 @@ const EmojiInput = {
|
|||
target.style.bottom = this.input.offsetHeight + 'px'
|
||||
}
|
||||
},
|
||||
overflowsBottom (el) {
|
||||
overflowsBottom(el) {
|
||||
return el.getBoundingClientRect().bottom > window.innerHeight
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,11 +42,14 @@
|
|||
:class="{ highlighted: index === highlighted }"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
>
|
||||
<span v-if="!suggestion.mfm" class="image">
|
||||
<span
|
||||
v-if="!suggestion.mfm"
|
||||
class="image"
|
||||
>
|
||||
<img
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
>
|
||||
/>
|
||||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<div class="label">
|
||||
|
@ -77,7 +80,7 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: .2em .25em;
|
||||
margin: 0.2em 0.25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
|
@ -93,7 +96,7 @@
|
|||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +107,7 @@
|
|||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-body {
|
||||
|
@ -178,7 +181,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
input,
|
||||
textarea {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,26 @@
|
|||
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
|
||||
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
|
||||
const MFM_TAGS = [
|
||||
'blur',
|
||||
'bounce',
|
||||
'flip',
|
||||
'font',
|
||||
'jelly',
|
||||
'jump',
|
||||
'rainbow',
|
||||
'rotate',
|
||||
'shake',
|
||||
'sparkle',
|
||||
'spin',
|
||||
'tada',
|
||||
'twitch',
|
||||
'x2',
|
||||
'x3',
|
||||
'x4'
|
||||
].map((tag) => ({
|
||||
displayText: tag,
|
||||
detailText: '$[' + tag + ' ]',
|
||||
replacement: '$[' + tag + ' ]',
|
||||
mfm: true
|
||||
}))
|
||||
|
||||
/**
|
||||
* suggest - generates a suggestor function to be used by emoji-input
|
||||
|
@ -13,10 +34,10 @@ const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow',
|
|||
* doesn't support user linking you can just provide only emoji.
|
||||
*/
|
||||
|
||||
export default data => {
|
||||
export default (data) => {
|
||||
const emojiCurry = suggestEmoji(data.emoji)
|
||||
const usersCurry = data.store && suggestUsers(data.store)
|
||||
return input => {
|
||||
return (input) => {
|
||||
const firstChar = input[0]
|
||||
if (firstChar === ':' && data.emoji) {
|
||||
return emojiCurry(input)
|
||||
|
@ -25,14 +46,15 @@ export default data => {
|
|||
return usersCurry(input)
|
||||
}
|
||||
if (firstChar === '$') {
|
||||
return MFM_TAGS
|
||||
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
|
||||
return MFM_TAGS.filter(
|
||||
({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1
|
||||
)
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const suggestEmoji = emojis => input => {
|
||||
export const suggestEmoji = (emojis) => (input) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
return emojis
|
||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
||||
|
@ -85,7 +107,7 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
})
|
||||
}
|
||||
|
||||
return async input => {
|
||||
return async (input) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
if (previousQuery === noPrefix) return suggestions
|
||||
|
||||
|
@ -99,36 +121,47 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
await debounceUserSearch(noPrefix)
|
||||
}
|
||||
|
||||
const newSuggestions = state.users.users.filter(
|
||||
user =>
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)
|
||||
).slice(0, 20).sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
const newSuggestions = state.users.users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)
|
||||
)
|
||||
.slice(0, 20)
|
||||
.sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
|
||||
// Matches on screen name (i.e. user@instance) makes a priority
|
||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
// Matches on screen name (i.e. user@instance) makes a priority
|
||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
|
||||
// Matches on name takes second priority
|
||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
// Matches on name takes second priority
|
||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
|
||||
const diff = (bScore - aScore) * 10
|
||||
const diff = (bScore - aScore) * 10
|
||||
|
||||
// Then sort alphabetically
|
||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||
// Then sort alphabetically
|
||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
/* eslint-disable camelcase */
|
||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||
displayText: screen_name_ui,
|
||||
detailText: name,
|
||||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
}))
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
/* eslint-disable camelcase */
|
||||
})
|
||||
.map(
|
||||
({
|
||||
screen_name,
|
||||
screen_name_ui,
|
||||
name,
|
||||
profile_image_url_original
|
||||
}) => ({
|
||||
displayText: screen_name_ui,
|
||||
detailText: name,
|
||||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
})
|
||||
)
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
suggestions = newSuggestions || []
|
||||
|
|
|
@ -8,11 +8,7 @@ import {
|
|||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { trim, escapeRegExp, startCase } from 'lodash'
|
||||
|
||||
library.add(
|
||||
faBoxOpen,
|
||||
faStickyNote,
|
||||
faSmileBeam
|
||||
)
|
||||
library.add(faBoxOpen, faStickyNote, faSmileBeam)
|
||||
|
||||
// At widest, approximately 20 emoji are visible in a row,
|
||||
// loading 3 rows, could be overkill for narrow picker
|
||||
|
@ -29,7 +25,7 @@ const EmojiPicker = {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
activeGroup: 'standard',
|
||||
|
@ -42,35 +38,39 @@ const EmojiPicker = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||
StickerPicker: defineAsyncComponent(() =>
|
||||
import('../sticker_picker/sticker_picker.vue')
|
||||
),
|
||||
Checkbox
|
||||
},
|
||||
methods: {
|
||||
onStickerUploaded (e) {
|
||||
onStickerUploaded(e) {
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
onStickerUploadFailed (e) {
|
||||
onStickerUploadFailed(e) {
|
||||
this.$emit('sticker-upload-failed', e)
|
||||
},
|
||||
onEmoji (emoji) {
|
||||
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||
onEmoji(emoji) {
|
||||
const value = emoji.imageUrl
|
||||
? `:${emoji.displayText}:`
|
||||
: emoji.replacement
|
||||
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
||||
},
|
||||
onScroll (e) {
|
||||
onScroll(e) {
|
||||
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||
this.updateScrolledClass(target)
|
||||
this.scrolledGroup(target)
|
||||
this.triggerLoadMore(target)
|
||||
},
|
||||
onWheel (e) {
|
||||
onWheel(e) {
|
||||
e.preventDefault()
|
||||
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
||||
},
|
||||
highlight (key) {
|
||||
highlight(key) {
|
||||
this.setShowStickers(false)
|
||||
this.activeGroup = key
|
||||
},
|
||||
updateScrolledClass (target) {
|
||||
updateScrolledClass(target) {
|
||||
if (target.scrollTop <= 5) {
|
||||
this.groupsScrolledClass = 'scrolled-top'
|
||||
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
||||
|
@ -79,7 +79,7 @@ const EmojiPicker = {
|
|||
this.groupsScrolledClass = 'scrolled-middle'
|
||||
}
|
||||
},
|
||||
triggerLoadMore (target) {
|
||||
triggerLoadMore(target) {
|
||||
const ref = this.$refs['group-end-custom']
|
||||
if (!ref) return
|
||||
const bottom = ref.offsetTop + ref.offsetHeight
|
||||
|
@ -93,15 +93,16 @@ const EmojiPicker = {
|
|||
// Always load when at the very top in case there's no scroll space yet
|
||||
const atTop = scrollerTop < 5
|
||||
// Don't load when looking at unicode category or at the very bottom
|
||||
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
|
||||
const bottomAboveViewport =
|
||||
bottom < scrollerTop || scrollerBottom === scrollerMax
|
||||
if (!bottomAboveViewport && (approachingBottom || atTop)) {
|
||||
this.loadEmoji()
|
||||
}
|
||||
},
|
||||
scrolledGroup (target) {
|
||||
scrolledGroup(target) {
|
||||
const top = target.scrollTop + 5
|
||||
this.$nextTick(() => {
|
||||
this.emojisView.forEach(group => {
|
||||
this.emojisView.forEach((group) => {
|
||||
const ref = this.$refs['group-' + group.id]
|
||||
if (ref.offsetTop <= top) {
|
||||
this.activeGroup = group.id
|
||||
|
@ -109,8 +110,9 @@ const EmojiPicker = {
|
|||
})
|
||||
})
|
||||
},
|
||||
loadEmoji () {
|
||||
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
|
||||
loadEmoji() {
|
||||
const allLoaded =
|
||||
this.customEmojiBuffer.length === this.filteredEmoji.length
|
||||
|
||||
if (allLoaded) {
|
||||
return
|
||||
|
@ -118,7 +120,7 @@ const EmojiPicker = {
|
|||
|
||||
this.customEmojiBufferSlice += LOAD_EMOJI_BY
|
||||
},
|
||||
startEmojiLoad (forceUpdate = false) {
|
||||
startEmojiLoad(forceUpdate = false) {
|
||||
if (!forceUpdate) {
|
||||
this.keyword = ''
|
||||
}
|
||||
|
@ -132,46 +134,47 @@ const EmojiPicker = {
|
|||
}
|
||||
this.customEmojiBufferSlice = LOAD_EMOJI_BY
|
||||
},
|
||||
toggleStickers () {
|
||||
toggleStickers() {
|
||||
this.showingStickers = !this.showingStickers
|
||||
},
|
||||
setShowStickers (value) {
|
||||
setShowStickers(value) {
|
||||
this.showingStickers = value
|
||||
},
|
||||
filterByKeyword (list) {
|
||||
filterByKeyword(list) {
|
||||
if (this.keyword === '') return list
|
||||
const regex = new RegExp(escapeRegExp(trim(this.keyword)), 'i')
|
||||
return list.filter(emoji => {
|
||||
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
|
||||
return list.filter((emoji) => {
|
||||
return (
|
||||
regex.test(emoji.displayText) ||
|
||||
(!emoji.imageUrl && emoji.replacement === this.keyword)
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
keyword () {
|
||||
keyword() {
|
||||
this.customEmojiLoadAllConfirmed = false
|
||||
this.onScroll()
|
||||
this.startEmojiLoad(true)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeGroupView () {
|
||||
activeGroupView() {
|
||||
return this.showingStickers ? '' : this.activeGroup
|
||||
},
|
||||
stickersAvailable () {
|
||||
stickersAvailable() {
|
||||
if (this.$store.state.instance.stickers) {
|
||||
return this.$store.state.instance.stickers.length > 0
|
||||
}
|
||||
return 0
|
||||
},
|
||||
filteredEmoji () {
|
||||
return this.filterByKeyword(
|
||||
this.$store.state.instance.customEmoji || []
|
||||
)
|
||||
filteredEmoji() {
|
||||
return this.filterByKeyword(this.$store.state.instance.customEmoji || [])
|
||||
},
|
||||
customEmojiBuffer () {
|
||||
customEmojiBuffer() {
|
||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
||||
},
|
||||
emojis () {
|
||||
emojis() {
|
||||
const standardEmojis = this.$store.state.instance.emoji || []
|
||||
const customEmojis = this.sortedEmoji
|
||||
const emojiPacks = []
|
||||
|
@ -195,7 +198,7 @@ const EmojiPicker = {
|
|||
}
|
||||
].concat(emojiPacks)
|
||||
},
|
||||
sortedEmoji () {
|
||||
sortedEmoji() {
|
||||
const customEmojis = this.$store.state.instance.customEmoji || []
|
||||
const sortedEmojiGroups = new Map()
|
||||
customEmojis.forEach((emoji) => {
|
||||
|
@ -207,19 +210,22 @@ const EmojiPicker = {
|
|||
})
|
||||
return new Map([...sortedEmojiGroups.entries()].sort())
|
||||
},
|
||||
emojisView () {
|
||||
emojisView() {
|
||||
if (this.keyword === '') {
|
||||
return this.emojis.filter(pack => {
|
||||
return this.emojis.filter((pack) => {
|
||||
return pack.id === this.activeGroup
|
||||
})
|
||||
} else {
|
||||
return this.emojis.filter(pack => {
|
||||
return this.emojis.filter((pack) => {
|
||||
return pack.emojis.length > 0
|
||||
})
|
||||
}
|
||||
},
|
||||
stickerPickerEnabled () {
|
||||
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker
|
||||
stickerPickerEnabled() {
|
||||
return (
|
||||
(this.$store.state.instance.stickers || []).length !== 0 &&
|
||||
this.enableStickerPicker
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: .4em;
|
||||
padding: 0.4em;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
|
@ -133,7 +133,7 @@
|
|||
}
|
||||
|
||||
.sticker-picker {
|
||||
flex: 1 1 auto
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.stickers,
|
||||
|
@ -168,8 +168,8 @@
|
|||
overflow: auto;
|
||||
user-select: none;
|
||||
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
|
||||
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
transition: mask-size 150ms;
|
||||
mask-size: 100% 20px, 100% 20px, auto;
|
||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||
|
@ -221,7 +221,5 @@
|
|||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<div class="emoji-picker panel panel-default panel-body">
|
||||
<div class="heading">
|
||||
<span
|
||||
ref="emoji-tabs"
|
||||
class="emoji-tabs"
|
||||
@wheel="onWheel"
|
||||
ref="emoji-tabs"
|
||||
>
|
||||
<span
|
||||
v-for="group in emojis"
|
||||
|
@ -17,16 +17,18 @@
|
|||
:title="group.text"
|
||||
@click.prevent="highlight(group.id)"
|
||||
>
|
||||
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
|
||||
<span v-if="!group.first.imageUrl">{{
|
||||
group.first.replacement
|
||||
}}</span>
|
||||
<img
|
||||
v-else
|
||||
:src="group.first.imageUrl"
|
||||
>
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="stickerPickerEnabled"
|
||||
class="stickers-tab-icon emoji-tabs-item"
|
||||
:class="{active: showingStickers}"
|
||||
:class="{ active: showingStickers }"
|
||||
:title="$t('emoji.stickers')"
|
||||
@click.prevent="toggleStickers"
|
||||
>
|
||||
|
@ -40,7 +42,7 @@
|
|||
<div class="content">
|
||||
<div
|
||||
class="emoji-content"
|
||||
:class="{hidden: showingStickers}"
|
||||
:class="{ hidden: showingStickers }"
|
||||
>
|
||||
<div class="emoji-search">
|
||||
<input
|
||||
|
@ -49,7 +51,7 @@
|
|||
class="form-control"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
@input="$event.target.composing = false"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref="emoji-groups"
|
||||
|
@ -79,7 +81,7 @@
|
|||
<img
|
||||
v-else
|
||||
:src="emoji.imageUrl"
|
||||
>
|
||||
/>
|
||||
</span>
|
||||
<span :ref="'group-end-' + group.id" />
|
||||
</div>
|
||||
|
|
|
@ -14,18 +14,20 @@ const EmojiReactions = {
|
|||
showAll: false
|
||||
}),
|
||||
computed: {
|
||||
tooManyReactions () {
|
||||
tooManyReactions() {
|
||||
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
|
||||
},
|
||||
emojiReactions () {
|
||||
emojiReactions() {
|
||||
return this.showAll
|
||||
? this.status.emoji_reactions
|
||||
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
||||
},
|
||||
showMoreString () {
|
||||
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
|
||||
showMoreString() {
|
||||
return `+${
|
||||
this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF
|
||||
}`
|
||||
},
|
||||
accountsForEmoji () {
|
||||
accountsForEmoji() {
|
||||
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
||||
if (reaction.url) {
|
||||
acc[reaction.url] = reaction.accounts || []
|
||||
|
@ -35,30 +37,30 @@ const EmojiReactions = {
|
|||
return acc
|
||||
}, {})
|
||||
},
|
||||
loggedIn () {
|
||||
loggedIn() {
|
||||
return !!this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleShowAll () {
|
||||
toggleShowAll() {
|
||||
this.showAll = !this.showAll
|
||||
},
|
||||
reactedWith (emoji) {
|
||||
return this.status.emoji_reactions.find(r => r.name === emoji).me
|
||||
reactedWith(emoji) {
|
||||
return this.status.emoji_reactions.find((r) => r.name === emoji).me
|
||||
},
|
||||
fetchEmojiReactionsByIfMissing () {
|
||||
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
||||
fetchEmojiReactionsByIfMissing() {
|
||||
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
|
||||
if (hasNoAccounts) {
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
||||
}
|
||||
},
|
||||
reactWith (emoji) {
|
||||
reactWith(emoji) {
|
||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
},
|
||||
unreact (emoji) {
|
||||
unreact(emoji) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
},
|
||||
emojiOnClick (emoji, event) {
|
||||
emojiOnClick(emoji, event) {
|
||||
if (!this.loggedIn) return
|
||||
|
||||
if (this.reactedWith(emoji)) {
|
||||
|
|
|
@ -1,28 +1,35 @@
|
|||
<template>
|
||||
<div class="emoji-reactions">
|
||||
<UserListPopover
|
||||
v-for="(reaction) in emojiReactions"
|
||||
v-for="reaction in emojiReactions"
|
||||
:key="reaction.url || reaction.name"
|
||||
:users="accountsForEmoji[reaction.url || reaction.name]"
|
||||
>
|
||||
<button
|
||||
class="emoji-reaction btn button-default"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||
:class="{
|
||||
'picked-reaction': reactedWith(reaction.name),
|
||||
'not-clickable': !loggedIn
|
||||
}"
|
||||
@click="emojiOnClick(reaction.name, $event)"
|
||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
<span
|
||||
v-if="reaction.url !== null"
|
||||
class="emoji-button-inner"
|
||||
>
|
||||
<img
|
||||
:src="reaction.url"
|
||||
:title="reaction.name"
|
||||
class="reaction-emoji"
|
||||
width="2.55em"
|
||||
>
|
||||
/>
|
||||
{{ reaction.count }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<span
|
||||
v-else
|
||||
class="emoji-button-inner"
|
||||
>
|
||||
<span class="reaction-emoji unicode-emoji">
|
||||
{{ reaction.name }}
|
||||
</span>
|
||||
|
@ -41,7 +48,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./emoji_reactions.js" ></script>
|
||||
<script src="./emoji_reactions.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
|
@ -52,7 +59,7 @@
|
|||
}
|
||||
|
||||
.unicode-emoji {
|
||||
font-size: 210%;
|
||||
font-size: 128%;
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
|
@ -60,13 +67,17 @@
|
|||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
.reaction-emoji {
|
||||
width: 2.55em !important;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
img.reaction-emoji {
|
||||
width: 1.55em !important;
|
||||
display: block;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
@ -93,9 +104,12 @@
|
|||
}
|
||||
|
||||
.button-default.picked-reaction {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
}
|
||||
background: none;
|
||||
padding: 1px 0.5em;
|
||||
|
||||
.emoji-button-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const Exporter = {
|
||||
props: {
|
||||
|
@ -18,26 +16,30 @@ const Exporter = {
|
|||
exportButtonLabel: { type: String },
|
||||
processingMessage: { type: String }
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
process () {
|
||||
process() {
|
||||
this.processing = true
|
||||
this.getContent()
|
||||
.then((content) => {
|
||||
const fileToDownload = document.createElement('a')
|
||||
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
|
||||
fileToDownload.setAttribute('download', this.filename)
|
||||
fileToDownload.style.display = 'none'
|
||||
document.body.appendChild(fileToDownload)
|
||||
fileToDownload.click()
|
||||
document.body.removeChild(fileToDownload)
|
||||
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||
setTimeout(() => { this.processing = false }, 2000)
|
||||
})
|
||||
this.getContent().then((content) => {
|
||||
const fileToDownload = document.createElement('a')
|
||||
fileToDownload.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' + encodeURIComponent(content)
|
||||
)
|
||||
fileToDownload.setAttribute('download', this.filename)
|
||||
fileToDownload.style.display = 'none'
|
||||
document.body.appendChild(fileToDownload)
|
||||
fileToDownload.click()
|
||||
document.body.removeChild(fileToDownload)
|
||||
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||
setTimeout(() => {
|
||||
this.processing = false
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ const ExtraButtons = {
|
|||
Popover,
|
||||
ConfirmModal
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
showingDeleteDialog: false,
|
||||
|
@ -43,154 +43,205 @@ const ExtraButtons = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
deleteStatus () {
|
||||
deleteStatus() {
|
||||
if (this.shouldConfirmDelete) {
|
||||
this.showDeleteStatusConfirmDialog()
|
||||
} else {
|
||||
this.doDeleteStatus()
|
||||
}
|
||||
},
|
||||
doDeleteStatus () {
|
||||
doDeleteStatus() {
|
||||
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
||||
this.hideDeleteStatusConfirmDialog()
|
||||
},
|
||||
showDeleteStatusConfirmDialog () {
|
||||
showDeleteStatusConfirmDialog() {
|
||||
this.showingDeleteDialog = true
|
||||
},
|
||||
hideDeleteStatusConfirmDialog () {
|
||||
hideDeleteStatusConfirmDialog() {
|
||||
this.showingDeleteDialog = false
|
||||
},
|
||||
translateStatus () {
|
||||
translateStatus() {
|
||||
if (this.noTranslationTargetSet) {
|
||||
this.$store.dispatch('pushGlobalNotice', { messageKey: 'toast.no_translation_target_set', level: 'info' })
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
messageKey: 'toast.no_translation_target_set',
|
||||
level: 'info'
|
||||
})
|
||||
}
|
||||
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
|
||||
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo })
|
||||
const translateTo =
|
||||
this.$store.getters.mergedConfig.translationLanguage ||
|
||||
this.$store.state.instance.interfaceLanguage
|
||||
this.$store
|
||||
.dispatch('translateStatus', {
|
||||
id: this.status.id,
|
||||
language: translateTo
|
||||
})
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
},
|
||||
pinStatus () {
|
||||
this.$store.dispatch('pinStatus', this.status.id)
|
||||
pinStatus() {
|
||||
this.$store
|
||||
.dispatch('pinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unpinStatus () {
|
||||
this.$store.dispatch('unpinStatus', this.status.id)
|
||||
unpinStatus() {
|
||||
this.$store
|
||||
.dispatch('unpinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
},
|
||||
muteConversation () {
|
||||
this.$store.dispatch('muteConversation', this.status.id)
|
||||
muteConversation() {
|
||||
this.$store
|
||||
.dispatch('muteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unmuteConversation () {
|
||||
this.$store.dispatch('unmuteConversation', this.status.id)
|
||||
unmuteConversation() {
|
||||
this.$store
|
||||
.dispatch('unmuteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
},
|
||||
copyLink () {
|
||||
navigator.clipboard.writeText(this.statusLink)
|
||||
copyLink() {
|
||||
navigator.clipboard
|
||||
.writeText(this.statusLink)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
},
|
||||
bookmarkStatus () {
|
||||
this.$store.dispatch('bookmark', { id: this.status.id })
|
||||
bookmarkStatus() {
|
||||
this.$store
|
||||
.dispatch('bookmark', { id: this.status.id })
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unbookmarkStatus () {
|
||||
this.$store.dispatch('unbookmark', { id: this.status.id })
|
||||
unbookmarkStatus() {
|
||||
this.$store
|
||||
.dispatch('unbookmark', { id: this.status.id })
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
.catch((err) => this.$emit('onError', err.error.error))
|
||||
},
|
||||
reportStatus () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
|
||||
reportStatus() {
|
||||
this.$store.dispatch('openUserReportingModal', {
|
||||
userId: this.status.user.id,
|
||||
statusIds: [this.status.id]
|
||||
})
|
||||
},
|
||||
editStatus () {
|
||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then(data => this.$store.dispatch('openEditStatusModal', {
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
statusIsSensitive: this.status.nsfw,
|
||||
statusPoll: this.status.poll,
|
||||
statusFiles: [...this.status.attachments],
|
||||
visibility: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
}))
|
||||
editStatus() {
|
||||
this.$store
|
||||
.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then((data) =>
|
||||
this.$store.dispatch('openEditStatusModal', {
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
statusIsSensitive: this.status.nsfw,
|
||||
statusPoll: this.status.poll,
|
||||
statusFiles: [...this.status.attachments],
|
||||
visibility: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
})
|
||||
)
|
||||
},
|
||||
showStatusHistory () {
|
||||
showStatusHistory() {
|
||||
const originalStatus = { ...this.status }
|
||||
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
|
||||
stripFieldsList.forEach(p => delete originalStatus[p])
|
||||
const stripFieldsList = [
|
||||
'attachments',
|
||||
'created_at',
|
||||
'emojis',
|
||||
'text',
|
||||
'raw_html',
|
||||
'nsfw',
|
||||
'poll',
|
||||
'summary',
|
||||
'summary_raw_html'
|
||||
]
|
||||
stripFieldsList.forEach((p) => delete originalStatus[p])
|
||||
this.$store.dispatch('openStatusHistoryModal', originalStatus)
|
||||
},
|
||||
redraftStatus () {
|
||||
redraftStatus() {
|
||||
if (this.shouldConfirmDelete) {
|
||||
this.showRedraftStatusConfirmDialog()
|
||||
} else {
|
||||
this.doRedraftStatus()
|
||||
}
|
||||
},
|
||||
doRedraftStatus () {
|
||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then(data => this.$store.dispatch('openPostStatusModal', {
|
||||
isRedraft: true,
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
statusIsSensitive: this.status.nsfw,
|
||||
statusPoll: this.status.poll,
|
||||
statusFiles: [...this.status.attachments],
|
||||
statusScope: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
}))
|
||||
doRedraftStatus() {
|
||||
this.$store
|
||||
.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then((data) =>
|
||||
this.$store.dispatch('openPostStatusModal', {
|
||||
isRedraft: true,
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
statusIsSensitive: this.status.nsfw,
|
||||
statusPoll: this.status.poll,
|
||||
statusFiles: [...this.status.attachments],
|
||||
statusScope: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
})
|
||||
)
|
||||
this.doDeleteStatus()
|
||||
},
|
||||
showRedraftStatusConfirmDialog () {
|
||||
showRedraftStatusConfirmDialog() {
|
||||
this.showingRedraftDialog = true
|
||||
},
|
||||
hideRedraftStatusConfirmDialog () {
|
||||
hideRedraftStatusConfirmDialog() {
|
||||
this.showingRedraftDialog = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
canDelete () {
|
||||
if (!this.currentUser) { return }
|
||||
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
canDelete() {
|
||||
if (!this.currentUser) {
|
||||
return
|
||||
}
|
||||
const superuser =
|
||||
this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||
return superuser || this.status.user.id === this.currentUser.id
|
||||
},
|
||||
ownStatus () {
|
||||
ownStatus() {
|
||||
return this.status.user.id === this.currentUser.id
|
||||
},
|
||||
canPin () {
|
||||
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
|
||||
canPin() {
|
||||
return (
|
||||
this.ownStatus &&
|
||||
(this.status.visibility === 'public' ||
|
||||
this.status.visibility === 'unlisted')
|
||||
)
|
||||
},
|
||||
canMute () {
|
||||
canMute() {
|
||||
return !!this.currentUser
|
||||
},
|
||||
canTranslate () {
|
||||
canTranslate() {
|
||||
return this.$store.state.instance.translationEnabled === true
|
||||
},
|
||||
noTranslationTargetSet () {
|
||||
noTranslationTargetSet() {
|
||||
return this.$store.getters.mergedConfig.translationLanguage === undefined
|
||||
},
|
||||
statusLink () {
|
||||
statusLink() {
|
||||
if (this.status.is_local) {
|
||||
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
|
||||
return `${this.$store.state.instance.server}${
|
||||
this.$router.resolve({
|
||||
name: 'conversation',
|
||||
params: { id: this.status.id }
|
||||
}).href
|
||||
}`
|
||||
} else {
|
||||
return this.status.external_url
|
||||
}
|
||||
},
|
||||
shouldConfirmDelete () {
|
||||
shouldConfirmDelete() {
|
||||
return this.$store.getters.mergedConfig.modalOnDelete
|
||||
},
|
||||
isEdited () {
|
||||
isEdited() {
|
||||
return this.status.edited_at !== null
|
||||
},
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable }
|
||||
editingAvailable() {
|
||||
return this.$store.state.instance.editingAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:content="{close}">
|
||||
<template #content="{ close }">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
|
@ -17,7 +17,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="eye-slash"
|
||||
/><span>{{ $t("status.mute_conversation") }}</span>
|
||||
/><span>{{ $t('status.mute_conversation') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canMute && status.thread_muted"
|
||||
|
@ -27,7 +27,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="eye-slash"
|
||||
/><span>{{ $t("status.unmute_conversation") }}</span>
|
||||
/><span>{{ $t('status.unmute_conversation') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!status.pinned && canPin"
|
||||
|
@ -38,7 +38,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t("status.pin") }}</span>
|
||||
/><span>{{ $t('status.pin') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.pinned && canPin"
|
||||
|
@ -49,7 +49,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t("status.unpin") }}</span>
|
||||
/><span>{{ $t('status.unpin') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!status.bookmarked"
|
||||
|
@ -60,7 +60,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
:icon="['far', 'bookmark']"
|
||||
/><span>{{ $t("status.bookmark") }}</span>
|
||||
/><span>{{ $t('status.bookmark') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.bookmarked"
|
||||
|
@ -71,7 +71,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="bookmark"
|
||||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
/><span>{{ $t('status.unbookmark') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="ownStatus && editingAvailable"
|
||||
|
@ -82,7 +82,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="pen"
|
||||
/><span>{{ $t("status.edit") }}</span>
|
||||
/><span>{{ $t('status.edit') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isEdited && editingAvailable"
|
||||
|
@ -93,7 +93,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="history"
|
||||
/><span>{{ $t("status.edit_history") }}</span>
|
||||
/><span>{{ $t('status.edit_history') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="ownStatus"
|
||||
|
@ -104,7 +104,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="file-pen"
|
||||
/><span>{{ $t("status.redraft") }}</span>
|
||||
/><span>{{ $t('status.redraft') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
|
@ -115,7 +115,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="times"
|
||||
/><span>{{ $t("status.delete") }}</span>
|
||||
/><span>{{ $t('status.delete') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -125,7 +125,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="share-alt"
|
||||
/><span>{{ $t("status.copy_link") }}</span>
|
||||
/><span>{{ $t('status.copy_link') }}</span>
|
||||
</button>
|
||||
<a
|
||||
v-if="!status.is_local"
|
||||
|
@ -137,7 +137,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="external-link-alt"
|
||||
/><span>{{ $t("status.external_source") }}</span>
|
||||
/><span>{{ $t('status.external_source') }}</span>
|
||||
</a>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -147,7 +147,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
:icon="['far', 'flag']"
|
||||
/><span>{{ $t("user_card.report") }}</span>
|
||||
/><span>{{ $t('user_card.report') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canTranslate"
|
||||
|
@ -158,7 +158,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="globe"
|
||||
/><span>{{ $t("status.translate") }}</span>
|
||||
/><span>{{ $t('status.translate') }}</span>
|
||||
|
||||
<template v-if="noTranslationTargetSet">
|
||||
<span class="dropdown-item-icon__badge warning">
|
||||
|
@ -172,7 +172,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled popover-trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
|
@ -205,7 +205,7 @@
|
|||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./extra_buttons.js" ></script>
|
||||
<script src="./extra_buttons.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faStar } from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faStar as faStarRegular
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faStar,
|
||||
faStarRegular
|
||||
)
|
||||
library.add(faStar, faStarRegular)
|
||||
|
||||
const FavoriteButton = {
|
||||
props: ['status', 'loggedIn'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
animated: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
favorite () {
|
||||
favorite() {
|
||||
if (!this.status.favorited) {
|
||||
this.$store.dispatch('favorite', { id: this.status.id })
|
||||
} else {
|
||||
|
@ -32,8 +27,10 @@ const FavoriteButton = {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters(['mergedConfig']),
|
||||
remoteInteractionLink () {
|
||||
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||
remoteInteractionLink() {
|
||||
return this.$store.getters.remoteInteractionLink({
|
||||
statusId: this.status.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./favorite_button.js" ></script>
|
||||
<script src="./favorite_button.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
@ -56,6 +56,7 @@
|
|||
.interactive {
|
||||
.svg-inline--fa {
|
||||
animation-duration: 0.6s;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
&:hover .svg-inline--fa,
|
||||
|
|
|
@ -2,10 +2,20 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
|||
|
||||
const FeaturesPanel = {
|
||||
computed: {
|
||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||
textlimit: function () { return this.$store.state.instance.textlimit },
|
||||
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
|
||||
whoToFollow: function () {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
mediaProxy: function () {
|
||||
return this.$store.state.instance.mediaProxyAvailable
|
||||
},
|
||||
textlimit: function () {
|
||||
return this.$store.state.instance.textlimit
|
||||
},
|
||||
uploadlimit: function () {
|
||||
return fileSizeFormatService.fileSizeFormat(
|
||||
this.$store.state.instance.uploadlimit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,17 +16,20 @@
|
|||
</li>
|
||||
<li>{{ $t('features_panel.scope_options') }}</li>
|
||||
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
|
||||
<li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li>
|
||||
<li>
|
||||
{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }}
|
||||
{{ $t('upload.file_size_units.' + uploadlimit.unit) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./features_panel.js" ></script>
|
||||
<script src="./features_panel.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.features-panel li {
|
||||
line-height: 24px;
|
||||
}
|
||||
.features-panel li {
|
||||
line-height: 24px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,14 +5,11 @@ import {
|
|||
faExclamationTriangle
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faStop,
|
||||
faExclamationTriangle
|
||||
)
|
||||
library.add(faStop, faExclamationTriangle)
|
||||
|
||||
const Flash = {
|
||||
props: [ 'src' ],
|
||||
data () {
|
||||
props: ['src'],
|
||||
data() {
|
||||
return {
|
||||
player: false, // can be true, "hidden", false. hidden = element exists
|
||||
loaded: false,
|
||||
|
@ -20,7 +17,7 @@ const Flash = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
openPlayer () {
|
||||
openPlayer() {
|
||||
if (this.player) return // prevent double-loading, or re-loading on failure
|
||||
this.player = 'hidden'
|
||||
RuffleService.getRuffle().then((ruffle) => {
|
||||
|
@ -32,17 +29,20 @@ const Flash = {
|
|||
container.appendChild(player)
|
||||
player.style.width = '100%'
|
||||
player.style.height = '100%'
|
||||
player.load(this.src).then(() => {
|
||||
this.player = true
|
||||
}).catch((e) => {
|
||||
console.error('Error loading ruffle', e)
|
||||
this.player = 'error'
|
||||
})
|
||||
player
|
||||
.load(this.src)
|
||||
.then(() => {
|
||||
this.player = true
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Error loading ruffle', e)
|
||||
this.player = 'error'
|
||||
})
|
||||
this.ruffleInstance = player
|
||||
this.$emit('playerOpened')
|
||||
})
|
||||
},
|
||||
closePlayer () {
|
||||
closePlayer() {
|
||||
this.ruffleInstance && this.ruffleInstance.remove()
|
||||
this.player = false
|
||||
this.$emit('playerClosed')
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
import {
|
||||
requestFollow,
|
||||
requestUnfollow
|
||||
} from '../../services/follow_manipulate/follow_manipulate'
|
||||
export default {
|
||||
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
||||
components: {
|
||||
ConfirmModal
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
inProgress: false,
|
||||
showingConfirmUnfollow: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmUnfollow () {
|
||||
shouldConfirmUnfollow() {
|
||||
return this.$store.getters.mergedConfig.modalOnUnfollow
|
||||
},
|
||||
isPressed () {
|
||||
isPressed() {
|
||||
return this.inProgress || this.relationship.following
|
||||
},
|
||||
title () {
|
||||
title() {
|
||||
if (this.inProgress || this.relationship.following) {
|
||||
return this.$t('user_card.follow_unfollow')
|
||||
} else if (this.relationship.requested) {
|
||||
|
@ -27,7 +30,7 @@ export default {
|
|||
return this.$t('user_card.follow')
|
||||
}
|
||||
},
|
||||
label () {
|
||||
label() {
|
||||
if (this.inProgress) {
|
||||
return this.$t('user_card.follow_progress')
|
||||
} else if (this.relationship.following) {
|
||||
|
@ -38,39 +41,44 @@ export default {
|
|||
return this.$t('user_card.follow')
|
||||
}
|
||||
},
|
||||
disabled () {
|
||||
disabled() {
|
||||
return this.inProgress || this.user.deactivated
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showConfirmUnfollow () {
|
||||
showConfirmUnfollow() {
|
||||
this.showingConfirmUnfollow = true
|
||||
},
|
||||
hideConfirmUnfollow () {
|
||||
hideConfirmUnfollow() {
|
||||
this.showingConfirmUnfollow = false
|
||||
},
|
||||
onClick () {
|
||||
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
|
||||
onClick() {
|
||||
this.relationship.following || this.relationship.requested
|
||||
? this.unfollow()
|
||||
: this.follow()
|
||||
},
|
||||
follow () {
|
||||
follow() {
|
||||
this.inProgress = true
|
||||
requestFollow(this.relationship.id, this.$store).then(() => {
|
||||
this.inProgress = false
|
||||
})
|
||||
},
|
||||
unfollow () {
|
||||
unfollow() {
|
||||
if (this.shouldConfirmUnfollow) {
|
||||
this.showConfirmUnfollow()
|
||||
} else {
|
||||
this.doUnfollow()
|
||||
}
|
||||
},
|
||||
doUnfollow () {
|
||||
doUnfollow() {
|
||||
const store = this.$store
|
||||
this.inProgress = true
|
||||
requestUnfollow(this.relationship.id, store).then(() => {
|
||||
this.inProgress = false
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
|
||||
store.commit('removeStatus', {
|
||||
timeline: 'friends',
|
||||
userId: this.relationship.id
|
||||
})
|
||||
})
|
||||
|
||||
this.hideConfirmUnfollow()
|
||||
|
|
|
@ -21,9 +21,7 @@
|
|||
tag="span"
|
||||
>
|
||||
<template #user>
|
||||
<span
|
||||
v-text="user.screen_name_ui"
|
||||
/>
|
||||
<span v-text="user.screen_name_ui" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
|
|
|
@ -4,10 +4,7 @@ import FollowButton from '../follow_button/follow_button.vue'
|
|||
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
||||
|
||||
const FollowCard = {
|
||||
props: [
|
||||
'user',
|
||||
'noFollowsYou'
|
||||
],
|
||||
props: ['user', 'noFollowsYou'],
|
||||
components: {
|
||||
BasicUserCard,
|
||||
RemoteFollow,
|
||||
|
@ -15,13 +12,13 @@ const FollowCard = {
|
|||
RemoveFollowerButton
|
||||
},
|
||||
computed: {
|
||||
isMe () {
|
||||
isMe() {
|
||||
return this.$store.state.users.currentUser.id === this.user.id
|
||||
},
|
||||
loggedIn () {
|
||||
loggedIn() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
relationship () {
|
||||
relationship() {
|
||||
return this.$store.getters.relationship(this.user.id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,39 +8,41 @@ const FollowRequestCard = {
|
|||
BasicUserCard,
|
||||
ConfirmModal
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
showingApproveConfirmDialog: false,
|
||||
showingDenyConfirmDialog: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findFollowRequestNotificationId () {
|
||||
findFollowRequestNotificationId() {
|
||||
const notif = notificationsFromStore(this.$store).find(
|
||||
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
|
||||
(notif) =>
|
||||
notif.from_profile.id === this.user.id &&
|
||||
notif.type === 'follow_request'
|
||||
)
|
||||
return notif && notif.id
|
||||
},
|
||||
showApproveConfirmDialog () {
|
||||
showApproveConfirmDialog() {
|
||||
this.showingApproveConfirmDialog = true
|
||||
},
|
||||
hideApproveConfirmDialog () {
|
||||
hideApproveConfirmDialog() {
|
||||
this.showingApproveConfirmDialog = false
|
||||
},
|
||||
showDenyConfirmDialog () {
|
||||
showDenyConfirmDialog() {
|
||||
this.showingDenyConfirmDialog = true
|
||||
},
|
||||
hideDenyConfirmDialog () {
|
||||
hideDenyConfirmDialog() {
|
||||
this.showingDenyConfirmDialog = false
|
||||
},
|
||||
approveUser () {
|
||||
approveUser() {
|
||||
if (this.shouldConfirmApprove) {
|
||||
this.showApproveConfirmDialog()
|
||||
} else {
|
||||
this.doApprove()
|
||||
}
|
||||
},
|
||||
doApprove () {
|
||||
doApprove() {
|
||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
|
||||
|
@ -48,22 +50,23 @@ const FollowRequestCard = {
|
|||
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
|
||||
this.$store.dispatch('updateNotification', {
|
||||
id: notifId,
|
||||
updater: notification => {
|
||||
updater: (notification) => {
|
||||
notification.type = 'follow'
|
||||
}
|
||||
})
|
||||
this.hideApproveConfirmDialog()
|
||||
},
|
||||
denyUser () {
|
||||
denyUser() {
|
||||
if (this.shouldConfirmDeny) {
|
||||
this.showDenyConfirmDialog()
|
||||
} else {
|
||||
this.doDeny()
|
||||
}
|
||||
},
|
||||
doDeny () {
|
||||
doDeny() {
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
||||
this.$store.state.api.backendInteractor
|
||||
.denyUser({ id: this.user.id })
|
||||
.then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
|
@ -72,13 +75,13 @@ const FollowRequestCard = {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
mergedConfig () {
|
||||
mergedConfig() {
|
||||
return this.$store.getters.mergedConfig
|
||||
},
|
||||
shouldConfirmApprove () {
|
||||
shouldConfirmApprove() {
|
||||
return this.mergedConfig.modalOnApproveFollow
|
||||
},
|
||||
shouldConfirmDeny () {
|
||||
shouldConfirmDeny() {
|
||||
return this.mergedConfig.modalOnDenyFollow
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ const FollowRequests = {
|
|||
FollowRequestCard
|
||||
},
|
||||
computed: {
|
||||
requests () {
|
||||
requests() {
|
||||
return this.$store.state.api.followRequests
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,9 @@ export default {
|
|||
components: {
|
||||
Select
|
||||
},
|
||||
props: [
|
||||
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
|
||||
],
|
||||
props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
|
||||
emits: ['update:modelValue'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
lValue: this.modelValue,
|
||||
availableOptions: [
|
||||
|
@ -19,43 +17,45 @@ export default {
|
|||
'serif',
|
||||
'monospace',
|
||||
'sans-serif'
|
||||
].filter(_ => _)
|
||||
].filter((_) => _)
|
||||
}
|
||||
},
|
||||
beforeUpdate () {
|
||||
beforeUpdate() {
|
||||
this.lValue = this.modelValue
|
||||
},
|
||||
computed: {
|
||||
present () {
|
||||
present() {
|
||||
return typeof this.lValue !== 'undefined'
|
||||
},
|
||||
dValue () {
|
||||
dValue() {
|
||||
return this.lValue || this.fallback || {}
|
||||
},
|
||||
family: {
|
||||
get () {
|
||||
get() {
|
||||
return this.dValue.family
|
||||
},
|
||||
set (v) {
|
||||
set(v) {
|
||||
set(this.lValue, 'family', v)
|
||||
this.$emit('update:modelValue', this.lValue)
|
||||
}
|
||||
},
|
||||
isCustom () {
|
||||
isCustom() {
|
||||
return this.preset === 'custom'
|
||||
},
|
||||
preset: {
|
||||
get () {
|
||||
if (this.family === 'serif' ||
|
||||
this.family === 'sans-serif' ||
|
||||
this.family === 'monospace' ||
|
||||
this.family === 'inherit') {
|
||||
get() {
|
||||
if (
|
||||
this.family === 'serif' ||
|
||||
this.family === 'sans-serif' ||
|
||||
this.family === 'monospace' ||
|
||||
this.family === 'inherit'
|
||||
) {
|
||||
return this.family
|
||||
} else {
|
||||
return 'custom'
|
||||
}
|
||||
},
|
||||
set (v) {
|
||||
set(v) {
|
||||
this.family = v === 'custom' ? '' : v
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,13 @@
|
|||
class="opt exlcude-disabled"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
>
|
||||
@change="
|
||||
$emit(
|
||||
'update:modelValue',
|
||||
typeof modelValue === 'undefined' ? fallback : undefined
|
||||
)
|
||||
"
|
||||
/>
|
||||
<label
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
|
@ -43,11 +48,11 @@
|
|||
v-model="family"
|
||||
class="custom-font"
|
||||
type="text"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./font_control.js" ></script>
|
||||
<script src="./font_control.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
|
|
@ -4,7 +4,9 @@ const FriendsTimeline = {
|
|||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.friends }
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.friends
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const Gallery = {
|
|||
'editAttachment',
|
||||
'grid'
|
||||
],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
sizes: {},
|
||||
hidingLong: true
|
||||
|
@ -25,42 +25,61 @@ const Gallery = {
|
|||
},
|
||||
components: { Attachment },
|
||||
computed: {
|
||||
rows () {
|
||||
rows() {
|
||||
if (!this.attachments) {
|
||||
return []
|
||||
}
|
||||
const attachments = this.limit > 0
|
||||
? this.attachments.slice(0, this.limit)
|
||||
: this.attachments
|
||||
const attachments =
|
||||
this.limit > 0
|
||||
? this.attachments.slice(0, this.limit)
|
||||
: this.attachments
|
||||
if (this.size === 'hide') {
|
||||
return attachments.map(item => ({ minimal: true, items: [item] }))
|
||||
return attachments.map((item) => ({ minimal: true, items: [item] }))
|
||||
}
|
||||
const rows = this.grid
|
||||
? [{ grid: true, items: attachments }]
|
||||
: attachments.reduce((acc, attachment, i) => {
|
||||
if (attachment.mimetype.includes('audio')) {
|
||||
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
|
||||
}
|
||||
if (!(
|
||||
attachment.mimetype.includes('image') ||
|
||||
attachment.mimetype.includes('video') ||
|
||||
attachment.mimetype.includes('flash')
|
||||
)) {
|
||||
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
|
||||
}
|
||||
const maxPerRow = 3
|
||||
const attachmentsRemaining = this.attachments.length - i + 1
|
||||
const currentRow = acc[acc.length - 1].items
|
||||
currentRow.push(attachment)
|
||||
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
|
||||
return [...acc, { items: [] }]
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, [{ items: [] }]).filter(_ => _.items.length > 0)
|
||||
: attachments
|
||||
.reduce(
|
||||
(acc, attachment, i) => {
|
||||
if (attachment.mimetype.includes('audio')) {
|
||||
return [
|
||||
...acc,
|
||||
{ audio: true, items: [attachment] },
|
||||
{ items: [] }
|
||||
]
|
||||
}
|
||||
if (
|
||||
!(
|
||||
attachment.mimetype.includes('image') ||
|
||||
attachment.mimetype.includes('video') ||
|
||||
attachment.mimetype.includes('flash')
|
||||
)
|
||||
) {
|
||||
return [
|
||||
...acc,
|
||||
{ minimal: true, items: [attachment] },
|
||||
{ items: [] }
|
||||
]
|
||||
}
|
||||
const maxPerRow = 3
|
||||
const attachmentsRemaining = this.attachments.length - i + 1
|
||||
const currentRow = acc[acc.length - 1].items
|
||||
currentRow.push(attachment)
|
||||
if (
|
||||
currentRow.length >= maxPerRow &&
|
||||
attachmentsRemaining > maxPerRow
|
||||
) {
|
||||
return [...acc, { items: [] }]
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
},
|
||||
[{ items: [] }]
|
||||
)
|
||||
.filter((_) => _.items.length > 0)
|
||||
return rows
|
||||
},
|
||||
attachmentsDimensionalScore () {
|
||||
attachmentsDimensionalScore() {
|
||||
return this.rows.reduce((acc, row) => {
|
||||
let size = 0
|
||||
if (row.minimal) {
|
||||
|
@ -73,7 +92,7 @@ const Gallery = {
|
|||
return acc + size
|
||||
}, 0)
|
||||
},
|
||||
tooManyAttachments () {
|
||||
tooManyAttachments() {
|
||||
if (this.editable || this.size === 'small') {
|
||||
return false
|
||||
} else if (this.size === 'hide') {
|
||||
|
@ -84,32 +103,32 @@ const Gallery = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onNaturalSizeLoad ({ id, width, height }) {
|
||||
onNaturalSizeLoad({ id, width, height }) {
|
||||
set(this.sizes, id, { width, height })
|
||||
},
|
||||
rowStyle (row) {
|
||||
rowStyle(row) {
|
||||
if (row.audio) {
|
||||
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
||||
} else if (!row.minimal && !row.grid) {
|
||||
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
|
||||
return { 'padding-bottom': `${100 / (row.items.length + 0.6)}%` }
|
||||
}
|
||||
},
|
||||
itemStyle (id, row) {
|
||||
const total = sumBy(row, item => this.getAspectRatio(item.id))
|
||||
itemStyle(id, row) {
|
||||
const total = sumBy(row, (item) => this.getAspectRatio(item.id))
|
||||
return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
|
||||
},
|
||||
getAspectRatio (id) {
|
||||
getAspectRatio(id) {
|
||||
const size = this.sizes[id]
|
||||
return size ? size.width / size.height : 1
|
||||
},
|
||||
toggleHidingLong (event) {
|
||||
toggleHidingLong(event) {
|
||||
this.hidingLong = event
|
||||
},
|
||||
openGallery () {
|
||||
openGallery() {
|
||||
this.$store.dispatch('setMedia', this.attachments)
|
||||
this.$store.dispatch('setCurrentMedia', this.attachments[0])
|
||||
},
|
||||
onMedia () {
|
||||
onMedia() {
|
||||
this.$store.dispatch('setMedia', this.attachments)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,11 +25,20 @@
|
|||
:size="size"
|
||||
:editable="editable"
|
||||
:remove="removeAttachment"
|
||||
:shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
|
||||
:shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
|
||||
:shift-up="
|
||||
!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment
|
||||
"
|
||||
:shift-dn="
|
||||
!(
|
||||
attachmentIndex === row.items.length - 1 &&
|
||||
rowIndex === rows.length - 1
|
||||
) && shiftDnAttachment
|
||||
"
|
||||
:edit="editAttachment"
|
||||
:description="descriptions && descriptions[attachment.id]"
|
||||
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
|
||||
:hide-description="
|
||||
size === 'small' || (tooManyAttachments && hidingLong)
|
||||
"
|
||||
:style="itemStyle(attachment.id, row.items)"
|
||||
@setMedia="onMedia"
|
||||
@naturalSizeLoad="onNaturalSizeLoad"
|
||||
|
@ -42,7 +51,7 @@
|
|||
class="many-attachments"
|
||||
>
|
||||
<div class="many-attachments-text">
|
||||
{{ $t("status.many_attachments", { number: attachments.length }) }}
|
||||
{{ $t('status.many_attachments', { number: attachments.length }) }}
|
||||
</div>
|
||||
<div class="many-attachments-buttons">
|
||||
<span
|
||||
|
@ -53,7 +62,7 @@
|
|||
class="button-unstyled -link"
|
||||
@click="toggleHidingLong(true)"
|
||||
>
|
||||
{{ $t("status.collapse_attachments") }}
|
||||
{{ $t('status.collapse_attachments') }}
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
|
@ -64,7 +73,7 @@
|
|||
class="button-unstyled -link"
|
||||
@click="toggleHidingLong(false)"
|
||||
>
|
||||
{{ $t("status.show_all_attachments") }}
|
||||
{{ $t('status.show_all_attachments') }}
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
|
@ -75,7 +84,7 @@
|
|||
class="button-unstyled -link"
|
||||
@click="openGallery"
|
||||
>
|
||||
{{ $t("status.open_gallery") }}
|
||||
{{ $t('status.open_gallery') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -83,7 +92,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src='./gallery.js'></script>
|
||||
<script src="./gallery.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
@ -109,8 +118,8 @@
|
|||
.gallery-rows {
|
||||
max-height: 25em;
|
||||
overflow: hidden;
|
||||
mask:
|
||||
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px
|
||||
no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes
|
||||
)
|
||||
library.add(faTimes)
|
||||
|
||||
const GlobalNoticeList = {
|
||||
computed: {
|
||||
notices () {
|
||||
notices() {
|
||||
return this.$store.state.interface.globalNotices
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeNotice (notice) {
|
||||
closeNotice(notice) {
|
||||
this.$store.dispatch('removeGlobalNotice', notice)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const HashtagLink = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
onClick() {
|
||||
const tag = this.tag || extractTagFromUrl(this.url)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
|
@ -27,7 +27,7 @@ const HashtagLink = {
|
|||
window.open(this.url, '_blank')
|
||||
}
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
generateTagLink(tag) {
|
||||
return `/tag/${tag}`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<template>
|
||||
<span
|
||||
class="HashtagLink"
|
||||
>
|
||||
<span class="HashtagLink">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<a
|
||||
:href="url"
|
||||
|
@ -14,6 +12,6 @@
|
|||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./hashtag_link.js"/>
|
||||
<script src="./hashtag_link.js" />
|
||||
|
||||
<style lang="scss" src="./hashtag_link.scss"/>
|
||||
<style lang="scss" src="./hashtag_link.scss" />
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import Cropper from 'cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCircleNotch
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const ImageCropper = {
|
||||
props: {
|
||||
|
@ -21,7 +17,7 @@ const ImageCropper = {
|
|||
},
|
||||
cropperOptions: {
|
||||
type: Object,
|
||||
default () {
|
||||
default() {
|
||||
return {
|
||||
aspectRatio: 1,
|
||||
autoCropArea: 1,
|
||||
|
@ -46,7 +42,7 @@ const ImageCropper = {
|
|||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
cropper: undefined,
|
||||
dataUrl: undefined,
|
||||
|
@ -55,18 +51,21 @@ const ImageCropper = {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
saveText () {
|
||||
saveText() {
|
||||
return this.saveButtonLabel || this.$t('image_cropper.save')
|
||||
},
|
||||
saveWithoutCroppingText () {
|
||||
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping')
|
||||
saveWithoutCroppingText() {
|
||||
return (
|
||||
this.saveWithoutCroppingButtonlabel ||
|
||||
this.$t('image_cropper.save_without_cropping')
|
||||
)
|
||||
},
|
||||
cancelText () {
|
||||
cancelText() {
|
||||
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
destroy () {
|
||||
destroy() {
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy()
|
||||
}
|
||||
|
@ -74,7 +73,7 @@ const ImageCropper = {
|
|||
this.dataUrl = undefined
|
||||
this.$emit('close')
|
||||
},
|
||||
submit (cropping = true) {
|
||||
submit(cropping = true) {
|
||||
this.submitting = true
|
||||
this.submitHandler(cropping && this.cropper, this.file)
|
||||
.then(() => this.destroy())
|
||||
|
@ -82,16 +81,18 @@ const ImageCropper = {
|
|||
this.submitting = false
|
||||
})
|
||||
},
|
||||
pickImage () {
|
||||
pickImage() {
|
||||
this.$refs.input.click()
|
||||
},
|
||||
createCropper () {
|
||||
createCropper() {
|
||||
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
|
||||
},
|
||||
getTriggerDOM () {
|
||||
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
||||
getTriggerDOM() {
|
||||
return typeof this.trigger === 'object'
|
||||
? this.trigger
|
||||
: document.querySelector(this.trigger)
|
||||
},
|
||||
readFile () {
|
||||
readFile() {
|
||||
const fileInput = this.$refs.input
|
||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||
this.file = fileInput.files[0]
|
||||
|
@ -105,7 +106,7 @@ const ImageCropper = {
|
|||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
// listen for click event on trigger
|
||||
const trigger = this.getTriggerDOM()
|
||||
if (!trigger) {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:src="dataUrl"
|
||||
alt=""
|
||||
@load.stop="createCropper"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="image-cropper-buttons-wrapper">
|
||||
<button
|
||||
|
@ -43,7 +43,7 @@
|
|||
type="file"
|
||||
class="image-cropper-img-input"
|
||||
:accept="mimes"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCircleNotch,
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCircleNotch, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch,
|
||||
faTimes
|
||||
)
|
||||
library.add(faCircleNotch, faTimes)
|
||||
|
||||
const Importer = {
|
||||
props: {
|
||||
|
@ -19,7 +13,7 @@ const Importer = {
|
|||
successMessage: { type: String },
|
||||
errorMessage: { type: String }
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
error: false,
|
||||
|
@ -28,18 +22,24 @@ const Importer = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
change () {
|
||||
change() {
|
||||
this.file = this.$refs.input.files[0]
|
||||
},
|
||||
submit () {
|
||||
submit() {
|
||||
this.dismiss()
|
||||
this.submitting = true
|
||||
this.submitHandler(this.file)
|
||||
.then(() => { this.success = true })
|
||||
.catch(() => { this.error = true })
|
||||
.finally(() => { this.submitting = false })
|
||||
.then(() => {
|
||||
this.success = true
|
||||
})
|
||||
.catch(() => {
|
||||
this.error = true
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
dismiss () {
|
||||
dismiss() {
|
||||
this.success = false
|
||||
this.error = false
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
ref="input"
|
||||
type="file"
|
||||
@change="change"
|
||||
>
|
||||
/>
|
||||
</form>
|
||||
<FAIcon
|
||||
v-if="submitting"
|
||||
|
@ -25,9 +25,7 @@
|
|||
class="button-unstyled"
|
||||
@click="dismiss"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
/>
|
||||
<FAIcon icon="times" />
|
||||
</button>
|
||||
{{ ' ' }}
|
||||
<span>{{ successMessage || $t('importer.success') }}</span>
|
||||
|
@ -37,9 +35,7 @@
|
|||
class="button-unstyled"
|
||||
@click="dismiss"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
/>
|
||||
<FAIcon icon="times" />
|
||||
</button>
|
||||
{{ ' ' }}
|
||||
<span>{{ errorMessage || $t('importer.error') }}</span>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const InstanceSpecificPanel = {
|
||||
computed: {
|
||||
instanceSpecificPanelContent () {
|
||||
instanceSpecificPanelContent() {
|
||||
return this.$store.state.instance.instanceSpecificPanelContent
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,4 +10,4 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./instance_specific_panel.js" ></script>
|
||||
<script src="./instance_specific_panel.js"></script>
|
||||
|
|
|
@ -9,14 +9,15 @@ const tabModeDict = {
|
|||
}
|
||||
|
||||
const Interactions = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
allowFollowingMove:
|
||||
this.$store.state.users.currentUser.allow_following_move,
|
||||
filterMode: tabModeDict['mentions']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onModeSwitch (key) {
|
||||
onModeSwitch(key) {
|
||||
this.filterMode = tabModeDict[key]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t("nav.interactions") }}
|
||||
{{ $t('nav.interactions') }}
|
||||
</div>
|
||||
</div>
|
||||
<tab-switcher
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue