forked from AkkomaGang/akkoma-fe
Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
2c9b73646c | |||
80a519d7e4 | |||
975f04bf5a | |||
c8c8d40827 | |||
d7499a1f91 | |||
5972d89117 | |||
d03872d598 |
456 changed files with 11628 additions and 15698 deletions
6
.babelrc
6
.babelrc
|
@ -1,9 +1,5 @@
|
|||
{
|
||||
"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,9 +5,14 @@ module.exports = {
|
|||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'],
|
||||
extends: [
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: ['vue', 'import'],
|
||||
plugins: [
|
||||
'vue',
|
||||
'import'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow paren-less arrow functions
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
name: "Bug report"
|
||||
about: "Something isn't working as expected"
|
||||
title: "[bug] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thanks for taking the time to file this bug report! Please try to be as specific and detailed as you can, so we can track down the issue and fix it as soon as possible."
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: "Version"
|
||||
description: "Which version of pleroma-fe are you running? If running develop, specify the commit hash."
|
||||
placeholder: "e.g. 2022.11, 40e86998e6"
|
||||
- type: textarea
|
||||
id: attempt
|
||||
attributes:
|
||||
label: "What were you trying to do?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expectation
|
||||
attributes:
|
||||
label: "What did you expect to happen?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reality
|
||||
attributes:
|
||||
label: "What actually happened?"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: "Severity"
|
||||
description: "Does this issue prevent you from using the software as normal?"
|
||||
options:
|
||||
- "I cannot use the software"
|
||||
- "I cannot use it as easily as I'd like"
|
||||
- "I can manage"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: "Have you searched for this issue?"
|
||||
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev) or [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues)."
|
||||
options:
|
||||
- label: "I have double-checked and have not found this issue mentioned anywhere."
|
|
@ -1,29 +0,0 @@
|
|||
name: "Feature request"
|
||||
about: "I'd like something to be added to pleroma-fe"
|
||||
title: "[feat] "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Thanks for taking the time to request a new feature! Please be as concise and clear as you can in your proposal, so we could understand what you're going for."
|
||||
- type: textarea
|
||||
id: idea
|
||||
attributes:
|
||||
label: "The idea"
|
||||
description: "What do you think you should be able to do in pleroma-fe?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reason
|
||||
attributes:
|
||||
label: "The reasoning"
|
||||
description: "Why would this be a worthwhile feature? Does it solve any problems? Have people talked about wanting it?"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: "Have you searched for this feature request?"
|
||||
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev), [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues), or the one for [the backend](https://akkoma.dev/AkkomaGang/akkoma/issues)."
|
||||
options:
|
||||
- label: "I have double-checked and have not found this feature request mentioned anywhere."
|
||||
- label: "This feature is related to the pleroma-fe Akkoma frontend specifically, and not the backend."
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,4 +9,3 @@ selenium-debug.log
|
|||
config/local.json
|
||||
config/local.*.json
|
||||
docs/site/
|
||||
.vscode/
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"singleAttributePerLine": true
|
||||
}
|
|
@ -1,13 +1,19 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-config-recommended-vue/scss",
|
||||
"stylelint-rscss/config",
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-standard"
|
||||
],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"declaration-no-important": true,
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null
|
||||
"rscss/no-descendant-combinator": false,
|
||||
"rscss/class-format": [
|
||||
true,
|
||||
{
|
||||
"component": "pascal-case",
|
||||
"variant": "^-[a-z]\\w+",
|
||||
"element": "^[a-z]\\w+"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ pipeline:
|
|||
commands:
|
||||
- yarn
|
||||
- yarn lint
|
||||
#- yarn stylelint
|
||||
- yarn stylelint
|
||||
|
||||
test:
|
||||
when:
|
||||
|
|
12
README.md
12
README.md
|
@ -1,22 +1,22 @@
|
|||
# Akkoma-FE
|
||||
# Pleroma-FE
|
||||
|
||||
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
|
||||
|
||||
This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as:
|
||||
This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as:
|
||||
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
|
||||
- Custom emoji reactions
|
||||
|
||||
# For Translators
|
||||
|
||||
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
|
||||
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Pleroma-FE.
|
||||
|
||||
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
|
||||
|
||||
Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
||||
Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
|
||||
|
||||
# FOR ADMINS
|
||||
|
||||
To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
|
||||
To use Pleroma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Pleroma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
|
||||
|
||||
## Build Setup
|
||||
|
||||
|
@ -52,4 +52,4 @@ Edit config.json for configuration.
|
|||
|
||||
### Login methods
|
||||
|
||||
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
|
||||
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
|
||||
|
|
|
@ -11,17 +11,14 @@ 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)
|
||||
|
@ -29,13 +26,11 @@ 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,7 +2,8 @@ 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 = [
|
||||
|
@ -23,23 +24,16 @@ 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,10 +6,9 @@ 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
|
||||
|
@ -51,10 +50,7 @@ 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,8 +4,7 @@ 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)
|
||||
|
@ -14,7 +13,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) {
|
||||
|
@ -28,11 +27,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$/,
|
||||
|
@ -53,8 +52,8 @@ exports.cssLoaders = function (options) {
|
|||
},
|
||||
{
|
||||
test: /\.styl(us)?$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader'])
|
||||
}
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,14 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
const WorkboxPlugin = require('workbox-webpack-plugin');
|
||||
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,12 +19,9 @@ 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: {
|
||||
|
@ -33,15 +31,17 @@ 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'
|
||||
}
|
||||
},
|
||||
|
@ -67,15 +67,14 @@ 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$/,
|
||||
|
@ -116,8 +115,15 @@ module.exports = {
|
|||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [new VueLoaderPlugin()]
|
||||
plugins: [
|
||||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
||||
swDest: 'sw-pleroma.js',
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
|
||||
}),
|
||||
new VueLoaderPlugin()
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,9 +7,7 @@ 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, {
|
||||
|
@ -22,10 +20,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,27 +2,22 @@ var path = require('path')
|
|||
var config = require('../config')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
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: {
|
||||
|
@ -37,18 +32,13 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
|
||||
},
|
||||
plugins: [
|
||||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
||||
swDest: 'sw-pleroma.js',
|
||||
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({
|
||||
|
@ -58,8 +48,9 @@ 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: {
|
||||
|
@ -72,7 +63,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
|
||||
|
@ -90,7 +81,9 @@ 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,11 +38,6 @@ module.exports = {
|
|||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
'/manifest.json': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/api': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
|
@ -59,7 +54,7 @@ module.exports = {
|
|||
cookieDomainRewrite: 'localhost',
|
||||
ws: true,
|
||||
headers: {
|
||||
Origin: target
|
||||
'Origin': target
|
||||
}
|
||||
},
|
||||
'/oauth/revoke': {
|
||||
|
@ -76,7 +71,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
|
||||
|
|
40
index.html
40
index.html
|
@ -1,25 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<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/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/theme-holder.css" id="theme-holder">
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
</head>
|
||||
|
||||
<body class="hidden">
|
||||
<noscript>To use Akkoma, please enable JavaScript.</noscript>
|
||||
<div id="app"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
<head>
|
||||
<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">
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
</head>
|
||||
<body class="hidden">
|
||||
<noscript>To use Akkoma, please enable JavaScript.</noscript>
|
||||
<div id="app"></div>
|
||||
<div id="modal"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
33
package.json
33
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "3.5.0",
|
||||
"version": "3.2.0",
|
||||
"description": "A frontend for Akkoma instances",
|
||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||
"private": true,
|
||||
|
@ -11,30 +11,30 @@
|
|||
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"test": "npm run unit && npm run e2e",
|
||||
"stylelint": "stylelint src/**/*.scss",
|
||||
"stylelint": "npx stylelint src/components/status/status.scss",
|
||||
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.17.8",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@floatingghost/pinch-zoom-element": "^1.3.1",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@vuelidate/core": "^2.0.0",
|
||||
"@vuelidate/validators": "^2.0.0",
|
||||
"blurhash": "^2.0.4",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@vuelidate/core": "2.0.0-alpha.42",
|
||||
"@vuelidate/validators": "2.0.0-alpha.30",
|
||||
"body-scroll-lock": "2.7.1",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "1.5.12",
|
||||
"diff": "3.5.0",
|
||||
"escape-html": "1.0.3",
|
||||
"iso-639-1": "^2.1.15",
|
||||
"js-cookie": "^3.0.1",
|
||||
"localforage": "1.10.0",
|
||||
"marked": "^4.2.2",
|
||||
"marked-mfm": "^0.5.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"phoenix": "1.6.2",
|
||||
"punycode.js": "2.1.0",
|
||||
|
@ -58,7 +58,7 @@
|
|||
"@vue/babel-plugin-jsx": "1.1.1",
|
||||
"@vue/compiler-sfc": "^3.1.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"autoprefixer": "6.7.7",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "^4.3.7",
|
||||
|
@ -69,13 +69,11 @@
|
|||
"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",
|
||||
|
@ -86,6 +84,7 @@
|
|||
"html-webpack-plugin": "^5.5.0",
|
||||
"http-proxy-middleware": "0.21.0",
|
||||
"inject-loader": "2.0.1",
|
||||
"iso-639-1": "2.1.15",
|
||||
"isparta-loader": "2.0.0",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "6.3.17",
|
||||
|
@ -104,11 +103,7 @@
|
|||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"ora": "0.4.1",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "^7.0.2",
|
||||
"postcss-sass": "^0.5.0",
|
||||
"prettier": "2.8.1",
|
||||
"postcss-loader": "3.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "^1.56.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
|
@ -117,11 +112,9 @@
|
|||
"shelljs": "0.8.5",
|
||||
"sinon": "2.4.1",
|
||||
"sinon-chai": "2.14.0",
|
||||
"stylelint": "^14.15.0",
|
||||
"stylelint-config-recommended-vue": "^1.4.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-config-standard-scss": "^6.1.0",
|
||||
"stylelint-rscss": "^0.4.0",
|
||||
"stylelint": "13.6.1",
|
||||
"stylelint-config-standard": "20.0.0",
|
||||
"stylelint-rscss": "0.4.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: [require('autoprefixer')]
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"]
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
|
|
83
src/App.js
83
src/App.js
|
@ -24,9 +24,7 @@ export default {
|
|||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(() =>
|
||||
import('./components/notifications/notifications.vue')
|
||||
),
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
@ -46,20 +44,17 @@ 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,
|
||||
|
@ -69,76 +64,48 @@ 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())
|
||||
}
|
||||
|
|
136
src/App.scss
136
src/App.scss
|
@ -1,7 +1,6 @@
|
|||
// stylelint-disable rscss/class-format
|
||||
@import './_variables.scss';
|
||||
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
|
||||
:root {
|
||||
--navbar-height: 3.5rem;
|
||||
--post-line-height: 1.4;
|
||||
|
@ -13,8 +12,8 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: $system-sans-serif;
|
||||
font-family: var(--interfaceFont, $system-sans-serif);
|
||||
font-family: sans-serif;
|
||||
font-family: var(--interfaceFont, sans-serif);
|
||||
margin: 0;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
|
@ -23,13 +22,84 @@ 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;
|
||||
|
@ -40,7 +110,7 @@ h4 {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
i[class*='icon-'],
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--icon;
|
||||
color: var(--icon, $fallback--icon);
|
||||
|
@ -58,7 +128,6 @@ nav {
|
|||
box-sizing: border-box;
|
||||
height: var(--navbar-height);
|
||||
position: fixed;
|
||||
backdrop-filter: blur(12px) saturate(1.2);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
|
@ -113,7 +182,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;
|
||||
|
@ -122,7 +191,6 @@ nav {
|
|||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
padding: 0 calc(var(--columnGap) / 2);
|
||||
|
||||
.column {
|
||||
--___columnMargin: var(--columnGap);
|
||||
|
@ -160,9 +228,7 @@ 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
|
||||
|
@ -170,9 +236,7 @@ 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;
|
||||
|
@ -212,21 +276,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 {
|
||||
|
@ -283,7 +347,7 @@ nav {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
i[class*='icon-'],
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
|
@ -299,9 +363,7 @@ 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);
|
||||
|
@ -334,9 +396,7 @@ 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,
|
||||
|
@ -401,15 +461,14 @@ 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);
|
||||
color: $fallback--lightText;
|
||||
color: var(--inputText, $fallback--lightText);
|
||||
font-family: sans-serif;
|
||||
font-family: var(--interfaceFont, sans-serif);
|
||||
font-family: var(--inputFont, sans-serif);
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
|
@ -420,13 +479,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;
|
||||
|
@ -434,13 +493,12 @@ textarea,
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
&[type='radio'] {
|
||||
&[type=radio] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset,
|
||||
0 0 0 4px var(--fg, $fallback--fg) inset;
|
||||
box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;
|
||||
background-color: var(--accent, $fallback--link);
|
||||
}
|
||||
|
||||
|
@ -461,7 +519,7 @@ textarea,
|
|||
height: 1.1em;
|
||||
border-radius: 100%; // Radio buttons should always be circle
|
||||
box-shadow: 0 0 2px black inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
margin-right: 0.5em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
@ -475,7 +533,7 @@ textarea,
|
|||
}
|
||||
}
|
||||
|
||||
&[type='checkbox'] {
|
||||
&[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
|
@ -501,7 +559,7 @@ textarea,
|
|||
border-radius: $fallback--checkboxRadius;
|
||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||
box-shadow: 0 0 2px black inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
margin-right: 0.5em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
@ -536,8 +594,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, 0.5);
|
||||
$fallback--faint: rgba(185, 185, 186, .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, 0.5);
|
||||
$fallback--alertWarning: rgba(111, 111, 20, 0.5);
|
||||
$fallback--alertError: rgba(211,16,20,.5);
|
||||
$fallback--alertWarning: rgba(111,111,20,.5);
|
||||
|
||||
$fallback--panelRadius: 10px;
|
||||
$fallback--checkboxRadius: 2px;
|
||||
|
@ -28,14 +28,6 @@ $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,21 +3,13 @@ import { createApp } from 'vue'
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeLayers
|
||||
} from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
config.autoAddCss = false
|
||||
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'
|
||||
|
@ -31,9 +23,7 @@ const parsedInitialResults = () => {
|
|||
return null
|
||||
}
|
||||
if (!staticInitialResults) {
|
||||
staticInitialResults = JSON.parse(
|
||||
document.getElementById('initial-results').textContent
|
||||
)
|
||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||
}
|
||||
return staticInitialResults
|
||||
}
|
||||
|
@ -81,30 +71,18 @@ 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')
|
||||
|
@ -119,12 +97,10 @@ 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)
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +111,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.')
|
||||
|
@ -174,16 +150,19 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('showPanelNavShortcuts')
|
||||
copyInstanceOption('stopGifs')
|
||||
copyInstanceOption('logo')
|
||||
copyInstanceOption('conversationDisplay')
|
||||
|
||||
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)
|
||||
|
@ -211,7 +190,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")
|
||||
|
@ -224,12 +203,9 @@ 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")
|
||||
|
@ -242,30 +218,25 @@ 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")
|
||||
|
@ -280,19 +251,13 @@ 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 }) => {
|
||||
|
@ -302,146 +267,65 @@ 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
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'publicTimelineVisibility',
|
||||
value: metadata.publicTimelineVisibility
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federatedTimelineAvailable',
|
||||
value: metadata.federatedTimelineAvailable
|
||||
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')
|
||||
|
@ -451,16 +335,11 @@ 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 }) => {
|
||||
|
@ -483,10 +362,7 @@ 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 })
|
||||
|
@ -496,10 +372,7 @@ 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)
|
||||
|
@ -520,6 +393,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
])
|
||||
|
||||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
store.dispatch('startFetchingAnnouncements')
|
||||
store.dispatch('startFetchingReports')
|
||||
getTOS({ store })
|
||||
getStickers({ store })
|
||||
|
||||
|
@ -527,7 +403,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 {}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,8 +22,6 @@ import Lists from 'components/lists/lists.vue'
|
|||
import ListTimeline from 'components/list_timeline/list_timeline.vue'
|
||||
import ListEdit from 'components/list_edit/list_edit.vue'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
import RegistrationRequestSent from 'components/registration_request_sent/registration_request_sent.vue'
|
||||
import AwaitingEmailConfirmation from 'components/awaiting_email_confirmation/awaiting_email_confirmation.vue'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
|
@ -35,145 +33,49 @@ 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: '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,17 +15,13 @@ 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,6 +9,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./about.js"></script>
|
||||
<script src="./about.js" ></script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
</style>
|
||||
|
|
|
@ -3,13 +3,19 @@ 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
|
||||
}
|
||||
|
@ -20,59 +26,45 @@ const AccountActions = {
|
|||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
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])
|
||||
.then(() => this.refetchRelationship())
|
||||
},
|
||||
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 #content>
|
||||
<template v-slot:content>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="relationship.following">
|
||||
<button
|
||||
|
@ -55,23 +55,9 @@
|
|||
>
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="relationship.domain_blocking"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="unmuteDomain"
|
||||
>
|
||||
{{ $t('user_card.domain_muted') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="!user.is_local"
|
||||
class="btn button-default btn-block dropdown-item"
|
||||
@click="muteDomain"
|
||||
>
|
||||
{{ $t('user_card.mute_domain') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<template v-slot:trigger>
|
||||
<button class="button-unstyled ellipsis-button">
|
||||
<FAIcon
|
||||
class="icon"
|
||||
|
@ -93,8 +79,10 @@
|
|||
keypath="user_card.block_confirm"
|
||||
tag="span"
|
||||
>
|
||||
<template #user>
|
||||
<span v-text="user.screen_name_ui" />
|
||||
<template v-slot: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,93 +25,78 @@ 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,9 +21,7 @@
|
|||
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 }) }}
|
||||
|
@ -101,7 +99,7 @@
|
|||
<script src="./announcement.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
@import "../../variables";
|
||||
|
||||
.announcement {
|
||||
border-bottom-width: 1px;
|
||||
|
@ -110,8 +108,7 @@
|
|||
border-radius: 0;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading,
|
||||
.body {
|
||||
.heading, .body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,26 +10,22 @@
|
|||
: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
|
||||
|
@ -37,9 +33,7 @@
|
|||
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,35 +19,34 @@ 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,7 +6,9 @@
|
|||
</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>
|
||||
|
@ -48,7 +50,9 @@
|
|||
v-for="announcement in announcements"
|
||||
:key="announcement.id"
|
||||
>
|
||||
<announcement :announcement="announcement" />
|
||||
<announcement
|
||||
:announcement="announcement"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,14 +61,13 @@
|
|||
<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: 0.5em;
|
||||
padding: 0.5em 2em;
|
||||
margin: .5em;
|
||||
padding: .5em 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
faPencilAlt,
|
||||
faAlignRight
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import Blurhash from '../blurhash/Blurhash.vue'
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
|
@ -47,16 +46,14 @@ 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,11 +63,10 @@ const Attachment = {
|
|||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment,
|
||||
Blurhash
|
||||
VideoAttachment
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
classNames () {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
|
@ -82,40 +78,37 @@ const Attachment = {
|
|||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
]
|
||||
},
|
||||
usePlaceholder() {
|
||||
usePlaceholder () {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit() {
|
||||
useContainFit () {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
useBlurhash() {
|
||||
return this.$store.getters.mergedConfig.useBlurhash
|
||||
},
|
||||
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':
|
||||
|
@ -130,29 +123,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)
|
||||
|
@ -160,35 +153,34 @@ 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)
|
||||
|
@ -209,16 +201,14 @@ 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 === '') {
|
||||
|
@ -229,16 +219,14 @@ 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,6 +117,7 @@
|
|||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
@click.prevent
|
||||
>
|
||||
<FAIcon :icon="placeholderIconClass" />
|
||||
<b>{{ nsfw ? 'NSFW / ' : '' }}</b
|
||||
>{{ edit ? '' : placeholderName }}
|
||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
||||
</a>
|
||||
<div
|
||||
v-if="edit || remove"
|
||||
|
@ -31,11 +30,7 @@
|
|||
</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 }"
|
||||
>
|
||||
|
@ -46,7 +41,7 @@
|
|||
class="description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
/>
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
|
@ -69,19 +64,11 @@
|
|||
:title="attachment.description"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
>
|
||||
<Blurhash
|
||||
v-if="useBlurhash && attachment.blurhash"
|
||||
:height="512"
|
||||
:width="1024"
|
||||
:hash="attachment.blurhash"
|
||||
:punch="1"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:key="nsfwImage"
|
||||
class="nsfw"
|
||||
:src="nsfwImage"
|
||||
/>
|
||||
>
|
||||
<FAIcon
|
||||
v-if="type === 'video'"
|
||||
class="play-icon"
|
||||
|
@ -101,12 +88,7 @@
|
|||
<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"
|
||||
|
@ -158,7 +140,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"
|
||||
|
@ -236,13 +218,11 @@
|
|||
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>
|
||||
|
@ -264,11 +244,7 @@
|
|||
</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,17 +6,13 @@ 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,13 +2,11 @@ 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: {
|
||||
|
@ -16,7 +14,7 @@ export default {
|
|||
default: 'Search...'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
term: '',
|
||||
timeout: null,
|
||||
|
@ -25,31 +23,29 @@ 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,12 +12,8 @@ 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,3 +0,0 @@
|
|||
export default {
|
||||
computed: {}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4>{{ $t('registration.awaiting_email_confirmation_title') }}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>{{ $t('registration.awaiting_email_confirmation') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./awaiting_email_confirmation.js"></script>
|
|
@ -4,8 +4,10 @@ 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
|
||||
}
|
||||
|
@ -16,15 +18,11 @@ 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
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
<template>
|
||||
<canvas
|
||||
ref="canvas"
|
||||
class="blurhash"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { decode } from 'blurhash'
|
||||
|
||||
export default {
|
||||
name: 'Blurhash',
|
||||
props: {
|
||||
hash: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
punch: {
|
||||
type: Number,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
canvas: null,
|
||||
ctx: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.canvas = this.$refs.canvas
|
||||
this.ctx = this.canvas.getContext('2d')
|
||||
this.canvas.width = 1024
|
||||
this.canvas.height = 512
|
||||
this.draw()
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
const pixels = decode(this.hash, this.width, this.height, this.punch)
|
||||
const imageData = this.ctx.createImageData(this.width, this.height)
|
||||
imageData.data.set(pixels)
|
||||
this.ctx.putImageData(imageData, 0, 0)
|
||||
fetch('/static/blurhash-overlay.png')
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const img = new Image()
|
||||
img.src = URL.createObjectURL(blob)
|
||||
img.onload = () => {
|
||||
this.ctx.drawImage(img, 0, 0, this.width, this.height)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -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,16 +4,15 @@ 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,17 +11,14 @@ 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,8 +22,12 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: ['modelValue', 'indeterminate', 'disabled'],
|
||||
emits: ['update:modelValue']
|
||||
emits: ['update:modelValue'],
|
||||
props: [
|
||||
'modelValue',
|
||||
'indeterminate',
|
||||
'disabled'
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -52,7 +56,7 @@ export default {
|
|||
border-radius: $fallback--checkboxRadius;
|
||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||
box-shadow: 0px 0px 2px black inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
vertical-align: top;
|
||||
|
@ -67,7 +71,7 @@ export default {
|
|||
&.disabled {
|
||||
.checkbox-indicator::before,
|
||||
.label {
|
||||
opacity: 0.5;
|
||||
opacity: .5;
|
||||
}
|
||||
.label {
|
||||
color: $fallback--faint;
|
||||
|
@ -75,7 +79,7 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
|
@ -88,10 +92,11 @@ export default {
|
|||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
& > span {
|
||||
margin-left: 0.5em;
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
flex: 0 0 0;
|
||||
max-width: 9em;
|
||||
align-items: stretch;
|
||||
padding: 0.2em 8px;
|
||||
padding: .2em 8px;
|
||||
|
||||
input {
|
||||
background: none;
|
||||
|
@ -40,10 +40,9 @@
|
|||
}
|
||||
.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;
|
||||
|
@ -65,4 +64,5 @@
|
|||
.label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,12 +14,7 @@
|
|||
: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
|
||||
|
@ -29,7 +24,7 @@
|
|||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
>
|
||||
<input
|
||||
v-if="validColor"
|
||||
:id="name"
|
||||
|
@ -38,7 +33,7 @@
|
|||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
v-if="transparentColor"
|
||||
class="transparentIndicator"
|
||||
|
@ -46,11 +41,12 @@
|
|||
<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'
|
||||
|
@ -97,22 +93,21 @@ 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,12 +22,13 @@ const ConfirmModal = {
|
|||
type: String
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
onCancel() {
|
||||
onCancel () {
|
||||
this.$emit('cancelled')
|
||||
},
|
||||
onAccept() {
|
||||
onAccept () {
|
||||
this.$emit('accepted')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,6 @@
|
|||
</dialog-modal>
|
||||
</template>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../_variables';
|
||||
|
||||
|
@ -37,3 +35,5 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
|
|
@ -43,7 +43,11 @@ import {
|
|||
faThumbsUp
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faAdjust, faExclamationTriangle, faThumbsUp)
|
||||
library.add(
|
||||
faAdjust,
|
||||
faExclamationTriangle,
|
||||
faThumbsUp
|
||||
)
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -61,35 +65,19 @@ 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,7 +11,11 @@ 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
|
||||
|
@ -35,17 +39,16 @@ 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,
|
||||
|
@ -63,78 +66,74 @@ 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 []
|
||||
}
|
||||
|
@ -143,203 +142,155 @@ 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: {}
|
||||
return a
|
||||
}, {
|
||||
forest: {}
|
||||
})
|
||||
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
}
|
||||
)
|
||||
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) =>
|
||||
topLevel
|
||||
.map((id) => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
}
|
||||
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), [])
|
||||
|
||||
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)
|
||||
)
|
||||
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]
|
||||
|
@ -347,7 +298,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'
|
||||
|
@ -358,7 +309,7 @@ const conversation = {
|
|||
return a
|
||||
}, {})
|
||||
},
|
||||
statusContentProperties() {
|
||||
statusContentProperties () {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
const id = k.id
|
||||
const props = (() => {
|
||||
|
@ -383,20 +334,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: {
|
||||
|
@ -404,59 +355,53 @@ 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
|
||||
|
||||
|
@ -467,38 +412,32 @@ 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]: {
|
||||
|
@ -507,14 +446,10 @@ 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) {
|
||||
|
@ -528,20 +463,18 @@ 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
|
||||
}
|
||||
|
@ -570,13 +503,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
|
||||
|
@ -587,11 +520,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) {
|
||||
|
@ -600,7 +533,7 @@ const conversation = {
|
|||
}
|
||||
return ancestors
|
||||
},
|
||||
topLevelAncestorOrSelfId(id) {
|
||||
topLevelAncestorOrSelfId (id) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(id)
|
||||
while (parent) {
|
||||
|
@ -609,7 +542,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,15 +35,13 @@
|
|||
@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>
|
||||
|
@ -56,20 +54,14 @@
|
|||
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()"
|
||||
|
@ -77,6 +69,7 @@
|
|||
: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"
|
||||
|
@ -85,47 +78,28 @@
|
|||
: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"
|
||||
|
@ -134,17 +108,13 @@
|
|||
@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>
|
||||
|
@ -157,6 +127,7 @@
|
|||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
:depth="0"
|
||||
|
||||
:status="status"
|
||||
:in-profile="inProfile"
|
||||
:conversation="conversation"
|
||||
|
@ -164,11 +135,13 @@
|
|||
: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"
|
||||
|
@ -192,9 +165,7 @@
|
|||
: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()"
|
||||
|
@ -202,6 +173,7 @@
|
|||
: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"
|
||||
|
@ -210,6 +182,7 @@
|
|||
:status-content-properties="statusContentProperties"
|
||||
:set-status-content-property="setStatusContentProperty"
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
|
@ -260,8 +233,7 @@
|
|||
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;
|
||||
|
@ -281,7 +253,8 @@
|
|||
.thread-ancestor-has-other-replies .conversation-status,
|
||||
.thread-ancestor:last-child .conversation-status,
|
||||
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||
&:last-child .conversation-status {
|
||||
&:last-child .conversation-status,
|
||||
&.-expanded .thread-tree .conversation-status {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
@ -298,8 +271,7 @@
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import SearchBar from 'components/search_bar/search_bar.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
publicTimelineVisible,
|
||||
federatedTimelineVisible,
|
||||
bubbleTimelineVisible
|
||||
} from '../../lib/timeline_visibility'
|
||||
import {
|
||||
faSignInAlt,
|
||||
faSignOutAlt,
|
||||
|
@ -24,7 +19,6 @@ import {
|
|||
faInfoCircle,
|
||||
faUserTie
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
library.add(
|
||||
faSignInAlt,
|
||||
|
@ -52,103 +46,76 @@ 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
|
||||
},
|
||||
...mapState({
|
||||
publicTimelineVisible,
|
||||
federatedTimelineVisible,
|
||||
bubbleTimelineVisible
|
||||
})
|
||||
}
|
||||
},
|
||||
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,12 +24,11 @@
|
|||
|
||||
&.-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);
|
||||
}
|
||||
|
@ -50,7 +49,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,25 +88,13 @@
|
|||
width: 2em;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
|
||||
&.router-link-active {
|
||||
// box-shadow: 0 -2px 0 var(--selectedMenuText, $fallback--text) inset;
|
||||
position: relative;
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
border-radius: $fallback--btnRadius;
|
||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||
}
|
||||
font-size: 1.2em;
|
||||
margin-top: 0.05em;
|
||||
|
||||
.svg-inline--fa {
|
||||
font-weight: bolder;
|
||||
color: $fallback--text;
|
||||
color: var(--selectedMenuText, $fallback--text);
|
||||
--lightText: var(--selectedMenuLightText, $fallback--lightText);
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
<img
|
||||
v-if="!hideSiteFavicon"
|
||||
class="favicon"
|
||||
src="/favicon.svg"
|
||||
/>
|
||||
src="/favicon.png"
|
||||
>
|
||||
<span
|
||||
v-if="!hideSiteName"
|
||||
class="site-name"
|
||||
|
@ -44,7 +44,6 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="publicTimelineVisible"
|
||||
:to="{ name: 'public-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -56,7 +55,7 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="bubbleTimelineVisible"
|
||||
v-if="currentUser && showBubbleTimeline"
|
||||
:to="{ name: 'bubble-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -68,7 +67,6 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="federatedTimelineVisible"
|
||||
:to="{ name: 'public-external-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -93,7 +91,7 @@
|
|||
<img
|
||||
:src="logo"
|
||||
:style="logoStyle"
|
||||
/>
|
||||
>
|
||||
</router-link>
|
||||
<div class="item right actions">
|
||||
<search-bar
|
||||
|
@ -108,10 +106,7 @@
|
|||
<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
|
||||
|
@ -157,10 +152,7 @@
|
|||
/>
|
||||
</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, 0.5);
|
||||
background: rgba(27,31,35,.5);
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@
|
|||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: 0.5em 0.5em;
|
||||
padding: .5em .5em;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
|
@ -84,8 +84,9 @@
|
|||
|
||||
button {
|
||||
width: auto;
|
||||
margin-left: 0.5rem;
|
||||
margin-left: .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 #progress>
|
||||
<template v-slot:progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
@ -19,7 +19,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template #progress>
|
||||
<template v-slot: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,16 +36,14 @@ 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,
|
||||
|
@ -57,8 +55,7 @@ const EditStatusModal = {
|
|||
contentType
|
||||
}
|
||||
|
||||
return statusPosterService
|
||||
.editStatus(params)
|
||||
return statusPosterService.editStatus(params)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
|
@ -69,7 +66,7 @@ const EditStatusModal = {
|
|||
}
|
||||
})
|
||||
},
|
||||
closeModal() {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeEditStatusModal')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
:disable-polls="true"
|
||||
:disable-visibility-selector="true"
|
||||
:post-handler="doEditStatus"
|
||||
@posted="closeModal"
|
||||
:disablePolls="true"
|
||||
:disableVisibilitySelector="true"
|
||||
:post-handler="doEditStatus"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
const EMOJI_SIZE = 32 + 8
|
||||
const GROUP_TITLE_HEIGHT = 24
|
||||
const BUFFER_SIZE = 3 * EMOJI_SIZE
|
||||
|
||||
const EmojiGrid = {
|
||||
props: {
|
||||
groups: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containerWidth: 0,
|
||||
containerHeight: 0,
|
||||
scrollPos: 0,
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const rect = this.$refs.container.getBoundingClientRect()
|
||||
this.containerWidth = rect.width
|
||||
this.containerHeight = rect.height
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
this.containerWidth = entry.contentRect.width
|
||||
this.containerHeight = entry.contentRect.height
|
||||
}
|
||||
})
|
||||
this.resizeObserver.observe(this.$refs.container)
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.resizeObserver.disconnect()
|
||||
this.resizeObserver = null
|
||||
},
|
||||
watch: {
|
||||
groups() {
|
||||
// Scroll to top when grid content changes
|
||||
if (this.$refs.container) {
|
||||
this.$refs.container.scrollTo(0, 0)
|
||||
}
|
||||
},
|
||||
activeGroup(group) {
|
||||
this.$emit('activeGroup', group)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onScroll() {
|
||||
this.scrollPos = this.$refs.container.scrollTop
|
||||
},
|
||||
onEmoji(emoji) {
|
||||
this.$emit('emoji', emoji)
|
||||
},
|
||||
scrollToItem(itemId) {
|
||||
const container = this.$refs.container
|
||||
if (!container) return
|
||||
|
||||
for (const item of this.itemList) {
|
||||
if (item.id === itemId) {
|
||||
container.scrollTo(0, item.position.y)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// Total height of scroller content
|
||||
gridHeight() {
|
||||
if (this.itemList.length === 0) return 0
|
||||
const lastItem = this.itemList[this.itemList.length - 1]
|
||||
return (
|
||||
lastItem.position.y +
|
||||
('title' in lastItem ? GROUP_TITLE_HEIGHT : EMOJI_SIZE)
|
||||
)
|
||||
},
|
||||
activeGroup() {
|
||||
const items = this.itemList
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
const item = items[i]
|
||||
if ('title' in item && item.position.y <= this.scrollPos) {
|
||||
return item.id
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
itemList() {
|
||||
const items = []
|
||||
let x = 0
|
||||
let y = 0
|
||||
for (const group of this.groups) {
|
||||
items.push({ position: { x, y }, id: group.id, title: group.text })
|
||||
if (group.text.length) {
|
||||
y += GROUP_TITLE_HEIGHT
|
||||
}
|
||||
for (const emoji of group.emojis) {
|
||||
items.push({
|
||||
position: { x, y },
|
||||
id: `${group.id}-${emoji.displayText}`,
|
||||
emoji
|
||||
})
|
||||
x += EMOJI_SIZE
|
||||
if (x + EMOJI_SIZE > this.containerWidth) {
|
||||
y += EMOJI_SIZE
|
||||
x = 0
|
||||
}
|
||||
}
|
||||
if (x > 0) {
|
||||
y += EMOJI_SIZE
|
||||
x = 0
|
||||
}
|
||||
}
|
||||
return items
|
||||
},
|
||||
visibleItems() {
|
||||
const startPos = this.scrollPos - BUFFER_SIZE
|
||||
const endPos = this.scrollPos + this.containerHeight + BUFFER_SIZE
|
||||
return this.itemList.filter((i) => {
|
||||
return i.position.y >= startPos && i.position.y < endPos
|
||||
})
|
||||
},
|
||||
scrolledClass() {
|
||||
if (this.scrollPos <= 5) {
|
||||
return 'scrolled-top'
|
||||
} else if (this.scrollPos >= this.gridHeight - this.containerHeight - 5) {
|
||||
return 'scrolled-bottom'
|
||||
} else {
|
||||
return 'scrolled-middle'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EmojiGrid
|
|
@ -1,60 +0,0 @@
|
|||
.emoji {
|
||||
&-grid {
|
||||
flex: 1 1 1px;
|
||||
position: relative;
|
||||
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);
|
||||
transition: mask-size 150ms;
|
||||
mask-size: 100% 20px, 100% 20px, auto;
|
||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
&.scrolled {
|
||||
&-top {
|
||||
mask-size: 100% 20px, 100% 0, auto;
|
||||
}
|
||||
&-bottom {
|
||||
mask-size: 100% 0, 100% 20px, auto;
|
||||
}
|
||||
}
|
||||
margin-left: 5px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
&-group-title {
|
||||
position: absolute;
|
||||
font-size: 0.85em;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
|
||||
&.disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
font-size: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="emoji-grid"
|
||||
:class="scrolledClass"
|
||||
@scroll.passive="onScroll"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
height: `${gridHeight}px`
|
||||
}"
|
||||
>
|
||||
<template v-for="item in visibleItems">
|
||||
<h6
|
||||
v-if="'title' in item && item.title.length"
|
||||
:key="'title-' + item.id"
|
||||
class="emoji-group-title"
|
||||
:style="{
|
||||
top: item.position.y + 'px',
|
||||
left: item.position.x + 'px'
|
||||
}"
|
||||
>
|
||||
{{ item.title }}
|
||||
</h6>
|
||||
<span
|
||||
v-else-if="'emoji' in item"
|
||||
:key="'emoji-' + item.id"
|
||||
class="emoji-item"
|
||||
:title="item.emoji.displayText"
|
||||
:style="{
|
||||
top: item.position.y + 'px',
|
||||
left: item.position.x + 'px'
|
||||
}"
|
||||
@click.stop.prevent="onEmoji(item.emoji)"
|
||||
>
|
||||
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
|
||||
<img
|
||||
v-else
|
||||
:src="item.emoji.imageUrl"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./emoji_grid.js"></script>
|
||||
<style lang="scss" src="./emoji_grid.scss"></style>
|
|
@ -4,9 +4,13 @@ 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
|
||||
|
@ -101,7 +105,7 @@ const EmojiInput = {
|
|||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
input: undefined,
|
||||
highlighted: 0,
|
||||
|
@ -119,34 +123,29 @@ 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()
|
||||
|
@ -159,7 +158,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)
|
||||
|
@ -184,29 +183,29 @@ 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(() => {
|
||||
this.scrollIntoView()
|
||||
this.focusPickerInput()
|
||||
|
@ -219,24 +218,21 @@ const EmojiInput = {
|
|||
this.disableClickOutside = false
|
||||
}, 0)
|
||||
},
|
||||
togglePicker() {
|
||||
togglePicker () {
|
||||
this.input.focus()
|
||||
this.showPicker = !this.showPicker
|
||||
if (this.showPicker) {
|
||||
this.scrollIntoView()
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
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) || ''
|
||||
|
||||
|
@ -255,25 +251,19 @@ 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()
|
||||
}
|
||||
|
@ -285,17 +275,12 @@ 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
|
||||
|
@ -310,7 +295,7 @@ const EmojiInput = {
|
|||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
cycleBackward(e) {
|
||||
cycleBackward (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 1) {
|
||||
this.highlighted -= 1
|
||||
|
@ -322,7 +307,7 @@ const EmojiInput = {
|
|||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
cycleForward(e) {
|
||||
cycleForward (e) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (len > 1) {
|
||||
this.highlighted += 1
|
||||
|
@ -334,28 +319,26 @@ 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
|
||||
|
@ -377,10 +360,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(() => {
|
||||
|
@ -389,10 +372,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
|
||||
|
@ -406,7 +389,7 @@ const EmojiInput = {
|
|||
this.resize()
|
||||
this.temporarilyHideSuggestions = false
|
||||
},
|
||||
onKeyUp(e) {
|
||||
onKeyUp (e) {
|
||||
const { key } = e
|
||||
this.setCaret(e)
|
||||
this.resize()
|
||||
|
@ -419,11 +402,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 })
|
||||
|
@ -470,31 +453,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
|
||||
|
@ -505,12 +488,9 @@ 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 {
|
||||
|
@ -518,7 +498,7 @@ const EmojiInput = {
|
|||
target.style.bottom = this.input.offsetHeight + 'px'
|
||||
}
|
||||
},
|
||||
overflowsBottom(el) {
|
||||
overflowsBottom (el) {
|
||||
return el.getBoundingClientRect().bottom > window.innerHeight
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
<EmojiPicker
|
||||
v-if="enableEmojiPicker"
|
||||
ref="picker"
|
||||
show-keep-open
|
||||
:class="{ hide: !showPicker }"
|
||||
:enable-sticker-picker="enableStickerPicker"
|
||||
class="emoji-picker-panel"
|
||||
|
@ -43,14 +42,11 @@
|
|||
: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">
|
||||
|
@ -81,7 +77,7 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0.2em 0.25em;
|
||||
margin: .2em .25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
|
@ -97,7 +93,7 @@
|
|||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +104,7 @@
|
|||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
display: none
|
||||
}
|
||||
|
||||
&-body {
|
||||
|
@ -182,8 +178,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
input, textarea {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,5 @@
|
|||
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
|
||||
|
@ -34,10 +13,10 @@ const MFM_TAGS = [
|
|||
* 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)
|
||||
|
@ -46,15 +25,14 @@ 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))
|
||||
|
@ -107,7 +85,7 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
})
|
||||
}
|
||||
|
||||
return async (input) => {
|
||||
return async input => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
if (previousQuery === noPrefix) return suggestions
|
||||
|
||||
|
@ -121,47 +99,36 @@ 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 || []
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faBoxOpen,
|
||||
|
@ -9,7 +8,18 @@ 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
|
||||
const LOAD_EMOJI_BY = 60
|
||||
|
||||
// When to start loading new batch emoji, in pixels
|
||||
const LOAD_EMOJI_MARGIN = 64
|
||||
|
||||
const EmojiPicker = {
|
||||
props: {
|
||||
|
@ -17,88 +27,147 @@ const EmojiPicker = {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showKeepOpen: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
keyword: '',
|
||||
activeGroup: 'standard',
|
||||
showingStickers: false,
|
||||
keepOpen: false
|
||||
groupsScrolledClass: 'scrolled-top',
|
||||
keepOpen: false,
|
||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
||||
customEmojiTimeout: null,
|
||||
customEmojiLoadAllConfirmed: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(() =>
|
||||
import('../sticker_picker/sticker_picker.vue')
|
||||
),
|
||||
Checkbox,
|
||||
EmojiGrid
|
||||
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 })
|
||||
this.$store.commit('emojiUsed', emoji)
|
||||
},
|
||||
onWheel(e) {
|
||||
e.preventDefault()
|
||||
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
||||
onScroll (e) {
|
||||
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||
this.updateScrolledClass(target)
|
||||
this.scrolledGroup(target)
|
||||
this.triggerLoadMore(target)
|
||||
},
|
||||
highlight(key) {
|
||||
highlight (key) {
|
||||
this.setShowStickers(false)
|
||||
this.activeGroup = key
|
||||
if (this.keyword.length) {
|
||||
this.$refs.emojiGrid.scrollToItem(key)
|
||||
},
|
||||
updateScrolledClass (target) {
|
||||
if (target.scrollTop <= 5) {
|
||||
this.groupsScrolledClass = 'scrolled-top'
|
||||
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
||||
this.groupsScrolledClass = 'scrolled-bottom'
|
||||
} else {
|
||||
this.groupsScrolledClass = 'scrolled-middle'
|
||||
}
|
||||
},
|
||||
onActiveGroup(group) {
|
||||
this.activeGroup = group
|
||||
triggerLoadMore (target) {
|
||||
const ref = this.$refs['group-end-custom']
|
||||
if (!ref) return
|
||||
const bottom = ref.offsetTop + ref.offsetHeight
|
||||
|
||||
const scrollerBottom = target.scrollTop + target.clientHeight
|
||||
const scrollerTop = target.scrollTop
|
||||
const scrollerMax = target.scrollHeight
|
||||
|
||||
// Loads more emoji when they come into view
|
||||
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
|
||||
// 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
|
||||
if (!bottomAboveViewport && (approachingBottom || atTop)) {
|
||||
this.loadEmoji()
|
||||
}
|
||||
},
|
||||
toggleStickers() {
|
||||
scrolledGroup (target) {
|
||||
const top = target.scrollTop + 5
|
||||
this.$nextTick(() => {
|
||||
this.emojisView.forEach(group => {
|
||||
const ref = this.$refs['group-' + group.id]
|
||||
if (ref.offsetTop <= top) {
|
||||
this.activeGroup = group.id
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
loadEmoji () {
|
||||
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
|
||||
|
||||
if (allLoaded) {
|
||||
return
|
||||
}
|
||||
|
||||
this.customEmojiBufferSlice += LOAD_EMOJI_BY
|
||||
},
|
||||
startEmojiLoad (forceUpdate = false) {
|
||||
if (!forceUpdate) {
|
||||
this.keyword = ''
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs['emoji-groups'].scrollTop = 0
|
||||
})
|
||||
const bufferSize = this.customEmojiBuffer.length
|
||||
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
|
||||
if (bufferPrefilledAll && !forceUpdate) {
|
||||
return
|
||||
}
|
||||
this.customEmojiBufferSlice = LOAD_EMOJI_BY
|
||||
},
|
||||
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)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
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 || []
|
||||
)
|
||||
},
|
||||
emojis() {
|
||||
const recentEmojis = this.$store.getters.recentEmojis
|
||||
customEmojiBuffer () {
|
||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
||||
},
|
||||
emojis () {
|
||||
const standardEmojis = this.$store.state.instance.emoji || []
|
||||
const customEmojis = this.sortedEmoji
|
||||
const emojiPacks = []
|
||||
|
@ -111,15 +180,6 @@ const EmojiPicker = {
|
|||
})
|
||||
})
|
||||
return [
|
||||
{
|
||||
id: 'recent',
|
||||
text: this.$t('emoji.recent'),
|
||||
first: {
|
||||
imageUrl: '',
|
||||
replacement: '🕒'
|
||||
},
|
||||
emojis: this.filterByKeyword(recentEmojis)
|
||||
},
|
||||
{
|
||||
id: 'standard',
|
||||
text: this.$t('emoji.unicode'),
|
||||
|
@ -131,7 +191,7 @@ const EmojiPicker = {
|
|||
}
|
||||
].concat(emojiPacks)
|
||||
},
|
||||
sortedEmoji() {
|
||||
sortedEmoji () {
|
||||
const customEmojis = this.$store.state.instance.customEmoji || []
|
||||
const sortedEmojiGroups = new Map()
|
||||
customEmojis.forEach((emoji) => {
|
||||
|
@ -143,22 +203,19 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,5 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
// The worst query selector ever
|
||||
// selects ONLY emojis pickers in replies in notifications
|
||||
// who thought this was a good idea?
|
||||
.notification
|
||||
> .Status
|
||||
> .status-container
|
||||
> .post-status-form
|
||||
> form
|
||||
> .form-group
|
||||
> .emoji-input
|
||||
> .emoji-picker {
|
||||
max-width: 100%;
|
||||
left: 0;
|
||||
@media (min-width: 1300px) {
|
||||
left: -30px;
|
||||
}
|
||||
}
|
||||
|
||||
.Notification {
|
||||
.emoji-picker {
|
||||
min-width: 160%;
|
||||
|
@ -25,7 +7,7 @@
|
|||
overflow: hidden;
|
||||
left: -70%;
|
||||
max-width: 100%;
|
||||
@media (min-width: 800px) and (max-width: 1280px) {
|
||||
@media (min-width: 800px) and (max-width: 1300px) {
|
||||
left: -50%;
|
||||
min-width: 50%;
|
||||
max-width: 130%;
|
||||
|
@ -36,10 +18,6 @@
|
|||
min-width: 50%;
|
||||
max-width: 130%;
|
||||
}
|
||||
|
||||
.Status > .emoji-picker {
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
.emoji-picker {
|
||||
|
@ -92,6 +70,10 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.emoji-groups {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.additional-tabs {
|
||||
border-left: 1px solid;
|
||||
border-left-color: $fallback--icon;
|
||||
|
@ -118,7 +100,7 @@
|
|||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0.4em;
|
||||
padding: .4em;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
|
@ -151,7 +133,7 @@
|
|||
}
|
||||
|
||||
.sticker-picker {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 auto
|
||||
}
|
||||
|
||||
.stickers,
|
||||
|
@ -170,12 +152,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.emoji-search {
|
||||
padding: 5px;
|
||||
flex: 0 0 auto;
|
||||
.emoji {
|
||||
&-search {
|
||||
padding: 5px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-groups {
|
||||
|
@ -184,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
|
||||
|
@ -237,5 +221,7 @@
|
|||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
<template>
|
||||
<div class="emoji-picker panel panel-default panel-body">
|
||||
<div class="heading">
|
||||
<span
|
||||
ref="emoji-tabs"
|
||||
class="emoji-tabs"
|
||||
@wheel="onWheel"
|
||||
>
|
||||
<span class="emoji-tabs">
|
||||
<span
|
||||
v-for="group in emojis"
|
||||
:key="group.id"
|
||||
|
@ -17,18 +13,16 @@
|
|||
: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"
|
||||
>
|
||||
|
@ -42,7 +36,7 @@
|
|||
<div class="content">
|
||||
<div
|
||||
class="emoji-content"
|
||||
:class="{ hidden: showingStickers }"
|
||||
:class="{hidden: showingStickers}"
|
||||
>
|
||||
<div class="emoji-search">
|
||||
<input
|
||||
|
@ -51,17 +45,13 @@
|
|||
class="form-control"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
@input="$event.target.composing = false"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<EmojiGrid
|
||||
ref="emojiGrid"
|
||||
:groups="emojisView"
|
||||
@emoji="onEmoji"
|
||||
@active-group="onActiveGroup"
|
||||
/>
|
||||
<div
|
||||
v-if="showKeepOpen"
|
||||
class="keep-open"
|
||||
ref="emoji-groups"
|
||||
class="emoji-groups"
|
||||
:class="groupsScrolledClass"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<div
|
||||
v-for="group in emojisView"
|
||||
|
@ -85,7 +75,7 @@
|
|||
<img
|
||||
v-else
|
||||
:src="emoji.imageUrl"
|
||||
/>
|
||||
>
|
||||
</span>
|
||||
<span :ref="'group-end-' + group.id" />
|
||||
</div>
|
||||
|
|
|
@ -3,11 +3,6 @@ import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
|||
|
||||
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||
|
||||
const findEmojiByReplacement = (state, replacement) => {
|
||||
const allEmojis = state.instance.emoji.concat(state.instance.customEmoji)
|
||||
return allEmojis.find((emoji) => emoji.replacement === replacement)
|
||||
}
|
||||
|
||||
const EmojiReactions = {
|
||||
name: 'EmojiReactions',
|
||||
components: {
|
||||
|
@ -19,20 +14,18 @@ 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 || []
|
||||
|
@ -42,32 +35,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 })
|
||||
const emojiObject = findEmojiByReplacement(this.$store.state, emoji)
|
||||
this.$store.commit('emojiUsed', emojiObject)
|
||||
},
|
||||
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,35 +1,28 @@
|
|||
<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
|
||||
class="emoji-button-inner"
|
||||
>
|
||||
<span v-else>
|
||||
<span class="reaction-emoji unicode-emoji">
|
||||
{{ reaction.name }}
|
||||
</span>
|
||||
|
@ -48,7 +41,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./emoji_reactions.js"></script>
|
||||
<script src="./emoji_reactions.js" ></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
|
@ -56,11 +49,10 @@
|
|||
display: flex;
|
||||
margin-top: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.unicode-emoji {
|
||||
font-size: 128%;
|
||||
font-size: 210%;
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
|
@ -68,20 +60,13 @@
|
|||
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: auto;
|
||||
max-width: 96cqw;
|
||||
height: 2.55em !important;
|
||||
width: 2.55em !important;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
img.reaction-emoji {
|
||||
width: 1.55em !important;
|
||||
display: block;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
@ -108,12 +93,9 @@
|
|||
}
|
||||
|
||||
.button-default.picked-reaction {
|
||||
background: none;
|
||||
padding: 1px 0.5em;
|
||||
|
||||
.emoji-button-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faCircleNotch)
|
||||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
|
||||
const Exporter = {
|
||||
props: {
|
||||
|
@ -16,30 +18,26 @@ 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
faBookmark as faBookmarkReg,
|
||||
faFlag
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
library.add(
|
||||
faEllipsisH,
|
||||
|
@ -36,7 +35,7 @@ const ExtraButtons = {
|
|||
Popover,
|
||||
ConfirmModal
|
||||
},
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
expanded: false,
|
||||
showingDeleteDialog: false,
|
||||
|
@ -44,206 +43,154 @@ 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,
|
||||
statusLanguage: this.status.language,
|
||||
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 #content="{ close }">
|
||||
<template v-slot: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 #trigger>
|
||||
<template v-slot: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,19 +1,24 @@
|
|||
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 {
|
||||
|
@ -27,10 +32,8 @@ 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,7 +56,6 @@
|
|||
.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,20 +2,10 @@ 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,20 +16,17 @@
|
|||
</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,11 +5,14 @@ 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,
|
||||
|
@ -17,7 +20,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) => {
|
||||
|
@ -29,20 +32,17 @@ 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,27 +1,24 @@
|
|||
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) {
|
||||
|
@ -30,7 +27,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) {
|
||||
|
@ -41,44 +38,39 @@ 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,7 +21,9 @@
|
|||
tag="span"
|
||||
>
|
||||
<template #user>
|
||||
<span v-text="user.screen_name_ui" />
|
||||
<span
|
||||
v-text="user.screen_name_ui"
|
||||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
|
|
|
@ -4,7 +4,10 @@ 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,
|
||||
|
@ -12,13 +15,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,90 +8,78 @@ 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)
|
||||
this.$store.dispatch('decrementFollowRequestsCount')
|
||||
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
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('decrementFollowRequestsCount')
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
})
|
||||
this.hideDenyConfirmDialog()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mergedConfig() {
|
||||
mergedConfig () {
|
||||
return this.$store.getters.mergedConfig
|
||||
},
|
||||
shouldConfirmApprove() {
|
||||
shouldConfirmApprove () {
|
||||
return this.mergedConfig.modalOnApproveFollow
|
||||
},
|
||||
shouldConfirmDeny() {
|
||||
shouldConfirmDeny () {
|
||||
return this.mergedConfig.modalOnDenyFollow
|
||||
},
|
||||
show() {
|
||||
const notifId = this.$store.state.api.followRequests.find(
|
||||
(req) => req.id === this.user.id
|
||||
)
|
||||
|
||||
return notifId !== undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<basic-user-card
|
||||
v-if="show"
|
||||
:user="user"
|
||||
>
|
||||
<basic-user-card :user="user">
|
||||
<div class="follow-request-card-content-container">
|
||||
<button
|
||||
class="btn button-default"
|
||||
|
|
|
@ -1,29 +1,11 @@
|
|||
import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
|
||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||
import List from '../list/list.vue'
|
||||
import get from 'lodash/get'
|
||||
|
||||
const FollowRequestList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchFollowRequests'),
|
||||
select: (props, $store) =>
|
||||
get($store.state.api, 'followRequests', []).map((req) =>
|
||||
$store.getters.findUser(req.id)
|
||||
),
|
||||
destroy: (props, $store) => $store.dispatch('clearFollowRequests'),
|
||||
childPropName: 'items',
|
||||
additionalPropNames: ['userId']
|
||||
})(List)
|
||||
|
||||
const FollowRequests = {
|
||||
components: {
|
||||
FollowRequestCard,
|
||||
FollowRequestList
|
||||
FollowRequestCard
|
||||
},
|
||||
computed: {
|
||||
userId() {
|
||||
return this.$store.state.users.currentUser.id
|
||||
},
|
||||
requests() {
|
||||
requests () {
|
||||
return this.$store.state.api.followRequests
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FollowRequestList :user-id="userId">
|
||||
<template #item="{ item }">
|
||||
<FollowRequestCard :user="item" />
|
||||
</template>
|
||||
</FollowRequestList>
|
||||
<FollowRequestCard
|
||||
v-for="request in requests"
|
||||
:key="request.id"
|
||||
:user="request"
|
||||
class="list-item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
<template>
|
||||
<div class="followed-tag-card">
|
||||
<span>
|
||||
<router-link :to="{ name: 'tag-timeline', params: { tag: tag.name } }">
|
||||
<span class="tag-link">#{{ tag.name }}</span>
|
||||
</router-link>
|
||||
<span class="unfollow-tag">
|
||||
<button
|
||||
v-if="isFollowing"
|
||||
class="button-default unfollow-tag-button"
|
||||
:title="$t('user_card.unfollow_tag')"
|
||||
@click="unfollowTag(tag.name)"
|
||||
>
|
||||
{{ $t('user_card.unfollow_tag') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="button-default follow-tag-button"
|
||||
:title="$t('user_card.follow_tag')"
|
||||
@click="followTag(tag.name)"
|
||||
>
|
||||
{{ $t('user_card.follow_tag') }}
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FollowedTagCard',
|
||||
props: {
|
||||
tag: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
// this is a hack to update the state of the button
|
||||
// for some reason, List does not update on changes to the tag object
|
||||
data: () => ({
|
||||
isFollowing: true
|
||||
}),
|
||||
mounted() {
|
||||
this.isFollowing = this.tag.following
|
||||
},
|
||||
methods: {
|
||||
unfollowTag(tag) {
|
||||
this.$store.dispatch('unfollowTag', tag)
|
||||
this.isFollowing = false
|
||||
},
|
||||
followTag(tag) {
|
||||
this.$store.dispatch('followTag', tag)
|
||||
this.isFollowing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.followed-tag-card {
|
||||
margin-left: 1rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.unfollow-tag {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.tag-link {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.unfollow-tag-button,
|
||||
.follow-tag-button {
|
||||
font-size: medium;
|
||||
}
|
||||
</style>
|
|
@ -5,9 +5,11 @@ 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: [
|
||||
|
@ -17,45 +19,43 @@ 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,13 +15,8 @@
|
|||
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"
|
||||
|
@ -48,11 +43,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';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue