forked from AkkomaGang/akkoma-fe
Compare commits
173 commits
Author | SHA1 | Date | |
---|---|---|---|
13f92fa2b1 | |||
8e880c349e | |||
3ca4c32b03 | |||
d25dd1cbd4 | |||
42ffce97d6 | |||
2f479c670f | |||
|
ee6e7026ab | ||
17c05a5ca2 | |||
|
42896c2abf | ||
ecb6be2152 | |||
|
6c92983af6 | ||
9e4985e225 | |||
|
60ff715aff | ||
|
04bcf7d804 | ||
|
5fa305c58c | ||
|
a2ceb89d5e | ||
6b3b55455d | |||
8c6ccc321d | |||
596ae7e377 | |||
0d22a22a10 | |||
2a76be56e7 | |||
661a98d38d | |||
94d640f9f1 | |||
|
1f943ce8a5 | ||
c540764408 | |||
|
a4dfdc0853 | ||
ddea499a36 | |||
|
db33fe8ee2 | ||
|
f1bf22436d | ||
|
459c73ec02 | ||
|
2acf1e5c59 | ||
|
33c4459744 | ||
|
b00487e51f | ||
|
1e1cab643c | ||
|
8d3219a6d2 | ||
|
ec9753758f | ||
|
97ff4a7241 | ||
14cedc5ed1 | |||
5911777aa2 | |||
47fc082fb9 | |||
7e1b1e79f4 | |||
b92b2f74a4 | |||
7361f4e77e | |||
9f7f9e2798 | |||
42ab3eada4 | |||
6fdef479d0 | |||
fe08691f05 | |||
6a9764951f | |||
0f33b1cd79 | |||
999c38594e | |||
626c880038 | |||
6d7761c7e5 | |||
996ce3dde3 | |||
|
2c007f06e3 | ||
|
00704bd88c | ||
3cee6c5934 | |||
5476a2794d | |||
d8fa8c4ee4 | |||
6a9d169e24 | |||
581c53a15e | |||
9e04e4fd80 | |||
88d5149db5 | |||
b4b13d777f | |||
7f4dd9ff03 | |||
a9a95e9120 | |||
56fd2e773b | |||
42dc1a027a | |||
236bc2c762 | |||
|
e9f47509ae | ||
f288d0c219 | |||
d973396c96 | |||
62287fffae | |||
e9f16af82d | |||
dfba8be134 | |||
313ddcebcb | |||
236b19e854 | |||
ea941d7cfa | |||
2e5001e5de | |||
014f8b0dd2 | |||
dd403b295f | |||
|
9cd62fe08d | ||
f668455dff | |||
5a4315384e | |||
401dfa8fa6 | |||
bb243168b3 | |||
da491f3278 | |||
d00e28d5e9 | |||
7ff17ab722 | |||
b009428814 | |||
7bec96a1bf | |||
0b5793c1e0 | |||
72ef2e7454 | |||
c39332c1bf | |||
8c6cf86de3 | |||
909271c764 | |||
fb317f2907 | |||
153c4d251f | |||
1d01475f7a | |||
a91e8d282d | |||
413acbc7dd | |||
1312b07e2e | |||
427e63cfc3 | |||
|
6e1ba218df | ||
|
830e8fdb45 | ||
9bf310d509 | |||
e3e8b19df3 | |||
9b75ca414f | |||
b07cf33a04 | |||
142f90c4cf | |||
83c6f7f9f9 | |||
65adfb01c3 | |||
65511042e3 | |||
235f3b2d94 | |||
2382696698 | |||
ae2d72131b | |||
98d38e3b73 | |||
47c05363f8 | |||
87d9c1ae15 | |||
5ad0da1766 | |||
97e9b2597a | |||
94bbf8f0a3 | |||
ce9d316a51 | |||
6ce12fc153 | |||
|
e86c7abb39 | ||
8a0da8861d | |||
|
6c7e691aea | ||
6a2cdcfc15 | |||
d7688fafd3 | |||
3d3425eda9 | |||
|
b33d15a739 | ||
40e86998e6 | |||
|
177f344033 | ||
9079ac4afa | |||
|
dfc4e0a026 | ||
|
3d732d1d28 | ||
|
e8ee31afed | ||
|
d9d6b1e80b | ||
|
1dd7a89544 | ||
d3280c4ab3 | |||
abc75c360b | |||
a8e119b0f1 | |||
17e574b173 | |||
71d2e0b0ce | |||
b68e968bf9 | |||
eb49295422 | |||
337a30fe01 | |||
105ecd3836 | |||
a3e490edcd | |||
f8f5e1c89b | |||
e132814478 | |||
6af1df8bef | |||
b86f12cede | |||
|
c669701762 | ||
0900a9d87b | |||
0a01a2bdf0 | |||
7860c885c4 | |||
1c3bd60af2 | |||
|
b8faee5d6d | ||
c01c62f149 | |||
105b934f90 | |||
b1f41add0e | |||
e4e8ed812b | |||
684894aee3 | |||
f8a796b234 | |||
70ea9e772c | |||
efe0f53736 | |||
fcbbbad8d4 | |||
39b6b0b49f | |||
867a86d887 | |||
7538369fa1 | |||
|
2d4b2f2e20 | ||
862c93706c | |||
|
e06348ee33 |
456 changed files with 15718 additions and 11648 deletions
6
.babelrc
6
.babelrc
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-runtime",
|
||||
"lodash",
|
||||
"@vue/babel-plugin-jsx"
|
||||
],
|
||||
"comments": false
|
||||
}
|
||||
|
|
|
@ -5,14 +5,9 @@ module.exports = {
|
|||
sourceType: 'module'
|
||||
},
|
||||
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
|
||||
extends: [
|
||||
'plugin:vue/recommended'
|
||||
],
|
||||
extends: ['plugin:vue/recommended', 'plugin:prettier/recommended'],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue',
|
||||
'import'
|
||||
],
|
||||
plugins: ['vue', 'import'],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// allow paren-less arrow functions
|
||||
|
|
49
.gitea/issue_template/bug.yml
Normal file
49
.gitea/issue_template/bug.yml
Normal file
|
@ -0,0 +1,49 @@
|
|||
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."
|
29
.gitea/issue_template/feat.yml
Normal file
29
.gitea/issue_template/feat.yml
Normal file
|
@ -0,0 +1,29 @@
|
|||
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,3 +9,4 @@ selenium-debug.log
|
|||
config/local.json
|
||||
config/local.*.json
|
||||
docs/site/
|
||||
.vscode/
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"singleAttributePerLine": true
|
||||
}
|
|
@ -1,19 +1,13 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-rscss/config",
|
||||
"stylelint-config-recommended-vue/scss",
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-standard"
|
||||
],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"declaration-no-important": true,
|
||||
"rscss/no-descendant-combinator": false,
|
||||
"rscss/class-format": [
|
||||
true,
|
||||
{
|
||||
"component": "pascal-case",
|
||||
"variant": "^-[a-z]\\w+",
|
||||
"element": "^[a-z]\\w+"
|
||||
}
|
||||
]
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ pipeline:
|
|||
commands:
|
||||
- yarn
|
||||
- yarn lint
|
||||
- yarn stylelint
|
||||
#- yarn stylelint
|
||||
|
||||
test:
|
||||
when:
|
||||
|
|
10
README.md
10
README.md
|
@ -1,22 +1,22 @@
|
|||
# Pleroma-FE
|
||||
# Akkoma-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 Pleroma-FE from the Pleroma project, with support for new Akkoma features such as:
|
||||
This is a fork of Akkoma-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 Pleroma-FE.
|
||||
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
# FOR ADMINS
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Build Setup
|
||||
|
||||
|
|
|
@ -12,13 +12,16 @@ 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'
|
||||
" Opening index.html over file:// won't work.\n"
|
||||
)
|
||||
|
||||
var spinner = ora('building for production...')
|
||||
spinner.start()
|
||||
|
||||
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
|
||||
var assetsPath = path.join(
|
||||
config.build.assetsRoot,
|
||||
config.build.assetsSubDirectory
|
||||
)
|
||||
rm('-rf', assetsPath)
|
||||
mkdir('-p', assetsPath)
|
||||
cp('-R', 'static/*', assetsPath)
|
||||
|
@ -26,11 +29,13 @@ cp('-R', 'static/*', assetsPath)
|
|||
webpack(webpackConfig, function (err, stats) {
|
||||
spinner.stop()
|
||||
if (err) throw err
|
||||
process.stdout.write(stats.toString({
|
||||
process.stdout.write(
|
||||
stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n')
|
||||
}) + '\n'
|
||||
)
|
||||
})
|
||||
|
|
|
@ -2,8 +2,7 @@ var semver = require('semver')
|
|||
var chalk = require('chalk')
|
||||
var packageConfig = require('../package.json')
|
||||
var exec = function (cmd) {
|
||||
return require('child_process')
|
||||
.execSync(cmd).toString().trim()
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
var versionRequirements = [
|
||||
|
@ -24,8 +23,11 @@ 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 ' +
|
||||
warnings.push(
|
||||
mod.name +
|
||||
': ' +
|
||||
chalk.red(mod.currentVersion) +
|
||||
' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
|
@ -33,7 +35,11 @@ module.exports = function () {
|
|||
|
||||
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,7 +6,8 @@ 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'
|
||||
var webpackConfig =
|
||||
process.env.NODE_ENV === 'testing'
|
||||
? require('./webpack.prod.conf')
|
||||
: require('./webpack.dev.conf')
|
||||
|
||||
|
@ -50,7 +51,10 @@ app.use(devMiddleware)
|
|||
app.use(hotMiddleware)
|
||||
|
||||
// serve pure static assets
|
||||
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
var staticPath = path.posix.join(
|
||||
config.dev.assetsPublicPath,
|
||||
config.dev.assetsSubDirectory
|
||||
)
|
||||
app.use(staticPath, express.static('./static'))
|
||||
|
||||
module.exports = app.listen(port, function (err) {
|
||||
|
|
|
@ -4,7 +4,8 @@ var sass = require('sass')
|
|||
var MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
var assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
var assetsSubDirectory =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
|
@ -27,11 +28,11 @@ exports.cssLoaders = function (options) {
|
|||
return [
|
||||
{
|
||||
test: /\.(post)?css$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader']),
|
||||
use: generateLoaders(['css-loader', 'postcss-loader'])
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']),
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader'])
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
|
@ -52,8 +53,8 @@ exports.cssLoaders = function (options) {
|
|||
},
|
||||
{
|
||||
test: /\.styl(us)?$/,
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader']),
|
||||
},
|
||||
use: generateLoaders(['css-loader', 'postcss-loader', 'stylus-loader'])
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,13 @@ 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()
|
||||
|
@ -19,9 +18,12 @@ module.exports = {
|
|||
app: './src/main.js'
|
||||
},
|
||||
output: {
|
||||
hashFunction: "sha256", // Workaround for builds with OpenSSL 3.
|
||||
hashFunction: 'sha256', // Workaround for builds with OpenSSL 3.
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
publicPath:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsPublicPath
|
||||
: config.dev.assetsPublicPath,
|
||||
filename: '[name].js'
|
||||
},
|
||||
optimization: {
|
||||
|
@ -31,17 +33,15 @@ module.exports = {
|
|||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.vue', '.mjs'],
|
||||
modules: [
|
||||
path.join(__dirname, '../node_modules')
|
||||
],
|
||||
modules: [path.join(__dirname, '../node_modules')],
|
||||
fallback: {
|
||||
"url": require.resolve("url/"),
|
||||
url: require.resolve('url/')
|
||||
},
|
||||
alias: {
|
||||
'static': path.resolve(__dirname, '../static'),
|
||||
'src': path.resolve(__dirname, '../src'),
|
||||
'assets': path.resolve(__dirname, '../src/assets'),
|
||||
'components': path.resolve(__dirname, '../src/components'),
|
||||
static: path.resolve(__dirname, '../static'),
|
||||
src: path.resolve(__dirname, '../src'),
|
||||
assets: path.resolve(__dirname, '../src/assets'),
|
||||
components: path.resolve(__dirname, '../src/components'),
|
||||
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||
}
|
||||
},
|
||||
|
@ -67,14 +67,15 @@ module.exports = {
|
|||
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||
type: 'javascript/auto',
|
||||
loader: '@intlify/vue-i18n-loader',
|
||||
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||
include: [
|
||||
// Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||
path.resolve(__dirname, '../src/i18n')
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: "javascript/auto"
|
||||
type: 'javascript/auto'
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
|
@ -115,15 +116,8 @@ module.exports = {
|
|||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new WorkboxPlugin.InjectManifest({
|
||||
swSrc: path.join(__dirname, '..', 'src/sw.js'),
|
||||
swDest: 'sw-pleroma.js',
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
|
||||
}),
|
||||
new VueLoaderPlugin()
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [new VueLoaderPlugin()]
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ var HtmlWebpackPlugin = require('html-webpack-plugin')
|
|||
|
||||
// add hot-reload related code to entry chunks
|
||||
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
|
||||
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(
|
||||
baseWebpackConfig.entry[name]
|
||||
)
|
||||
})
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
|
@ -20,10 +22,10 @@ module.exports = merge(baseWebpackConfig, {
|
|||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
'COMMIT_HASH': JSON.stringify('DEV'),
|
||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
|
||||
'__VUE_OPTIONS_API__': true,
|
||||
'__VUE_PROD_DEVTOOLS__': false
|
||||
COMMIT_HASH: JSON.stringify('DEV'),
|
||||
DEV_OVERRIDES: JSON.stringify(config.dev.settings),
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
|
|
|
@ -2,22 +2,27 @@ 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();
|
||||
.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: {
|
||||
|
@ -32,13 +37,18 @@ 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({
|
||||
|
@ -48,9 +58,8 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: process.env.NODE_ENV === 'testing'
|
||||
? 'index.html'
|
||||
: config.build.index,
|
||||
filename:
|
||||
process.env.NODE_ENV === 'testing' ? 'index.html' : config.build.index,
|
||||
template: 'index.html',
|
||||
inject: true,
|
||||
minify: {
|
||||
|
@ -63,7 +72,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||
},
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'auto'
|
||||
}),
|
||||
})
|
||||
// split vendor js into its own file
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
|
@ -81,9 +90,7 @@ if (config.build.productionGzip) {
|
|||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.build.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
'\\.(' + config.build.productionGzipExtensions.join('|') + ')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
|
|
|
@ -38,6 +38,11 @@ module.exports = {
|
|||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {
|
||||
'/manifest.json': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
'/api': {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
|
@ -54,7 +59,7 @@ module.exports = {
|
|||
cookieDomainRewrite: 'localhost',
|
||||
ws: true,
|
||||
headers: {
|
||||
'Origin': target
|
||||
Origin: target
|
||||
}
|
||||
},
|
||||
'/oauth/revoke': {
|
||||
|
@ -71,7 +76,7 @@ module.exports = {
|
|||
target,
|
||||
changeOrigin: true,
|
||||
cookieDomainRewrite: 'localhost'
|
||||
},
|
||||
}
|
||||
},
|
||||
// CSS Sourcemaps off by default because relative paths are "buggy"
|
||||
// with this option, according to the CSS-Loader README
|
||||
|
|
14
index.html
14
index.html
|
@ -1,21 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no" />
|
||||
<title>Akkoma</title>
|
||||
<link rel="stylesheet" href="/static/font/css/fontello.css">
|
||||
<link rel="stylesheet" href="/static/font/css/animation.css">
|
||||
<link rel="stylesheet" href="/static/font/tiresias.css">
|
||||
<link rel="stylesheet" href="/static/font/css/lato.css">
|
||||
<link rel="stylesheet" href="/static/mfm.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
|
||||
<!--server-generated-meta-->
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<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>
|
||||
|
||||
</html>
|
||||
|
|
33
package.json
33
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "3.2.0",
|
||||
"version": "3.5.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": "npx stylelint src/components/status/status.scss",
|
||||
"stylelint": "stylelint src/**/*.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",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@vuelidate/core": "2.0.0-alpha.42",
|
||||
"@vuelidate/validators": "2.0.0-alpha.30",
|
||||
"@vuelidate/core": "^2.0.0",
|
||||
"@vuelidate/validators": "^2.0.0",
|
||||
"blurhash": "^2.0.4",
|
||||
"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": "6.7.7",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "^4.3.7",
|
||||
|
@ -69,11 +69,13 @@
|
|||
"css-loader": "^6.7.2",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
|
@ -84,7 +86,6 @@
|
|||
"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",
|
||||
|
@ -103,7 +104,11 @@
|
|||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"ora": "0.4.1",
|
||||
"postcss-loader": "3.0.0",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-loader": "^7.0.2",
|
||||
"postcss-sass": "^0.5.0",
|
||||
"prettier": "2.8.1",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "^1.56.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
|
@ -112,9 +117,11 @@
|
|||
"shelljs": "0.8.5",
|
||||
"sinon": "2.4.1",
|
||||
"sinon-chai": "2.14.0",
|
||||
"stylelint": "13.6.1",
|
||||
"stylelint-config-standard": "20.0.0",
|
||||
"stylelint-rscss": "0.4.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",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
plugins: [require('autoprefixer')]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
"extends": ["config:base"]
|
||||
}
|
||||
|
|
65
src/App.js
65
src/App.js
|
@ -24,7 +24,9 @@ export default {
|
|||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
Notifications: defineAsyncComponent(() =>
|
||||
import('./components/notifications/notifications.vue')
|
||||
),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
@ -47,7 +49,10 @@ export default {
|
|||
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() {
|
||||
|
@ -64,14 +69,20 @@ export default {
|
|||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
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 },
|
||||
background() {
|
||||
return this.userBackground || this.instanceBackground
|
||||
},
|
||||
bgStyle() {
|
||||
if (this.background) {
|
||||
return {
|
||||
|
@ -79,29 +90,51 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||
suggestionsEnabled() {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return this.$store.state.instance.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'
|
||||
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
|
||||
},
|
||||
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
|
||||
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: {
|
||||
|
|
136
src/App.scss
136
src/App.scss
|
@ -1,6 +1,7 @@
|
|||
// 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;
|
||||
|
@ -12,8 +13,8 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-family: var(--interfaceFont, sans-serif);
|
||||
font-family: $system-sans-serif;
|
||||
font-family: var(--interfaceFont, $system-sans-serif);
|
||||
margin: 0;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
|
@ -22,84 +23,13 @@ body {
|
|||
overscroll-behavior-y: none;
|
||||
overflow-x: clip;
|
||||
overflow-y: scroll;
|
||||
background: var(--bg);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ## Custom scrollbars
|
||||
// Only show custom scrollbars on devices which
|
||||
// have a cursor/pointer to operate them
|
||||
@media (any-pointer: fine) {
|
||||
* {
|
||||
scrollbar-color: var(--btn) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button,
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--btn);
|
||||
box-shadow: var(--buttonShadow);
|
||||
border-radius: var(--btnRadius);
|
||||
}
|
||||
|
||||
// horizontal/vertical/increment/decrement are webkit-specific stuff
|
||||
// that indicates whether we're affecting vertical scrollbar, increase button etc
|
||||
// stylelint-disable selector-pseudo-class-no-unknown
|
||||
&::-webkit-scrollbar-button {
|
||||
--___bgPadding: 2px;
|
||||
|
||||
color: var(--btnText);
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
|
||||
&:horizontal {
|
||||
background-size: 50% calc(50% - var(--___bgPadding)), 50% calc(50% - var(--___bgPadding));
|
||||
|
||||
&:increment {
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--btnText) 50%, transparent 51%),
|
||||
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%);
|
||||
background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding);
|
||||
}
|
||||
|
||||
&:decrement {
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--btnText) 51%),
|
||||
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%);
|
||||
background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding);
|
||||
}
|
||||
}
|
||||
|
||||
&:vertical {
|
||||
background-size: calc(50% - var(--___bgPadding)) 50%, calc(50% - var(--___bgPadding)) 50%;
|
||||
|
||||
&:increment {
|
||||
background-image:
|
||||
linear-gradient(-45deg, transparent 50%, var(--btnText) 51%),
|
||||
linear-gradient(45deg, transparent 50%, var(--btnText) 51%);
|
||||
background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%;
|
||||
}
|
||||
|
||||
&:decrement {
|
||||
background-image:
|
||||
linear-gradient(-45deg, var(--btnText) 50%, transparent 51%),
|
||||
linear-gradient(45deg, var(--btnText) 50%, transparent 51%);
|
||||
background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
// stylelint-enable selector-pseudo-class-no-unknown
|
||||
}
|
||||
// Body should have background to scrollbar otherwise it will use white (body color?)
|
||||
html {
|
||||
scrollbar-color: var(--selectedMenu) var(--wallpaper);
|
||||
background: var(--wallpaper);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $fallback--link;
|
||||
|
@ -110,7 +40,7 @@ h4 {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
i[class*='icon-'],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--icon;
|
||||
color: var(--icon, $fallback--icon);
|
||||
|
@ -128,6 +58,7 @@ nav {
|
|||
box-sizing: border-box;
|
||||
height: var(--navbar-height);
|
||||
position: fixed;
|
||||
backdrop-filter: blur(12px) saturate(1.2);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
|
@ -182,7 +113,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;
|
||||
|
@ -191,6 +122,7 @@ nav {
|
|||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
overflow-x: clip;
|
||||
padding: 0 calc(var(--columnGap) / 2);
|
||||
|
||||
.column {
|
||||
--___columnMargin: var(--columnGap);
|
||||
|
@ -228,7 +160,9 @@ nav {
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-left: calc(var(--___paddingIncrease) * -1);
|
||||
padding-left: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
padding-left: calc(
|
||||
var(--___paddingIncrease) + var(--___columnMargin) / 2
|
||||
);
|
||||
|
||||
// On browsers that don't support hiding scrollbars we enforce "show scrolbars" mode
|
||||
// might implement old style of hiding scrollbars later if there's demand
|
||||
|
@ -236,7 +170,9 @@ nav {
|
|||
&:not(.-show-scrollbar) {
|
||||
scrollbar-width: none;
|
||||
margin-right: calc(var(--___paddingIncrease) * -1);
|
||||
padding-right: calc(var(--___paddingIncrease) + var(--___columnMargin) / 2);
|
||||
padding-right: calc(
|
||||
var(--___paddingIncrease) + var(--___columnMargin) / 2
|
||||
);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: block;
|
||||
|
@ -276,21 +212,21 @@ nav {
|
|||
|
||||
&.-reverse:not(.-wide):not(.-mobile) {
|
||||
grid-template-columns: var(--maxiColumn) var(--miniColumn);
|
||||
grid-template-areas: "content sidebar";
|
||||
grid-template-areas: 'content sidebar';
|
||||
}
|
||||
|
||||
&.-wide {
|
||||
grid-template-columns: var(--miniColumn) var(--maxiColumn) var(--miniColumn);
|
||||
grid-template-areas: "sidebar content notifs";
|
||||
grid-template-areas: 'sidebar content notifs';
|
||||
|
||||
&.-reverse {
|
||||
grid-template-areas: "notifs content sidebar";
|
||||
grid-template-areas: 'notifs content sidebar';
|
||||
}
|
||||
}
|
||||
|
||||
&.-mobile {
|
||||
grid-template-columns: 100vw;
|
||||
grid-template-areas: "content";
|
||||
grid-template-areas: 'content';
|
||||
padding: 0;
|
||||
|
||||
.column {
|
||||
|
@ -347,7 +283,7 @@ nav {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
i[class*='icon-'],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
|
@ -363,7 +299,9 @@ nav {
|
|||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
|
||||
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
|
||||
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
color: $fallback--text;
|
||||
color: var(--btnPressedText, $fallback--text);
|
||||
|
@ -396,7 +334,9 @@ nav {
|
|||
color: var(--btnToggledText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggled, $fallback--fg);
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3), 0 1px 0 0 rgba(0, 0, 0, 0.2) inset, 0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: 0 0 4px 0 rgba(255, 255, 255, 0.3),
|
||||
0 1px 0 0 rgba(0, 0, 0, 0.2) inset,
|
||||
0 -1px 0 0 rgba(255, 255, 255, 0.2) inset;
|
||||
box-shadow: var(--buttonPressedShadow);
|
||||
|
||||
svg,
|
||||
|
@ -461,14 +401,15 @@ 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(--inputFont, sans-serif);
|
||||
font-family: var(--interfaceFont, sans-serif);
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
|
@ -479,13 +420,13 @@ textarea,
|
|||
padding: 0 var(--_padding);
|
||||
|
||||
&:disabled,
|
||||
&[disabled=disabled],
|
||||
&[disabled='disabled'],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&[type=range] {
|
||||
&[type='range'] {
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
|
@ -493,12 +434,13 @@ 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: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset;
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset,
|
||||
0 0 0 4px var(--fg, $fallback--fg) inset;
|
||||
background-color: var(--accent, $fallback--link);
|
||||
}
|
||||
|
||||
|
@ -519,7 +461,7 @@ textarea,
|
|||
height: 1.1em;
|
||||
border-radius: 100%; // Radio buttons should always be circle
|
||||
box-shadow: 0 0 2px black inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
margin-right: 0.5em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
@ -533,7 +475,7 @@ textarea,
|
|||
}
|
||||
}
|
||||
|
||||
&[type=checkbox] {
|
||||
&[type='checkbox'] {
|
||||
display: none;
|
||||
|
||||
&:checked + label::before {
|
||||
|
@ -559,7 +501,7 @@ textarea,
|
|||
border-radius: $fallback--checkboxRadius;
|
||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||
box-shadow: 0 0 2px black inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
margin-right: 0.5em;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
|
@ -594,8 +536,8 @@ option {
|
|||
.hide-number-spinner {
|
||||
-moz-appearance: textfield;
|
||||
|
||||
&[type=number]::-webkit-inner-spin-button,
|
||||
&[type=number]::-webkit-outer-spin-button {
|
||||
&[type='number']::-webkit-inner-spin-button,
|
||||
&[type='number']::-webkit-outer-spin-button {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
:to="{ name: 'login' }"
|
||||
class="panel-body"
|
||||
>
|
||||
{{ $t("login.hint") }}
|
||||
{{ $t('login.hint') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<router-view />
|
||||
|
|
|
@ -4,7 +4,7 @@ $darkened-background: whitesmoke;
|
|||
|
||||
$fallback--bg: #121a24;
|
||||
$fallback--fg: #182230;
|
||||
$fallback--faint: rgba(185, 185, 186, .5);
|
||||
$fallback--faint: rgba(185, 185, 186, 0.5);
|
||||
$fallback--text: #b9b9ba;
|
||||
$fallback--link: #d8a070;
|
||||
$fallback--icon: #666;
|
||||
|
@ -16,8 +16,8 @@ $fallback--cBlue: #0095ff;
|
|||
$fallback--cGreen: #0fa00f;
|
||||
$fallback--cOrange: orange;
|
||||
|
||||
$fallback--alertError: rgba(211,16,20,.5);
|
||||
$fallback--alertWarning: rgba(111,111,20,.5);
|
||||
$fallback--alertError: rgba(211, 16, 20, 0.5);
|
||||
$fallback--alertWarning: rgba(111, 111, 20, 0.5);
|
||||
|
||||
$fallback--panelRadius: 10px;
|
||||
$fallback--checkboxRadius: 2px;
|
||||
|
@ -28,6 +28,14 @@ $fallback--avatarRadius: 4px;
|
|||
$fallback--avatarAltRadius: 10px;
|
||||
$fallback--attachmentRadius: 10px;
|
||||
|
||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1),
|
||||
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
|
||||
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
$system-sans-serif: -apple-system, BlinkMacSystemFont, avenir next, avenir,
|
||||
segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial,
|
||||
sans-serif;
|
||||
$system-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,
|
||||
monospace;
|
||||
|
|
|
@ -3,13 +3,21 @@ import { createApp } from 'vue'
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeLayers
|
||||
} from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
config.autoAddCss = false
|
||||
|
||||
import App from '../App.vue'
|
||||
import routes from './routes'
|
||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||
|
||||
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
|
||||
import {
|
||||
windowWidth,
|
||||
windowHeight
|
||||
} from '../services/window_utils/window_utils'
|
||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||
|
@ -23,7 +31,9 @@ const parsedInitialResults = () => {
|
|||
return null
|
||||
}
|
||||
if (!staticInitialResults) {
|
||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||
staticInitialResults = JSON.parse(
|
||||
document.getElementById('initial-results').textContent
|
||||
)
|
||||
}
|
||||
return staticInitialResults
|
||||
}
|
||||
|
@ -71,18 +81,30 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const textlimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'textlimit',
|
||||
value: textlimit
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'accountApprovalRequired',
|
||||
value: data.approval_required
|
||||
})
|
||||
// don't override cookie if set
|
||||
if (!Cookies.get('userLanguage')) {
|
||||
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
|
||||
store.dispatch('setOption', {
|
||||
name: 'interfaceLanguage',
|
||||
value: resolveLanguage(data.languages)
|
||||
})
|
||||
}
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'vapidPublicKey',
|
||||
value: vapidPublicKey
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load instance config, potentially fatal')
|
||||
|
@ -97,10 +119,12 @@ const getBackendProvidedConfig = async ({ store }) => {
|
|||
const data = await res.json()
|
||||
return data.pleroma_fe
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||
console.error(
|
||||
'Could not load backend-provided frontend config, potentially fatal'
|
||||
)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +135,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.')
|
||||
|
@ -150,19 +174,16 @@ 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)
|
||||
|
@ -190,7 +211,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")
|
||||
|
@ -203,9 +224,12 @@ const getInstancePanel = async ({ store }) => {
|
|||
const res = await preloadFetch('/instance/panel.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'instanceSpecificPanelContent',
|
||||
value: html
|
||||
})
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load instance panel")
|
||||
|
@ -218,7 +242,8 @@ 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(
|
||||
const stickers = (
|
||||
await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
var meta = {}
|
||||
|
@ -231,12 +256,16 @@ const getStickers = async ({ store }) => {
|
|||
meta
|
||||
}
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
)
|
||||
).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")
|
||||
|
@ -251,13 +280,19 @@ const getAppSecret = async ({ store }) => {
|
|||
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
||||
.then((token) => {
|
||||
commit('setAppToken', token.access_token)
|
||||
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
commit(
|
||||
'setBackendInteractor',
|
||||
backendInteractorService(store.getters.getToken())
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
const nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'staffAccounts',
|
||||
value: nicknames
|
||||
})
|
||||
}
|
||||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
|
@ -267,65 +302,146 @@ 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'
|
||||
value:
|
||||
typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federationPolicy',
|
||||
value: federation
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'localBubbleInstances',
|
||||
value: metadata.localBubbleInstances
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
value: typeof federation.enabled === 'undefined'
|
||||
? true
|
||||
: federation.enabled
|
||||
value:
|
||||
typeof federation.enabled === 'undefined' ? true : federation.enabled
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'publicTimelineVisibility',
|
||||
value: metadata.publicTimelineVisibility
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federatedTimelineAvailable',
|
||||
value: metadata.federatedTimelineAvailable
|
||||
})
|
||||
|
||||
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')
|
||||
|
@ -335,11 +451,16 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const setConfig = async ({ store }) => {
|
||||
// apiConfig, staticConfig
|
||||
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||
const configInfos = await Promise.all([
|
||||
getBackendProvidedConfig({ store }),
|
||||
getStaticConfig()
|
||||
])
|
||||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(
|
||||
getAppSecret({ store })
|
||||
)
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
|
@ -362,7 +483,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
FaviconService.initFaviconService()
|
||||
|
||||
const overrides = window.___pleromafe_dev_overrides || {}
|
||||
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||
const server =
|
||||
typeof overrides.target !== 'undefined'
|
||||
? overrides.target
|
||||
: window.location.origin
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
|
||||
await setConfig({ store })
|
||||
|
@ -372,7 +496,10 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
const customThemePresent = customThemeSource || customTheme
|
||||
|
||||
if (customThemePresent) {
|
||||
if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) {
|
||||
if (
|
||||
customThemeSource &&
|
||||
customThemeSource.themeEngineVersion === CURRENT_VERSION
|
||||
) {
|
||||
applyTheme(customThemeSource)
|
||||
} else {
|
||||
applyTheme(customTheme)
|
||||
|
@ -393,9 +520,6 @@ 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 })
|
||||
|
||||
|
@ -403,7 +527,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,6 +22,8 @@ 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) => {
|
||||
|
@ -33,49 +35,145 @@ export default (store) => {
|
|||
}
|
||||
|
||||
let routes = [
|
||||
{ name: 'root',
|
||||
{
|
||||
name: 'root',
|
||||
path: '/',
|
||||
redirect: _to => {
|
||||
return (store.state.users.currentUser
|
||||
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: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{
|
||||
name: 'registration-request-sent',
|
||||
path: '/registration-request-sent',
|
||||
component: RegistrationRequestSent
|
||||
},
|
||||
{
|
||||
name: 'awaiting-email-confirmation',
|
||||
path: '/awaiting-email-confirmation',
|
||||
component: AwaitingEmailConfirmation
|
||||
},
|
||||
{
|
||||
name: 'password-reset',
|
||||
path: '/password-reset',
|
||||
component: PasswordReset,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
name: 'registration-token',
|
||||
path: '/registration/:token',
|
||||
component: Registration
|
||||
},
|
||||
{
|
||||
name: 'friend-requests',
|
||||
path: '/friend-requests',
|
||||
component: FollowRequests,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
path: '/:username/notifications',
|
||||
component: Notifications,
|
||||
props: () => ({ disableTeleport: true }),
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{
|
||||
name: 'oauth-callback',
|
||||
path: '/oauth-callback',
|
||||
component: OAuthCallback,
|
||||
props: (route) => ({ code: route.query.code })
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: '/search',
|
||||
component: Search,
|
||||
props: (route) => ({ query: route.query.query })
|
||||
},
|
||||
{
|
||||
name: 'who-to-follow',
|
||||
path: '/who-to-follow',
|
||||
component: WhoToFollow,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
},
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: 'lists', path: '/lists', component: Lists },
|
||||
{ name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
|
||||
{ name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } }
|
||||
{
|
||||
name: 'announcements',
|
||||
path: '/announcements',
|
||||
component: AnnouncementsPage
|
||||
},
|
||||
{
|
||||
name: 'user-profile',
|
||||
path: '/:_(users)?/:name',
|
||||
component: UserProfile,
|
||||
meta: { dontScroll: true }
|
||||
}
|
||||
]
|
||||
|
||||
return routes
|
||||
|
|
|
@ -15,11 +15,15 @@ const About = {
|
|||
LocalBubblePanel
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
showFeaturesPanel() {
|
||||
return this.$store.state.instance.showFeaturesPanel
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
return (
|
||||
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
showLocalBubblePanel() {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
|
|
|
@ -11,5 +11,4 @@
|
|||
|
||||
<script src="./about.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
<style lang="scss"></style>
|
||||
|
|
|
@ -3,18 +3,12 @@ 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'
|
||||
],
|
||||
props: ['user', 'relationship'],
|
||||
data() {
|
||||
return {
|
||||
showingConfirmBlock: false
|
||||
|
@ -26,6 +20,9 @@ const AccountActions = {
|
|||
ConfirmModal
|
||||
},
|
||||
methods: {
|
||||
refetchRelationship() {
|
||||
return this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
showConfirmBlock() {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
|
@ -57,6 +54,16 @@ const AccountActions = {
|
|||
},
|
||||
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: {
|
||||
|
@ -64,7 +71,8 @@ const AccountActions = {
|
|||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
pleromaChatMessagesAvailable: (state) =>
|
||||
state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:content>
|
||||
<template #content>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="relationship.following">
|
||||
<button
|
||||
|
@ -55,9 +55,23 @@
|
|||
>
|
||||
{{ $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 v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled ellipsis-button">
|
||||
<FAIcon
|
||||
class="icon"
|
||||
|
@ -79,10 +93,8 @@
|
|||
keypath="user_card.block_confirm"
|
||||
tag="span"
|
||||
>
|
||||
<template v-slot:user>
|
||||
<span
|
||||
v-text="user.screen_name_ui"
|
||||
/>
|
||||
<template #user>
|
||||
<span v-text="user.screen_name_ui" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
|
|
|
@ -25,7 +25,7 @@ const Announcement = {
|
|||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser
|
||||
}),
|
||||
content() {
|
||||
return this.announcement.content
|
||||
|
@ -39,7 +39,10 @@ const Announcement = {
|
|||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
},
|
||||
startsAt() {
|
||||
const time = this.announcement['starts_at']
|
||||
|
@ -47,7 +50,10 @@ const Announcement = {
|
|||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
},
|
||||
endsAt() {
|
||||
const time = this.announcement['ends_at']
|
||||
|
@ -55,7 +61,10 @@ const Announcement = {
|
|||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
)
|
||||
},
|
||||
inactive() {
|
||||
return this.announcement.inactive
|
||||
|
@ -64,7 +73,10 @@ const Announcement = {
|
|||
methods: {
|
||||
markAsRead() {
|
||||
if (!this.isRead) {
|
||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||
return this.$store.dispatch(
|
||||
'markAnnouncementAsRead',
|
||||
this.announcement.id
|
||||
)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement() {
|
||||
|
@ -72,7 +84,9 @@ const Announcement = {
|
|||
},
|
||||
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() {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
|
||||
|
@ -82,14 +96,15 @@ const Announcement = {
|
|||
this.editing = true
|
||||
},
|
||||
submitEdit() {
|
||||
this.$store.dispatch('editAnnouncement', {
|
||||
this.$store
|
||||
.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
class="times"
|
||||
>
|
||||
<span v-if="publishedAt">
|
||||
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
|
||||
{{
|
||||
$t('announcements.published_time_display', { time: publishedAt })
|
||||
}}
|
||||
</span>
|
||||
<span v-if="startsAt">
|
||||
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
||||
|
@ -99,7 +101,7 @@
|
|||
<script src="./announcement.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
@import '../../variables';
|
||||
|
||||
.announcement {
|
||||
border-bottom-width: 1px;
|
||||
|
@ -108,7 +110,8 @@
|
|||
border-radius: 0;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
.heading,
|
||||
.body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,22 +10,26 @@
|
|||
:disabled="disabled"
|
||||
/>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
|
||||
<label for="announcement-start-time">{{
|
||||
$t('announcements.start_time_prompt')
|
||||
}}</label>
|
||||
<input
|
||||
id="announcement-start-time"
|
||||
v-model="announcement.startsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
/>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
|
||||
<label for="announcement-end-time">{{
|
||||
$t('announcements.end_time_prompt')
|
||||
}}</label>
|
||||
<input
|
||||
id="announcement-end-time"
|
||||
v-model="announcement.endsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
/>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<Checkbox
|
||||
|
@ -33,7 +37,9 @@
|
|||
v-model="announcement.allDay"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||
<label for="announcement-all-day">{{
|
||||
$t('announcements.all_day_prompt')
|
||||
}}</label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -24,7 +24,7 @@ const AnnouncementsPage = {
|
|||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser
|
||||
}),
|
||||
announcements() {
|
||||
return this.$store.state.announcements.announcements
|
||||
|
@ -33,13 +33,14 @@ const AnnouncementsPage = {
|
|||
methods: {
|
||||
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(() => {
|
||||
|
|
|
@ -6,9 +6,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
>
|
||||
<section v-if="currentUser && currentUser.role === 'admin'">
|
||||
<div class="post-form">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
||||
|
@ -50,9 +48,7 @@
|
|||
v-for="announcement in announcements"
|
||||
:key="announcement.id"
|
||||
>
|
||||
<announcement
|
||||
:announcement="announcement"
|
||||
/>
|
||||
<announcement :announcement="announcement" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,13 +57,14 @@
|
|||
<script src="./announcements_page.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
@import '../../variables';
|
||||
|
||||
.announcements-page {
|
||||
.post-form {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
.heading,
|
||||
.body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,8 +35,8 @@ export default {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
.btn {
|
||||
margin: .5em;
|
||||
padding: .5em 2em;
|
||||
margin: 0.5em;
|
||||
padding: 0.5em 2em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
faPencilAlt,
|
||||
faAlignRight
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import Blurhash from '../blurhash/Blurhash.vue'
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
|
@ -53,7 +54,9 @@ const Attachment = {
|
|||
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,
|
||||
|
@ -63,7 +66,8 @@ const Attachment = {
|
|||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment
|
||||
VideoAttachment,
|
||||
Blurhash
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
|
@ -84,6 +88,9 @@ const Attachment = {
|
|||
useContainFit() {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
useBlurhash() {
|
||||
return this.$store.getters.mergedConfig.useBlurhash
|
||||
},
|
||||
placeholderName() {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
return this.type.toUpperCase()
|
||||
|
@ -106,7 +113,7 @@ const Attachment = {
|
|||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty() {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
return this.type === 'html' && !this.attachment.oembed
|
||||
},
|
||||
useModal() {
|
||||
let modalTypes = []
|
||||
|
@ -180,7 +187,8 @@ const Attachment = {
|
|||
},
|
||||
toggleHidden(event) {
|
||||
if (
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
!this.showHidden &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
|
@ -208,7 +216,9 @@ const Attachment = {
|
|||
},
|
||||
resize(e) {
|
||||
const target = e.target || e
|
||||
if (!(target instanceof window.Element)) { return }
|
||||
if (!(target instanceof window.Element)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Reset to default height for empty form, nothing else to do here.
|
||||
if (target.value === '') {
|
||||
|
@ -219,7 +229,9 @@ 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)
|
||||
|
|
|
@ -117,7 +117,6 @@
|
|||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
@click.prevent
|
||||
>
|
||||
<FAIcon :icon="placeholderIconClass" />
|
||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
||||
<b>{{ nsfw ? 'NSFW / ' : '' }}</b
|
||||
>{{ edit ? '' : placeholderName }}
|
||||
</a>
|
||||
<div
|
||||
v-if="edit || remove"
|
||||
|
@ -30,7 +31,11 @@
|
|||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||
v-if="
|
||||
size !== 'hide' &&
|
||||
!hideDescription &&
|
||||
(edit || localDescription || showDescription)
|
||||
"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
|
@ -41,7 +46,7 @@
|
|||
class="description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
/>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
|
@ -64,11 +69,19 @@
|
|||
: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"
|
||||
|
@ -88,7 +101,12 @@
|
|||
<FAIcon icon="stop" />
|
||||
</button>
|
||||
<button
|
||||
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||
v-if="
|
||||
attachment.description &&
|
||||
size !== 'small' &&
|
||||
!edit &&
|
||||
type !== 'unknown'
|
||||
"
|
||||
class="button-unstyled attachment-button"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
|
@ -218,11 +236,13 @@
|
|||
v-if="attachment.thumb_url"
|
||||
class="image"
|
||||
>
|
||||
<img :src="attachment.thumb_url">
|
||||
<img :src="attachment.thumb_url" />
|
||||
</div>
|
||||
<div class="text">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
||||
<h1>
|
||||
<a :href="attachment.url">{{ attachment.oembed.title }}</a>
|
||||
</h1>
|
||||
<div v-html="attachment.oembed.oembedHTML" />
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
|
@ -244,7 +264,11 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||
v-if="
|
||||
size !== 'hide' &&
|
||||
!hideDescription &&
|
||||
(edit || (localDescription && showDescription))
|
||||
"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
|
|
|
@ -11,8 +11,12 @@ const AuthForm = {
|
|||
},
|
||||
computed: {
|
||||
authForm() {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
if (this.requiredTOTP) {
|
||||
return 'MFATOTPForm'
|
||||
}
|
||||
if (this.requiredRecovery) {
|
||||
return 'MFARecoveryForm'
|
||||
}
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
|
|
|
@ -2,11 +2,13 @@ const debounceMilliseconds = 500
|
|||
|
||||
export default {
|
||||
props: {
|
||||
query: { // function to query results and return a promise
|
||||
query: {
|
||||
// function to query results and return a promise
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
filter: { // function to filter results in real time
|
||||
filter: {
|
||||
// function to filter results in real time
|
||||
type: Function
|
||||
},
|
||||
placeholder: {
|
||||
|
@ -38,7 +40,9 @@ export default {
|
|||
this.timeout = setTimeout(() => {
|
||||
this.results = []
|
||||
if (term) {
|
||||
this.query(term).then((results) => { this.results = results })
|
||||
this.query(term).then((results) => {
|
||||
this.results = results
|
||||
})
|
||||
}
|
||||
}, debounceMilliseconds)
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
:placeholder="placeholder"
|
||||
class="autosuggest-input"
|
||||
@click="onInputClick"
|
||||
>
|
||||
/>
|
||||
<div
|
||||
v-if="resultsVisible && filtered.length > 0"
|
||||
class="autosuggest-results"
|
||||
|
|
|
@ -13,7 +13,11 @@ const AvatarList = {
|
|||
},
|
||||
methods: {
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
computed: {}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<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,9 +4,7 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
props: ['user'],
|
||||
data() {
|
||||
return {
|
||||
userExpanded: false
|
||||
|
@ -22,7 +20,11 @@ const BasicUserCard = {
|
|||
this.userExpanded = !this.userExpanded
|
||||
},
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
64
src/components/blurhash/Blurhash.vue
Normal file
64
src/components/blurhash/Blurhash.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<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>
|
|
@ -4,7 +4,9 @@ const PublicTimeline = {
|
|||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.bubble
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
|
@ -12,7 +14,6 @@ const PublicTimeline = {
|
|||
unmounted() {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PublicTimeline
|
||||
|
|
|
@ -18,7 +18,10 @@ export default {
|
|||
if (this.date.getTime() === today.getTime()) {
|
||||
return this.$t('display_date.today')
|
||||
} else {
|
||||
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
|
||||
return this.date.toLocaleDateString(
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
{ day: 'numeric', month: 'long' }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:checked="modelValue"
|
||||
:indeterminate="indeterminate"
|
||||
@change="$emit('update:modelValue', $event.target.checked)"
|
||||
>
|
||||
/>
|
||||
<i class="checkbox-indicator" />
|
||||
<span
|
||||
v-if="!!$slots.default"
|
||||
|
@ -22,12 +22,8 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['update:modelValue'],
|
||||
props: [
|
||||
'modelValue',
|
||||
'indeterminate',
|
||||
'disabled'
|
||||
]
|
||||
props: ['modelValue', 'indeterminate', 'disabled'],
|
||||
emits: ['update:modelValue']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -56,7 +52,7 @@ export default {
|
|||
border-radius: $fallback--checkboxRadius;
|
||||
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
|
||||
box-shadow: 0px 0px 2px black inset;
|
||||
box-shadow: var(--inputShadow);
|
||||
box-shadow: 0 0 0 1px var(--icon, $fallback--icon) inset;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--input, $fallback--fg);
|
||||
vertical-align: top;
|
||||
|
@ -71,7 +67,7 @@ export default {
|
|||
&.disabled {
|
||||
.checkbox-indicator::before,
|
||||
.label {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.label {
|
||||
color: $fallback--faint;
|
||||
|
@ -79,7 +75,7 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
input[type='checkbox'] {
|
||||
display: none;
|
||||
|
||||
&:checked + .checkbox-indicator::before {
|
||||
|
@ -92,11 +88,10 @@ export default {
|
|||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
& > span {
|
||||
margin-left: .5em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
flex: 0 0 0;
|
||||
max-width: 9em;
|
||||
align-items: stretch;
|
||||
padding: .2em 8px;
|
||||
padding: 0.2em 8px;
|
||||
|
||||
input {
|
||||
background: none;
|
||||
|
@ -40,9 +40,10 @@
|
|||
}
|
||||
.transparentIndicator {
|
||||
// forgot to install counter-strike source, ooops
|
||||
background-color: #FF00FF;
|
||||
background-color: #ff00ff;
|
||||
position: relative;
|
||||
&::before, &::after {
|
||||
&::before,
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
background-color: #000000;
|
||||
|
@ -64,5 +65,4 @@
|
|||
.label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,12 @@
|
|||
:model-value="present"
|
||||
:disabled="disabled"
|
||||
class="opt"
|
||||
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
@update:modelValue="
|
||||
$emit(
|
||||
'update:modelValue',
|
||||
typeof modelValue === 'undefined' ? fallback : undefined
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="input color-input-field">
|
||||
<input
|
||||
|
@ -24,7 +29,7 @@
|
|||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
/>
|
||||
<input
|
||||
v-if="validColor"
|
||||
:id="name"
|
||||
|
@ -33,7 +38,7 @@
|
|||
:value="modelValue || fallback"
|
||||
:disabled="!present || disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
/>
|
||||
<div
|
||||
v-if="transparentColor"
|
||||
class="transparentIndicator"
|
||||
|
@ -46,7 +51,6 @@
|
|||
</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'
|
||||
|
@ -108,6 +112,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
|
||||
<style lang="scss">
|
||||
.color-control {
|
||||
|
|
|
@ -22,8 +22,7 @@ const ConfirmModal = {
|
|||
type: String
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
onCancel() {
|
||||
this.$emit('cancelled')
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
</dialog-modal>
|
||||
</template>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../_variables';
|
||||
|
||||
|
@ -35,5 +37,3 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
|
|
|
@ -43,11 +43,7 @@ import {
|
|||
faThumbsUp
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAdjust,
|
||||
faExclamationTriangle,
|
||||
faThumbsUp
|
||||
)
|
||||
library.add(faAdjust, faExclamationTriangle, faThumbsUp)
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -66,18 +62,34 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
hint() {
|
||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||
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')
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,7 @@ import {
|
|||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAngleDoubleDown,
|
||||
faAngleDoubleLeft,
|
||||
faChevronLeft
|
||||
)
|
||||
library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
|
||||
|
||||
const sortById = (a, b) => {
|
||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||
|
@ -39,12 +35,13 @@ 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 = {
|
||||
|
@ -80,7 +77,10 @@ const conversation = {
|
|||
return maxDepth >= 1 ? maxDepth : 1
|
||||
},
|
||||
streamingEnabled() {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
return (
|
||||
this.mergedConfig.useStreamingApi &&
|
||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
)
|
||||
},
|
||||
displayStyle() {
|
||||
return this.$store.getters.mergedConfig.conversationDisplay
|
||||
|
@ -108,11 +108,12 @@ const conversation = {
|
|||
},
|
||||
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
|
||||
}
|
||||
|
@ -142,8 +143,12 @@ 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
|
||||
}
|
||||
|
@ -157,42 +162,57 @@ const conversation = {
|
|||
}, {})
|
||||
},
|
||||
threadTree() {
|
||||
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
||||
const reverseLookupTable = this.conversation.reduce(
|
||||
(table, status, index) => {
|
||||
table[status.id] = index
|
||||
return table
|
||||
}, {})
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const threads = this.conversation.reduce((a, cur) => {
|
||||
const threads = this.conversation.reduce(
|
||||
(a, cur) => {
|
||||
const id = cur.id
|
||||
a.forest[id] = this.getReplies(id)
|
||||
.map(s => s.id)
|
||||
a.forest[id] = this.getReplies(id).map((s) => s.id)
|
||||
|
||||
return a
|
||||
}, {
|
||||
},
|
||||
{
|
||||
forest: {}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) =>
|
||||
topLevel
|
||||
.map((id) => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
}
|
||||
|
||||
processed[id] = true
|
||||
return [{
|
||||
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), [])
|
||||
},
|
||||
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)
|
||||
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
|
||||
}, {})
|
||||
},
|
||||
|
@ -202,10 +222,14 @@ const conversation = {
|
|||
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
|
||||
|
@ -217,10 +241,14 @@ const conversation = {
|
|||
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
|
||||
|
@ -233,8 +261,16 @@ const 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)
|
||||
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() {
|
||||
|
@ -260,15 +296,26 @@ const conversation = {
|
|||
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
|
||||
return (
|
||||
this.isTreeView &&
|
||||
this.isExpanded &&
|
||||
this.ancestorsOf(this.diveRoot).length
|
||||
)
|
||||
},
|
||||
replies() {
|
||||
let i = 1
|
||||
// eslint-disable-next-line camelcase
|
||||
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
|
||||
return reduce(
|
||||
this.conversation,
|
||||
(result, { id, in_reply_to_status_id }) => {
|
||||
/* eslint-disable camelcase */
|
||||
const irid = in_reply_to_status_id
|
||||
/* eslint-enable camelcase */
|
||||
|
@ -281,7 +328,9 @@ const conversation = {
|
|||
}
|
||||
i++
|
||||
return result
|
||||
}, {})
|
||||
},
|
||||
{}
|
||||
)
|
||||
},
|
||||
isExpanded() {
|
||||
return !!(this.expanded || this.isPage)
|
||||
|
@ -298,7 +347,7 @@ const conversation = {
|
|||
if (this.threadDisplayStatusObject[id]) {
|
||||
return this.threadDisplayStatusObject[id]
|
||||
}
|
||||
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
|
||||
if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
|
||||
return 'showing'
|
||||
} else {
|
||||
return 'hidden'
|
||||
|
@ -339,7 +388,7 @@ const conversation = {
|
|||
},
|
||||
focused() {
|
||||
return (id) => {
|
||||
return (this.isExpanded) && id === this.highlight
|
||||
return this.isExpanded && id === this.highlight
|
||||
}
|
||||
},
|
||||
maybeHighlight() {
|
||||
|
@ -347,7 +396,7 @@ const conversation = {
|
|||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus
|
||||
})
|
||||
},
|
||||
components: {
|
||||
|
@ -358,7 +407,11 @@ const conversation = {
|
|||
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()
|
||||
|
@ -372,23 +425,25 @@ const conversation = {
|
|||
}
|
||||
},
|
||||
virtualHidden(value) {
|
||||
this.$store.dispatch(
|
||||
'setVirtualHeight',
|
||||
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
|
||||
)
|
||||
this.$store.dispatch('setVirtualHeight', {
|
||||
statusId: this.statusId,
|
||||
height: `${this.$el.clientHeight}px`
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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()
|
||||
|
@ -417,7 +472,11 @@ const conversation = {
|
|||
},
|
||||
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) {
|
||||
this.threadDisplayStatusObject = {
|
||||
|
@ -432,7 +491,9 @@ const conversation = {
|
|||
},
|
||||
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) {
|
||||
this.setThreadDisplayRecursively(id, 'showing')
|
||||
|
@ -447,7 +508,11 @@ const conversation = {
|
|||
}
|
||||
},
|
||||
toggleStatusContentProperty(id, name) {
|
||||
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
||||
this.setStatusContentProperty(
|
||||
id,
|
||||
name,
|
||||
!this.statusContentProperties[id][name]
|
||||
)
|
||||
},
|
||||
leastVisibleAncestor(id) {
|
||||
let cur = id
|
||||
|
@ -467,7 +532,9 @@ const conversation = {
|
|||
this.tryScrollTo(id)
|
||||
},
|
||||
diveToTopLevel() {
|
||||
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
|
||||
this.tryScrollTo(
|
||||
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id
|
||||
)
|
||||
},
|
||||
// only used when we are not on a page
|
||||
undive() {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-if="!hideStatus"
|
||||
:style="hiddenStyle"
|
||||
class="Conversation"
|
||||
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
|
||||
:class="{ '-expanded': isExpanded, panel: isExpanded }"
|
||||
>
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
|
@ -35,13 +35,15 @@
|
|||
@click.prevent="diveToTopLevel"
|
||||
>
|
||||
<template #icon>
|
||||
<FAIcon
|
||||
icon="angle-double-left"
|
||||
/>
|
||||
<FAIcon icon="angle-double-left" />
|
||||
</template>
|
||||
<template #text>
|
||||
<span>
|
||||
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
|
||||
{{
|
||||
$tc('status.show_all_conversation', otherTopLevelCount, {
|
||||
numStatus: otherTopLevelCount
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -54,14 +56,20 @@
|
|||
v-for="status in ancestorsOf(diveRoot)"
|
||||
:key="status.id"
|
||||
class="thread-ancestor"
|
||||
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||
:class="{
|
||||
'thread-ancestor-has-other-replies':
|
||||
getReplies(status.id).length > 1,
|
||||
'-faded': shouldFadeAncestors
|
||||
}"
|
||||
>
|
||||
<status
|
||||
ref="statusComponent"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:show-pinned="
|
||||
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
|
||||
"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
@ -69,7 +77,6 @@
|
|||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
|
||||
:simple-tree="treeViewIsSimple"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
|
@ -78,28 +85,47 @@
|
|||
:total-reply-depth="totalReplyDepth"
|
||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||
:dive="() => diveIntoStatus(status.id)"
|
||||
|
||||
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
||||
:controlled-showing-tall="
|
||||
statusContentProperties[status.id].showingTall
|
||||
"
|
||||
:controlled-expanding-subject="
|
||||
statusContentProperties[status.id].expandingSubject
|
||||
"
|
||||
:controlled-showing-long-subject="
|
||||
statusContentProperties[status.id].showingLongSubject
|
||||
"
|
||||
:controlled-replying="statusContentProperties[status.id].replying"
|
||||
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
||||
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
||||
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
||||
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||
|
||||
:controlled-media-playing="
|
||||
statusContentProperties[status.id].mediaPlaying
|
||||
"
|
||||
:controlled-toggle-showing-tall="
|
||||
() => toggleStatusContentProperty(status.id, 'showingTall')
|
||||
"
|
||||
:controlled-toggle-expanding-subject="
|
||||
() => toggleStatusContentProperty(status.id, 'expandingSubject')
|
||||
"
|
||||
:controlled-toggle-showing-long-subject="
|
||||
() =>
|
||||
toggleStatusContentProperty(status.id, 'showingLongSubject')
|
||||
"
|
||||
:controlled-toggle-replying="
|
||||
() => toggleStatusContentProperty(status.id, 'replying')
|
||||
"
|
||||
:controlled-set-media-playing="
|
||||
(newVal) =>
|
||||
toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)
|
||||
"
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
<div
|
||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||
v-if="
|
||||
showOtherRepliesButtonBelowStatus &&
|
||||
getReplies(status.id).length > 1
|
||||
"
|
||||
class="thread-ancestor-dive-box"
|
||||
>
|
||||
<div
|
||||
class="thread-ancestor-dive-box-inner"
|
||||
>
|
||||
<div class="thread-ancestor-dive-box-inner">
|
||||
<i18n-t
|
||||
tag="button"
|
||||
scope="global"
|
||||
|
@ -108,13 +134,17 @@
|
|||
@click.prevent="diveIntoStatus(status.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<FAIcon
|
||||
icon="angle-double-right"
|
||||
/>
|
||||
<FAIcon icon="angle-double-right" />
|
||||
</template>
|
||||
<template #text>
|
||||
<span>
|
||||
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
|
||||
{{
|
||||
$tc(
|
||||
'status.ancestor_follow',
|
||||
getReplies(status.id).length - 1,
|
||||
{ numReplies: getReplies(status.id).length - 1 }
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
@ -127,7 +157,6 @@
|
|||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
:depth="0"
|
||||
|
||||
:status="status"
|
||||
:in-profile="inProfile"
|
||||
:conversation="conversation"
|
||||
|
@ -135,13 +164,11 @@
|
|||
:is-expanded="isExpanded"
|
||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:focused="focused"
|
||||
:get-replies="getReplies"
|
||||
:highlight="maybeHighlight"
|
||||
:set-highlight="setHighlight"
|
||||
:toggle-expanded="toggleExpanded"
|
||||
|
||||
:simple="treeViewIsSimple"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
|
@ -165,7 +192,9 @@
|
|||
:inline-expanded="collapsable && isExpanded"
|
||||
:statusoid="status"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:show-pinned="
|
||||
pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]
|
||||
"
|
||||
:focused="focused(status.id)"
|
||||
:in-conversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
@ -173,7 +202,6 @@
|
|||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
|
@ -182,7 +210,6 @@
|
|||
:status-content-properties="statusContentProperties"
|
||||
:set-status-content-property="setStatusContentProperty"
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
|
@ -233,7 +260,8 @@
|
|||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
/* Make the button stretch along the whole row */
|
||||
&, &-inner {
|
||||
&,
|
||||
&-inner {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
@ -253,8 +281,7 @@
|
|||
.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,
|
||||
&.-expanded .thread-tree .conversation-status {
|
||||
&:last-child .conversation-status {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
@ -271,7 +298,8 @@
|
|||
border-left-color: $fallback--cRed;
|
||||
border-left-color: var(--cRed, $fallback--cRed);
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius)
|
||||
var(--panelRadius, $fallback--panelRadius);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
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,
|
||||
|
@ -19,6 +24,7 @@ import {
|
|||
faInfoCircle,
|
||||
faUserTie
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
library.add(
|
||||
faSignInAlt,
|
||||
|
@ -46,42 +52,56 @@ 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 },
|
||||
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 ? {
|
||||
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`,
|
||||
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 },
|
||||
this.enableMask
|
||||
? {}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
}
|
||||
)
|
||||
},
|
||||
logo() {
|
||||
return this.$store.state.instance.logo
|
||||
},
|
||||
mergedConfig() {
|
||||
return this.$store.getters.mergedConfig
|
||||
},
|
||||
sitename () { return this.$store.state.instance.name },
|
||||
sitename() {
|
||||
return this.$store.state.instance.name
|
||||
},
|
||||
showNavShortcuts() {
|
||||
return this.mergedConfig.showNavShortcuts
|
||||
},
|
||||
|
@ -94,16 +114,29 @@ export default {
|
|||
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 },
|
||||
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() {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
}
|
||||
},
|
||||
...mapState({
|
||||
publicTimelineVisible,
|
||||
federatedTimelineVisible,
|
||||
bubbleTimelineVisible
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
display: grid;
|
||||
grid-template-rows: var(--navbar-height);
|
||||
grid-template-columns: 2fr auto 2fr;
|
||||
grid-template-areas: "nav-left logo actions";
|
||||
grid-template-areas: 'nav-left logo actions';
|
||||
box-sizing: border-box;
|
||||
padding: 0 1.2em;
|
||||
margin: auto;
|
||||
|
@ -24,11 +24,12 @@
|
|||
|
||||
&.-logoLeft .inner-nav {
|
||||
grid-template-columns: auto 2fr 2fr;
|
||||
grid-template-areas: "logo nav-left actions";
|
||||
grid-template-areas: 'logo nav-left actions';
|
||||
}
|
||||
|
||||
.button-default {
|
||||
&, svg {
|
||||
&,
|
||||
svg {
|
||||
color: $fallback--text;
|
||||
color: var(--btnTopBarText, $fallback--text);
|
||||
}
|
||||
|
@ -49,7 +50,7 @@
|
|||
color: $fallback--text;
|
||||
color: var(--btnToggledTopBarText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||
background-color: var(--btnToggledTopBar, $fallback--fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,13 +89,25 @@
|
|||
width: 2em;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
|
||||
&.router-link-active {
|
||||
font-size: 1.2em;
|
||||
margin-top: 0.05em;
|
||||
// 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);
|
||||
}
|
||||
|
||||
.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.png"
|
||||
>
|
||||
src="/favicon.svg"
|
||||
/>
|
||||
<span
|
||||
v-if="!hideSiteName"
|
||||
class="site-name"
|
||||
|
@ -44,6 +44,7 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="publicTimelineVisible"
|
||||
:to="{ name: 'public-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -55,7 +56,7 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="currentUser && showBubbleTimeline"
|
||||
v-if="bubbleTimelineVisible"
|
||||
:to="{ name: 'bubble-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -67,6 +68,7 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="federatedTimelineVisible"
|
||||
:to="{ name: 'public-external-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -91,7 +93,7 @@
|
|||
<img
|
||||
:src="logo"
|
||||
:style="logoStyle"
|
||||
>
|
||||
/>
|
||||
</router-link>
|
||||
<div class="item right actions">
|
||||
<search-bar
|
||||
|
@ -106,7 +108,10 @@
|
|||
<router-link
|
||||
v-if="currentUser"
|
||||
class="nav-icon"
|
||||
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
|
||||
:to="{
|
||||
name: 'interactions',
|
||||
params: { username: currentUser.screen_name }
|
||||
}"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
|
@ -152,7 +157,10 @@
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||
v-if="
|
||||
(currentUser && currentUser.role === 'admin') ||
|
||||
currentUser.role === 'moderator'
|
||||
"
|
||||
class="button-unstyled nav-icon"
|
||||
@click.stop="openModModal"
|
||||
>
|
||||
|
|
|
@ -31,14 +31,14 @@
|
|||
.dark-overlay {
|
||||
&::before {
|
||||
bottom: 0;
|
||||
content: " ";
|
||||
content: ' ';
|
||||
display: block;
|
||||
cursor: default;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: rgba(27,31,35,.5);
|
||||
background: rgba(27, 31, 35, 0.5);
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@
|
|||
|
||||
.dialog-modal-footer {
|
||||
margin: 0;
|
||||
padding: .5em .5em;
|
||||
padding: 0.5em 0.5em;
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
border-top: 1px solid $fallback--border;
|
||||
|
@ -84,9 +84,8 @@
|
|||
|
||||
button {
|
||||
width: auto;
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
@ -19,7 +19,7 @@
|
|||
class="btn button-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template v-slot:progress>
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
|
|
|
@ -38,7 +38,9 @@ const EditStatusModal = {
|
|||
},
|
||||
isFormVisible(val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
|
||||
this.$nextTick(
|
||||
() => this.$el && this.$el.querySelector('textarea').focus()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -55,7 +57,8 @@ const EditStatusModal = {
|
|||
contentType
|
||||
}
|
||||
|
||||
return statusPosterService.editStatus(params)
|
||||
return statusPosterService
|
||||
.editStatus(params)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
@posted="closeModal"
|
||||
:disablePolls="true"
|
||||
:disableVisibilitySelector="true"
|
||||
:disable-polls="true"
|
||||
:disable-visibility-selector="true"
|
||||
:post-handler="doEditStatus"
|
||||
@posted="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
133
src/components/emoji_grid/emoji_grid.js
Normal file
133
src/components/emoji_grid/emoji_grid.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
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
|
60
src/components/emoji_grid/emoji_grid.scss
Normal file
60
src/components/emoji_grid/emoji_grid.scss
Normal file
|
@ -0,0 +1,60 @@
|
|||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
48
src/components/emoji_grid/emoji_grid.vue
Normal file
48
src/components/emoji_grid/emoji_grid.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<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,13 +4,9 @@ import { take } from 'lodash'
|
|||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSmileBeam
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faSmileBeam
|
||||
)
|
||||
library.add(faSmileBeam)
|
||||
|
||||
/**
|
||||
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
||||
|
@ -127,25 +123,30 @@ const EmojiInput = {
|
|||
return this.$store.getters.mergedConfig.padEmoji
|
||||
},
|
||||
showSuggestions() {
|
||||
return this.focused &&
|
||||
return (
|
||||
this.focused &&
|
||||
this.suggestions &&
|
||||
this.suggestions.length > 0 &&
|
||||
!this.showPicker &&
|
||||
!this.temporarilyHideSuggestions
|
||||
)
|
||||
},
|
||||
textAtCaret() {
|
||||
return (this.wordAtCaret || {}).word || ''
|
||||
},
|
||||
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() {
|
||||
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()
|
||||
|
@ -183,11 +184,12 @@ 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) {
|
||||
|
@ -205,7 +207,6 @@ const EmojiInput = {
|
|||
},
|
||||
triggerShowPicker() {
|
||||
this.showPicker = true
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
this.$nextTick(() => {
|
||||
this.scrollIntoView()
|
||||
this.focusPickerInput()
|
||||
|
@ -223,12 +224,15 @@ const EmojiInput = {
|
|||
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)
|
||||
const newValue = Completion.replaceWord(
|
||||
this.modelValue,
|
||||
this.wordAtCaret,
|
||||
replacement
|
||||
)
|
||||
this.$emit('update:modelValue', newValue)
|
||||
this.caret = 0
|
||||
},
|
||||
|
@ -251,19 +255,25 @@ const EmojiInput = {
|
|||
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
|
||||
*/
|
||||
const isSpaceRegex = /\s/
|
||||
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
|
||||
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
|
||||
const spaceBefore =
|
||||
surroundingSpace &&
|
||||
!isSpaceRegex.exec(before.slice(-1)) &&
|
||||
before.length &&
|
||||
this.padEmoji > 0
|
||||
? ' '
|
||||
: ''
|
||||
const spaceAfter =
|
||||
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
|
||||
? ' '
|
||||
: ''
|
||||
|
||||
const newValue = [
|
||||
before,
|
||||
spaceBefore,
|
||||
insertion,
|
||||
spaceAfter,
|
||||
after
|
||||
].join('')
|
||||
const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
|
||||
''
|
||||
)
|
||||
this.keepOpen = keepOpen
|
||||
this.$emit('update:modelValue', newValue)
|
||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
const position =
|
||||
this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
if (!keepOpen) {
|
||||
this.input.focus()
|
||||
}
|
||||
|
@ -278,9 +288,14 @@ const EmojiInput = {
|
|||
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
|
||||
|
@ -325,20 +340,22 @@ const EmojiInput = {
|
|||
* 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') ||
|
||||
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
|
||||
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
|
||||
|
@ -490,7 +507,10 @@ const EmojiInput = {
|
|||
},
|
||||
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 {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<EmojiPicker
|
||||
v-if="enableEmojiPicker"
|
||||
ref="picker"
|
||||
show-keep-open
|
||||
:class="{ hide: !showPicker }"
|
||||
:enable-sticker-picker="enableStickerPicker"
|
||||
class="emoji-picker-panel"
|
||||
|
@ -42,11 +43,14 @@
|
|||
:class="{ highlighted: index === highlighted }"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
>
|
||||
<span v-if="!suggestion.mfm" class="image">
|
||||
<span
|
||||
v-if="!suggestion.mfm"
|
||||
class="image"
|
||||
>
|
||||
<img
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
>
|
||||
/>
|
||||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<div class="label">
|
||||
|
@ -77,7 +81,7 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: .2em .25em;
|
||||
margin: 0.2em 0.25em;
|
||||
font-size: 1.3em;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
|
@ -93,7 +97,7 @@
|
|||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,7 +108,7 @@
|
|||
margin-top: 2px;
|
||||
|
||||
&.hide {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-body {
|
||||
|
@ -178,7 +182,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
input,
|
||||
textarea {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,26 @@
|
|||
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
|
||||
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
|
||||
const MFM_TAGS = [
|
||||
'blur',
|
||||
'bounce',
|
||||
'flip',
|
||||
'font',
|
||||
'jelly',
|
||||
'jump',
|
||||
'rainbow',
|
||||
'rotate',
|
||||
'shake',
|
||||
'sparkle',
|
||||
'spin',
|
||||
'tada',
|
||||
'twitch',
|
||||
'x2',
|
||||
'x3',
|
||||
'x4'
|
||||
].map((tag) => ({
|
||||
displayText: tag,
|
||||
detailText: '$[' + tag + ' ]',
|
||||
replacement: '$[' + tag + ' ]',
|
||||
mfm: true
|
||||
}))
|
||||
|
||||
/**
|
||||
* suggest - generates a suggestor function to be used by emoji-input
|
||||
|
@ -13,10 +34,10 @@ const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow',
|
|||
* doesn't support user linking you can just provide only emoji.
|
||||
*/
|
||||
|
||||
export default data => {
|
||||
export default (data) => {
|
||||
const emojiCurry = suggestEmoji(data.emoji)
|
||||
const usersCurry = data.store && suggestUsers(data.store)
|
||||
return input => {
|
||||
return (input) => {
|
||||
const firstChar = input[0]
|
||||
if (firstChar === ':' && data.emoji) {
|
||||
return emojiCurry(input)
|
||||
|
@ -25,14 +46,15 @@ export default data => {
|
|||
return usersCurry(input)
|
||||
}
|
||||
if (firstChar === '$') {
|
||||
return MFM_TAGS
|
||||
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
|
||||
return MFM_TAGS.filter(
|
||||
({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1
|
||||
)
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const suggestEmoji = emojis => input => {
|
||||
export const suggestEmoji = (emojis) => (input) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
return emojis
|
||||
.filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
|
||||
|
@ -85,7 +107,7 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
})
|
||||
}
|
||||
|
||||
return async input => {
|
||||
return async (input) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
if (previousQuery === noPrefix) return suggestions
|
||||
|
||||
|
@ -99,11 +121,14 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
await debounceUserSearch(noPrefix)
|
||||
}
|
||||
|
||||
const newSuggestions = state.users.users.filter(
|
||||
user =>
|
||||
const newSuggestions = state.users.users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)
|
||||
).slice(0, 20).sort((a, b) => {
|
||||
)
|
||||
.slice(0, 20)
|
||||
.sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
|
||||
|
@ -123,12 +148,20 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
/* eslint-disable camelcase */
|
||||
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||
})
|
||||
.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,5 +1,6 @@
|
|||
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,
|
||||
|
@ -8,18 +9,7 @@ import {
|
|||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { trim, escapeRegExp, startCase } from 'lodash'
|
||||
|
||||
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
|
||||
library.add(faBoxOpen, faStickyNote, faSmileBeam)
|
||||
|
||||
const EmojiPicker = {
|
||||
props: {
|
||||
|
@ -27,6 +17,11 @@ const EmojiPicker = {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showKeepOpen: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -34,16 +29,15 @@ const EmojiPicker = {
|
|||
keyword: '',
|
||||
activeGroup: 'standard',
|
||||
showingStickers: false,
|
||||
groupsScrolledClass: 'scrolled-top',
|
||||
keepOpen: false,
|
||||
customEmojiBufferSlice: LOAD_EMOJI_BY,
|
||||
customEmojiTimeout: null,
|
||||
customEmojiLoadAllConfirmed: false
|
||||
keepOpen: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||
Checkbox
|
||||
StickerPicker: defineAsyncComponent(() =>
|
||||
import('../sticker_picker/sticker_picker.vue')
|
||||
),
|
||||
Checkbox,
|
||||
EmojiGrid
|
||||
},
|
||||
methods: {
|
||||
onStickerUploaded(e) {
|
||||
|
@ -53,80 +47,25 @@ const EmojiPicker = {
|
|||
this.$emit('sticker-upload-failed', e)
|
||||
},
|
||||
onEmoji(emoji) {
|
||||
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||
const value = emoji.imageUrl
|
||||
? `:${emoji.displayText}:`
|
||||
: emoji.replacement
|
||||
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
|
||||
this.$store.commit('emojiUsed', emoji)
|
||||
},
|
||||
onScroll (e) {
|
||||
const target = (e && e.target) || this.$refs['emoji-groups']
|
||||
this.updateScrolledClass(target)
|
||||
this.scrolledGroup(target)
|
||||
this.triggerLoadMore(target)
|
||||
onWheel(e) {
|
||||
e.preventDefault()
|
||||
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
|
||||
},
|
||||
highlight(key) {
|
||||
this.setShowStickers(false)
|
||||
this.activeGroup = 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'
|
||||
if (this.keyword.length) {
|
||||
this.$refs.emojiGrid.scrollToItem(key)
|
||||
}
|
||||
},
|
||||
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()
|
||||
}
|
||||
},
|
||||
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
|
||||
onActiveGroup(group) {
|
||||
this.activeGroup = group
|
||||
},
|
||||
toggleStickers() {
|
||||
this.showingStickers = !this.showingStickers
|
||||
|
@ -137,18 +76,14 @@ const EmojiPicker = {
|
|||
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)
|
||||
return list.filter((emoji) => {
|
||||
return (
|
||||
regex.test(emoji.displayText) ||
|
||||
(!emoji.imageUrl && emoji.replacement === this.keyword)
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
keyword () {
|
||||
this.customEmojiLoadAllConfirmed = false
|
||||
this.onScroll()
|
||||
this.startEmojiLoad(true)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeGroupView() {
|
||||
return this.showingStickers ? '' : this.activeGroup
|
||||
|
@ -160,14 +95,10 @@ const EmojiPicker = {
|
|||
return 0
|
||||
},
|
||||
filteredEmoji() {
|
||||
return this.filterByKeyword(
|
||||
this.$store.state.instance.customEmoji || []
|
||||
)
|
||||
},
|
||||
customEmojiBuffer () {
|
||||
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
|
||||
return this.filterByKeyword(this.$store.state.instance.customEmoji || [])
|
||||
},
|
||||
emojis() {
|
||||
const recentEmojis = this.$store.getters.recentEmojis
|
||||
const standardEmojis = this.$store.state.instance.emoji || []
|
||||
const customEmojis = this.sortedEmoji
|
||||
const emojiPacks = []
|
||||
|
@ -180,6 +111,15 @@ const EmojiPicker = {
|
|||
})
|
||||
})
|
||||
return [
|
||||
{
|
||||
id: 'recent',
|
||||
text: this.$t('emoji.recent'),
|
||||
first: {
|
||||
imageUrl: '',
|
||||
replacement: '🕒'
|
||||
},
|
||||
emojis: this.filterByKeyword(recentEmojis)
|
||||
},
|
||||
{
|
||||
id: 'standard',
|
||||
text: this.$t('emoji.unicode'),
|
||||
|
@ -205,17 +145,20 @@ const EmojiPicker = {
|
|||
},
|
||||
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
|
||||
return (
|
||||
(this.$store.state.instance.stickers || []).length !== 0 &&
|
||||
this.enableStickerPicker
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
@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%;
|
||||
|
@ -7,7 +25,7 @@
|
|||
overflow: hidden;
|
||||
left: -70%;
|
||||
max-width: 100%;
|
||||
@media (min-width: 800px) and (max-width: 1300px) {
|
||||
@media (min-width: 800px) and (max-width: 1280px) {
|
||||
left: -50%;
|
||||
min-width: 50%;
|
||||
max-width: 130%;
|
||||
|
@ -18,6 +36,10 @@
|
|||
min-width: 50%;
|
||||
max-width: 130%;
|
||||
}
|
||||
|
||||
.Status > .emoji-picker {
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
.emoji-picker {
|
||||
|
@ -70,10 +92,6 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.emoji-groups {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.additional-tabs {
|
||||
border-left: 1px solid;
|
||||
border-left-color: $fallback--icon;
|
||||
|
@ -100,7 +118,7 @@
|
|||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: .4em;
|
||||
padding: 0.4em;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
|
@ -133,7 +151,7 @@
|
|||
}
|
||||
|
||||
.sticker-picker {
|
||||
flex: 1 1 auto
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.stickers,
|
||||
|
@ -152,15 +170,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
&-search {
|
||||
.emoji-search {
|
||||
padding: 5px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-groups {
|
||||
flex: 1 1 1px;
|
||||
|
@ -221,7 +237,5 @@
|
|||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<div class="emoji-picker panel panel-default panel-body">
|
||||
<div class="heading">
|
||||
<span class="emoji-tabs">
|
||||
<span
|
||||
ref="emoji-tabs"
|
||||
class="emoji-tabs"
|
||||
@wheel="onWheel"
|
||||
>
|
||||
<span
|
||||
v-for="group in emojis"
|
||||
:key="group.id"
|
||||
|
@ -13,11 +17,13 @@
|
|||
: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"
|
||||
|
@ -45,13 +51,17 @@
|
|||
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
|
||||
ref="emoji-groups"
|
||||
class="emoji-groups"
|
||||
:class="groupsScrolledClass"
|
||||
@scroll="onScroll"
|
||||
v-if="showKeepOpen"
|
||||
class="keep-open"
|
||||
>
|
||||
<div
|
||||
v-for="group in emojisView"
|
||||
|
@ -75,7 +85,7 @@
|
|||
<img
|
||||
v-else
|
||||
:src="emoji.imageUrl"
|
||||
>
|
||||
/>
|
||||
</span>
|
||||
<span :ref="'group-end-' + group.id" />
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,11 @@ 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: {
|
||||
|
@ -23,7 +28,9 @@ const EmojiReactions = {
|
|||
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
||||
},
|
||||
showMoreString() {
|
||||
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
|
||||
return `+${
|
||||
this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF
|
||||
}`
|
||||
},
|
||||
accountsForEmoji() {
|
||||
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
||||
|
@ -44,16 +51,18 @@ const EmojiReactions = {
|
|||
this.showAll = !this.showAll
|
||||
},
|
||||
reactedWith(emoji) {
|
||||
return this.status.emoji_reactions.find(r => r.name === emoji).me
|
||||
return this.status.emoji_reactions.find((r) => r.name === emoji).me
|
||||
},
|
||||
fetchEmojiReactionsByIfMissing() {
|
||||
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
||||
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
|
||||
if (hasNoAccounts) {
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
||||
}
|
||||
},
|
||||
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) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
|
|
|
@ -1,28 +1,35 @@
|
|||
<template>
|
||||
<div class="emoji-reactions">
|
||||
<UserListPopover
|
||||
v-for="(reaction) in emojiReactions"
|
||||
v-for="reaction in emojiReactions"
|
||||
:key="reaction.url || reaction.name"
|
||||
:users="accountsForEmoji[reaction.url || reaction.name]"
|
||||
>
|
||||
<button
|
||||
class="emoji-reaction btn button-default"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||
:class="{
|
||||
'picked-reaction': reactedWith(reaction.name),
|
||||
'not-clickable': !loggedIn
|
||||
}"
|
||||
@click="emojiOnClick(reaction.name, $event)"
|
||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
<span
|
||||
v-if="reaction.url !== null"
|
||||
class="emoji-button-inner"
|
||||
>
|
||||
<img
|
||||
:src="reaction.url"
|
||||
:title="reaction.name"
|
||||
class="reaction-emoji"
|
||||
width="2.55em"
|
||||
>
|
||||
/>
|
||||
{{ reaction.count }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<span
|
||||
v-else
|
||||
class="emoji-button-inner"
|
||||
>
|
||||
<span class="reaction-emoji unicode-emoji">
|
||||
{{ reaction.name }}
|
||||
</span>
|
||||
|
@ -49,10 +56,11 @@
|
|||
display: flex;
|
||||
margin-top: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.unicode-emoji {
|
||||
font-size: 210%;
|
||||
font-size: 128%;
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
|
@ -60,13 +68,20 @@
|
|||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
.reaction-emoji {
|
||||
width: 2.55em !important;
|
||||
width: auto;
|
||||
max-width: 96cqw;
|
||||
height: 2.55em !important;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
img.reaction-emoji {
|
||||
width: 1.55em !important;
|
||||
display: block;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
@ -93,9 +108,12 @@
|
|||
}
|
||||
|
||||
.button-default.picked-reaction {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
}
|
||||
background: none;
|
||||
padding: 1px 0.5em;
|
||||
|
||||
.emoji-button-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const Exporter = {
|
||||
props: {
|
||||
|
@ -26,17 +24,21 @@ const Exporter = {
|
|||
methods: {
|
||||
process() {
|
||||
this.processing = true
|
||||
this.getContent()
|
||||
.then((content) => {
|
||||
this.getContent().then((content) => {
|
||||
const fileToDownload = document.createElement('a')
|
||||
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
|
||||
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)
|
||||
setTimeout(() => {
|
||||
this.processing = false
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
faBookmark as faBookmarkReg,
|
||||
faFlag
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
library.add(
|
||||
faEllipsisH,
|
||||
|
@ -62,54 +63,75 @@ const ExtraButtons = {
|
|||
},
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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 })
|
||||
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 })
|
||||
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] })
|
||||
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', {
|
||||
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,
|
||||
|
@ -118,12 +140,23 @@ const ExtraButtons = {
|
|||
statusFiles: [...this.status.attachments],
|
||||
visibility: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
}))
|
||||
})
|
||||
)
|
||||
},
|
||||
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() {
|
||||
|
@ -134,8 +167,10 @@ const ExtraButtons = {
|
|||
}
|
||||
},
|
||||
doRedraftStatus() {
|
||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then(data => this.$store.dispatch('openPostStatusModal', {
|
||||
this.$store
|
||||
.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then((data) =>
|
||||
this.$store.dispatch('openPostStatusModal', {
|
||||
isRedraft: true,
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
|
@ -144,8 +179,10 @@ const ExtraButtons = {
|
|||
statusPoll: this.status.poll,
|
||||
statusFiles: [...this.status.attachments],
|
||||
statusScope: this.status.visibility,
|
||||
statusLanguage: this.status.language,
|
||||
statusContentType: data.content_type
|
||||
}))
|
||||
})
|
||||
)
|
||||
this.doDeleteStatus()
|
||||
},
|
||||
showRedraftStatusConfirmDialog() {
|
||||
|
@ -156,17 +193,26 @@ const ExtraButtons = {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
canDelete() {
|
||||
if (!this.currentUser) { return }
|
||||
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||
if (!this.currentUser) {
|
||||
return
|
||||
}
|
||||
const superuser =
|
||||
this.currentUser.rights.moderator || this.currentUser.rights.admin
|
||||
return superuser || this.status.user.id === this.currentUser.id
|
||||
},
|
||||
ownStatus() {
|
||||
return this.status.user.id === this.currentUser.id
|
||||
},
|
||||
canPin() {
|
||||
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
|
||||
return (
|
||||
this.ownStatus &&
|
||||
(this.status.visibility === 'public' ||
|
||||
this.status.visibility === 'unlisted')
|
||||
)
|
||||
},
|
||||
canMute() {
|
||||
return !!this.currentUser
|
||||
|
@ -179,7 +225,12 @@ const ExtraButtons = {
|
|||
},
|
||||
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
|
||||
}
|
||||
|
@ -190,7 +241,9 @@ const ExtraButtons = {
|
|||
isEdited() {
|
||||
return this.status.edited_at !== null
|
||||
},
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable }
|
||||
editingAvailable() {
|
||||
return this.$store.state.instance.editingAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:content="{close}">
|
||||
<template #content="{ close }">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
|
@ -17,7 +17,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="eye-slash"
|
||||
/><span>{{ $t("status.mute_conversation") }}</span>
|
||||
/><span>{{ $t('status.mute_conversation') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canMute && status.thread_muted"
|
||||
|
@ -27,7 +27,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="eye-slash"
|
||||
/><span>{{ $t("status.unmute_conversation") }}</span>
|
||||
/><span>{{ $t('status.unmute_conversation') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!status.pinned && canPin"
|
||||
|
@ -38,7 +38,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t("status.pin") }}</span>
|
||||
/><span>{{ $t('status.pin') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.pinned && canPin"
|
||||
|
@ -49,7 +49,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="thumbtack"
|
||||
/><span>{{ $t("status.unpin") }}</span>
|
||||
/><span>{{ $t('status.unpin') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!status.bookmarked"
|
||||
|
@ -60,7 +60,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
:icon="['far', 'bookmark']"
|
||||
/><span>{{ $t("status.bookmark") }}</span>
|
||||
/><span>{{ $t('status.bookmark') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.bookmarked"
|
||||
|
@ -71,7 +71,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="bookmark"
|
||||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
/><span>{{ $t('status.unbookmark') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="ownStatus && editingAvailable"
|
||||
|
@ -82,7 +82,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="pen"
|
||||
/><span>{{ $t("status.edit") }}</span>
|
||||
/><span>{{ $t('status.edit') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isEdited && editingAvailable"
|
||||
|
@ -93,7 +93,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="history"
|
||||
/><span>{{ $t("status.edit_history") }}</span>
|
||||
/><span>{{ $t('status.edit_history') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="ownStatus"
|
||||
|
@ -104,7 +104,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="file-pen"
|
||||
/><span>{{ $t("status.redraft") }}</span>
|
||||
/><span>{{ $t('status.redraft') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
|
@ -115,7 +115,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="times"
|
||||
/><span>{{ $t("status.delete") }}</span>
|
||||
/><span>{{ $t('status.delete') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -125,7 +125,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="share-alt"
|
||||
/><span>{{ $t("status.copy_link") }}</span>
|
||||
/><span>{{ $t('status.copy_link') }}</span>
|
||||
</button>
|
||||
<a
|
||||
v-if="!status.is_local"
|
||||
|
@ -137,7 +137,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="external-link-alt"
|
||||
/><span>{{ $t("status.external_source") }}</span>
|
||||
/><span>{{ $t('status.external_source') }}</span>
|
||||
</a>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
@ -147,7 +147,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
:icon="['far', 'flag']"
|
||||
/><span>{{ $t("user_card.report") }}</span>
|
||||
/><span>{{ $t('user_card.report') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canTranslate"
|
||||
|
@ -158,7 +158,7 @@
|
|||
<FAIcon
|
||||
fixed-width
|
||||
icon="globe"
|
||||
/><span>{{ $t("status.translate") }}</span>
|
||||
/><span>{{ $t('status.translate') }}</span>
|
||||
|
||||
<template v-if="noTranslationTargetSet">
|
||||
<span class="dropdown-item-icon__badge warning">
|
||||
|
@ -172,7 +172,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<template #trigger>
|
||||
<button class="button-unstyled popover-trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
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'],
|
||||
|
@ -33,7 +28,9 @@ const FavoriteButton = {
|
|||
computed: {
|
||||
...mapGetters(['mergedConfig']),
|
||||
remoteInteractionLink() {
|
||||
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||
return this.$store.getters.remoteInteractionLink({
|
||||
statusId: this.status.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
.interactive {
|
||||
.svg-inline--fa {
|
||||
animation-duration: 0.6s;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
&:hover .svg-inline--fa,
|
||||
|
|
|
@ -2,10 +2,20 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
|||
|
||||
const FeaturesPanel = {
|
||||
computed: {
|
||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||
textlimit: function () { return this.$store.state.instance.textlimit },
|
||||
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
|
||||
whoToFollow: function () {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
mediaProxy: function () {
|
||||
return this.$store.state.instance.mediaProxyAvailable
|
||||
},
|
||||
textlimit: function () {
|
||||
return this.$store.state.instance.textlimit
|
||||
},
|
||||
uploadlimit: function () {
|
||||
return fileSizeFormatService.fileSizeFormat(
|
||||
this.$store.state.instance.uploadlimit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
</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>
|
||||
|
|
|
@ -5,10 +5,7 @@ import {
|
|||
faExclamationTriangle
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faStop,
|
||||
faExclamationTriangle
|
||||
)
|
||||
library.add(faStop, faExclamationTriangle)
|
||||
|
||||
const Flash = {
|
||||
props: ['src'],
|
||||
|
@ -32,9 +29,12 @@ const Flash = {
|
|||
container.appendChild(player)
|
||||
player.style.width = '100%'
|
||||
player.style.height = '100%'
|
||||
player.load(this.src).then(() => {
|
||||
player
|
||||
.load(this.src)
|
||||
.then(() => {
|
||||
this.player = true
|
||||
}).catch((e) => {
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Error loading ruffle', e)
|
||||
this.player = 'error'
|
||||
})
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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: {
|
||||
|
@ -50,7 +53,9 @@ export default {
|
|||
this.showingConfirmUnfollow = false
|
||||
},
|
||||
onClick() {
|
||||
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
|
||||
this.relationship.following || this.relationship.requested
|
||||
? this.unfollow()
|
||||
: this.follow()
|
||||
},
|
||||
follow() {
|
||||
this.inProgress = true
|
||||
|
@ -70,7 +75,10 @@ export default {
|
|||
this.inProgress = true
|
||||
requestUnfollow(this.relationship.id, store).then(() => {
|
||||
this.inProgress = false
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
|
||||
store.commit('removeStatus', {
|
||||
timeline: 'friends',
|
||||
userId: this.relationship.id
|
||||
})
|
||||
})
|
||||
|
||||
this.hideConfirmUnfollow()
|
||||
|
|
|
@ -21,9 +21,7 @@
|
|||
tag="span"
|
||||
>
|
||||
<template #user>
|
||||
<span
|
||||
v-text="user.screen_name_ui"
|
||||
/>
|
||||
<span v-text="user.screen_name_ui" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
|
|
|
@ -4,10 +4,7 @@ import FollowButton from '../follow_button/follow_button.vue'
|
|||
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
||||
|
||||
const FollowCard = {
|
||||
props: [
|
||||
'user',
|
||||
'noFollowsYou'
|
||||
],
|
||||
props: ['user', 'noFollowsYou'],
|
||||
components: {
|
||||
BasicUserCard,
|
||||
RemoteFollow,
|
||||
|
|
|
@ -17,7 +17,9 @@ const FollowRequestCard = {
|
|||
methods: {
|
||||
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
|
||||
},
|
||||
|
@ -43,12 +45,13 @@ const FollowRequestCard = {
|
|||
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'
|
||||
}
|
||||
})
|
||||
|
@ -63,9 +66,11 @@ const FollowRequestCard = {
|
|||
},
|
||||
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()
|
||||
|
@ -80,6 +85,13 @@ const FollowRequestCard = {
|
|||
},
|
||||
shouldConfirmDeny() {
|
||||
return this.mergedConfig.modalOnDenyFollow
|
||||
},
|
||||
show() {
|
||||
const notifId = this.$store.state.api.followRequests.find(
|
||||
(req) => req.id === this.user.id
|
||||
)
|
||||
|
||||
return notifId !== undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<basic-user-card
|
||||
v-if="show"
|
||||
:user="user"
|
||||
>
|
||||
<div class="follow-request-card-content-container">
|
||||
<button
|
||||
class="btn button-default"
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
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
|
||||
FollowRequestCard,
|
||||
FollowRequestList
|
||||
},
|
||||
computed: {
|
||||
userId() {
|
||||
return this.$store.state.users.currentUser.id
|
||||
},
|
||||
requests() {
|
||||
return this.$store.state.api.followRequests
|
||||
}
|
||||
|
|
|
@ -6,12 +6,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<FollowRequestCard
|
||||
v-for="request in requests"
|
||||
:key="request.id"
|
||||
:user="request"
|
||||
class="list-item"
|
||||
/>
|
||||
<FollowRequestList :user-id="userId">
|
||||
<template #item="{ item }">
|
||||
<FollowRequestCard :user="item" />
|
||||
</template>
|
||||
</FollowRequestList>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
78
src/components/followed_tag_card/FollowedTagCard.vue
Normal file
78
src/components/followed_tag_card/FollowedTagCard.vue
Normal file
|
@ -0,0 +1,78 @@
|
|||
<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,7 @@ export default {
|
|||
components: {
|
||||
Select
|
||||
},
|
||||
props: [
|
||||
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
|
||||
],
|
||||
props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
|
@ -19,7 +17,7 @@ export default {
|
|||
'serif',
|
||||
'monospace',
|
||||
'sans-serif'
|
||||
].filter(_ => _)
|
||||
].filter((_) => _)
|
||||
}
|
||||
},
|
||||
beforeUpdate() {
|
||||
|
@ -46,10 +44,12 @@ export default {
|
|||
},
|
||||
preset: {
|
||||
get() {
|
||||
if (this.family === 'serif' ||
|
||||
if (
|
||||
this.family === 'serif' ||
|
||||
this.family === 'sans-serif' ||
|
||||
this.family === 'monospace' ||
|
||||
this.family === 'inherit') {
|
||||
this.family === 'inherit'
|
||||
) {
|
||||
return this.family
|
||||
} else {
|
||||
return 'custom'
|
||||
|
|
|
@ -15,8 +15,13 @@
|
|||
class="opt exlcude-disabled"
|
||||
type="checkbox"
|
||||
:checked="present"
|
||||
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||
>
|
||||
@change="
|
||||
$emit(
|
||||
'update:modelValue',
|
||||
typeof modelValue === 'undefined' ? fallback : undefined
|
||||
)
|
||||
"
|
||||
/>
|
||||
<label
|
||||
v-if="typeof fallback !== 'undefined'"
|
||||
class="opt-l"
|
||||
|
@ -43,7 +48,7 @@
|
|||
v-model="family"
|
||||
class="custom-font"
|
||||
type="text"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@ const FriendsTimeline = {
|
|||
Timeline
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.friends }
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.friends
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,35 +29,54 @@ const Gallery = {
|
|||
if (!this.attachments) {
|
||||
return []
|
||||
}
|
||||
const attachments = this.limit > 0
|
||||
const attachments =
|
||||
this.limit > 0
|
||||
? this.attachments.slice(0, this.limit)
|
||||
: this.attachments
|
||||
if (this.size === 'hide') {
|
||||
return attachments.map(item => ({ minimal: true, items: [item] }))
|
||||
return attachments.map((item) => ({ minimal: true, items: [item] }))
|
||||
}
|
||||
const rows = this.grid
|
||||
? [{ grid: true, items: attachments }]
|
||||
: attachments.reduce((acc, attachment, i) => {
|
||||
: attachments
|
||||
.reduce(
|
||||
(acc, attachment, i) => {
|
||||
if (attachment.mimetype.includes('audio')) {
|
||||
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
|
||||
return [
|
||||
...acc,
|
||||
{ audio: true, items: [attachment] },
|
||||
{ items: [] }
|
||||
]
|
||||
}
|
||||
if (!(
|
||||
if (
|
||||
!(
|
||||
attachment.mimetype.includes('image') ||
|
||||
attachment.mimetype.includes('video') ||
|
||||
attachment.mimetype.includes('flash')
|
||||
)) {
|
||||
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
|
||||
)
|
||||
) {
|
||||
return [
|
||||
...acc,
|
||||
{ minimal: true, items: [attachment] },
|
||||
{ items: [] }
|
||||
]
|
||||
}
|
||||
const maxPerRow = 3
|
||||
const attachmentsRemaining = this.attachments.length - i + 1
|
||||
const currentRow = acc[acc.length - 1].items
|
||||
currentRow.push(attachment)
|
||||
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
|
||||
if (
|
||||
currentRow.length >= maxPerRow &&
|
||||
attachmentsRemaining > maxPerRow
|
||||
) {
|
||||
return [...acc, { items: [] }]
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, [{ items: [] }]).filter(_ => _.items.length > 0)
|
||||
},
|
||||
[{ items: [] }]
|
||||
)
|
||||
.filter((_) => _.items.length > 0)
|
||||
return rows
|
||||
},
|
||||
attachmentsDimensionalScore() {
|
||||
|
@ -91,11 +110,11 @@ const Gallery = {
|
|||
if (row.audio) {
|
||||
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
||||
} else if (!row.minimal && !row.grid) {
|
||||
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
|
||||
return { 'padding-bottom': `${100 / (row.items.length + 0.6)}%` }
|
||||
}
|
||||
},
|
||||
itemStyle(id, row) {
|
||||
const total = sumBy(row, item => this.getAspectRatio(item.id))
|
||||
const total = sumBy(row, (item) => this.getAspectRatio(item.id))
|
||||
return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
|
||||
},
|
||||
getAspectRatio(id) {
|
||||
|
|
|
@ -25,11 +25,20 @@
|
|||
:size="size"
|
||||
:editable="editable"
|
||||
:remove="removeAttachment"
|
||||
:shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
|
||||
:shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
|
||||
:shift-up="
|
||||
!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment
|
||||
"
|
||||
:shift-dn="
|
||||
!(
|
||||
attachmentIndex === row.items.length - 1 &&
|
||||
rowIndex === rows.length - 1
|
||||
) && shiftDnAttachment
|
||||
"
|
||||
:edit="editAttachment"
|
||||
:description="descriptions && descriptions[attachment.id]"
|
||||
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
|
||||
:hide-description="
|
||||
size === 'small' || (tooManyAttachments && hidingLong)
|
||||
"
|
||||
:style="itemStyle(attachment.id, row.items)"
|
||||
@setMedia="onMedia"
|
||||
@naturalSizeLoad="onNaturalSizeLoad"
|
||||
|
@ -42,7 +51,7 @@
|
|||
class="many-attachments"
|
||||
>
|
||||
<div class="many-attachments-text">
|
||||
{{ $t("status.many_attachments", { number: attachments.length }) }}
|
||||
{{ $t('status.many_attachments', { number: attachments.length }) }}
|
||||
</div>
|
||||
<div class="many-attachments-buttons">
|
||||
<span
|
||||
|
@ -53,7 +62,7 @@
|
|||
class="button-unstyled -link"
|
||||
@click="toggleHidingLong(true)"
|
||||
>
|
||||
{{ $t("status.collapse_attachments") }}
|
||||
{{ $t('status.collapse_attachments') }}
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
|
@ -64,7 +73,7 @@
|
|||
class="button-unstyled -link"
|
||||
@click="toggleHidingLong(false)"
|
||||
>
|
||||
{{ $t("status.show_all_attachments") }}
|
||||
{{ $t('status.show_all_attachments') }}
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
|
@ -75,7 +84,7 @@
|
|||
class="button-unstyled -link"
|
||||
@click="openGallery"
|
||||
>
|
||||
{{ $t("status.open_gallery") }}
|
||||
{{ $t('status.open_gallery') }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -83,7 +92,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src='./gallery.js'></script>
|
||||
<script src="./gallery.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
@ -109,8 +118,8 @@
|
|||
.gallery-rows {
|
||||
max-height: 25em;
|
||||
overflow: hidden;
|
||||
mask:
|
||||
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px
|
||||
no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes
|
||||
)
|
||||
library.add(faTimes)
|
||||
|
||||
const GlobalNoticeList = {
|
||||
computed: {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<template>
|
||||
<span
|
||||
class="HashtagLink"
|
||||
>
|
||||
<span class="HashtagLink">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<a
|
||||
:href="url"
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import Cropper from 'cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCircleNotch
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const ImageCropper = {
|
||||
props: {
|
||||
|
@ -59,7 +55,10 @@ const ImageCropper = {
|
|||
return this.saveButtonLabel || this.$t('image_cropper.save')
|
||||
},
|
||||
saveWithoutCroppingText() {
|
||||
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping')
|
||||
return (
|
||||
this.saveWithoutCroppingButtonlabel ||
|
||||
this.$t('image_cropper.save_without_cropping')
|
||||
)
|
||||
},
|
||||
cancelText() {
|
||||
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
||||
|
@ -89,7 +88,9 @@ const ImageCropper = {
|
|||
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
|
||||
},
|
||||
getTriggerDOM() {
|
||||
return typeof this.trigger === 'object' ? this.trigger : document.querySelector(this.trigger)
|
||||
return typeof this.trigger === 'object'
|
||||
? this.trigger
|
||||
: document.querySelector(this.trigger)
|
||||
},
|
||||
readFile() {
|
||||
const fileInput = this.$refs.input
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue